import {
  HashValue,
  LockTime,
  OutPoint,
  Script,
  Sequence,
  TxBuilder,
  TxOut,
  Value,
} from '@node-dlc/bitcoin';

import { CommitmentNumber } from './CommitmentNumber';
import { Htlc } from './Htlc';
import { HtlcDirection } from './HtlcDirection';
import { ScriptFactory } from './ScriptFactory';

export class TxFactory {
  /**
   * Creates a TxOut to attach to a funding transaction. This includes
   * the P2WSH-P2MS script that uses 2-2MS. The open and accept funding
   * pubkeys are sorted lexicographcially to create the script.
   * @param builder
   */
  public static createFundingOutput(
    value: Value,
    openPubKey: Buffer,
    acceptPubKey: Buffer,
  ): TxOut {
    const script = Script.p2wshLock(
      ScriptFactory.fundingScript(openPubKey, acceptPubKey),
    );
    return new TxOut(value, script);
  }

  /**
   * Constructs an unsigned commitment transaction according to BOLT3.
   * This method is a low level commitment transaction builder, meaning
   * it accepts primatives and constructs a commitment transaction
   * accordingly. The proper inputs are determiend
   *
   * @param isFunderLocal True when the funding node is local. This
   * is used to determine which output pays fees (to_local/to_remote).
   * @param commitmentNumber The commitment number of the transaction
   * which is used to generate the obscurred commitment number.
   * @param openPaymentBasePoint The basepoint sent in open_channel
   * which is used to generate the obscurred commitment number.
   * @param acceptPaymentBasePoint The basepoitn sent in accept_channel
   * which is used to generate the obscurred commitment number.
   * @param fundingOutPoint The outpoint of the funding transaction
   * which was established in funding_created.
   * @param dustLimitSatoshi The dust limit in sats after which outputs
   * will be prune
   * @param feePerKw The fee rate per kiloweight which will be deducted
   * from the funding node's output
   * @param localDelay The delay applied to the to_local output
   * @param localValue Value paid to the to_local RSMC output
   * @param remoteValue Value paid to the to_emote P2WPKH output
   * @param revocationPubKey The revocation public key used to in the
   * to_local and HTLC outputs
   * @param delayedPubKey The delayed public key used to spend the
   * to_local output
   * @param remotePubKey The public key used to spend the to_remote
   * output
   * @param reverseHtlcs True when the HTLC direction needs to be
   * inverted because the holder of this commitment transaction is
   * our counterparty.
   * @param localHtlcPubKey The public key used to spend HTLC outputs
   * by the commitment holder.
   * @param remoteHtlcPubKey The public key used to spend HTLC outputs
   * by the commitment counterparty.
   * @param htlcs A full list of HTLCs that will be selectively
   * included in the commitment transaction based on the feePerKw.
   */
  public static createCommitment(
    isFunderLocal: boolean,
    commitmentNumber: number,
    openPaymentBasePoint: Buffer,
    acceptPaymentBasePoint: Buffer,
    fundingOutPoint: OutPoint,
    dustLimitSatoshi: Value,
    feePerKw: bigint,
    localDelay: number,
    localValue: Value,
    remoteValue: Value,
    revocationPubKey: Buffer,
    delayedPubKey: Buffer,
    remotePubKey: Buffer,
    reverseHtlcs: boolean,
    localHtlcPubKey?: Buffer,
    remoteHtlcPubKey?: Buffer,
    htlcs: Htlc[] = [],
  ): [TxBuilder, Htlc[]] {
    const obscuredCommitmentNumber = CommitmentNumber.obscure(
      commitmentNumber,
      openPaymentBasePoint,
      acceptPaymentBasePoint,
    );

    // 1. add the input as the funding outpoint and set the nSequence
    const tx = new TxBuilder();
    tx.version = 2;
    tx.addInput(
      fundingOutPoint,
      CommitmentNumber.getSequence(obscuredCommitmentNumber),
    );

    // 2. set the locktime to the obscurred commitment number
    tx.locktime = CommitmentNumber.getLockTime(obscuredCommitmentNumber);

    // 3. find unpruned outputs
    const unprunedHtlcs: Htlc[] = [];
    for (const htlc of htlcs) {
      const valueInSats = htlc.value.sats;
      let feeWeight: bigint;

      // HtlcDirection refers to the local nodes perception of the HTLC.
      // When isLocal, offered uses the HTLC-Timeout weight of 663. When
      // remote, the commitment is for the remote counterparty and an
      // offered HTLC is received and will be spent by the remote
      // counterparty using the HTLC-Success transaction with a weight of 703
      if (reverseHtlcs) {
        feeWeight =
          htlc.direction === HtlcDirection.Offered ? BigInt(703) : BigInt(663);
      } else {
        feeWeight =
          htlc.direction === HtlcDirection.Offered ? BigInt(663) : BigInt(703);
      }

      // Calculate the HTLC less fees
      const feeInSats = (feeWeight * feePerKw) / BigInt(1000);
      const satsLessFee = valueInSats - feeInSats;

      // Only keep HTLCs greater than the dustLimitSatoshi for the tx
      if (satsLessFee >= dustLimitSatoshi.sats) {
        unprunedHtlcs.push(htlc);
      }
    }

    // 4. calculate base fee
    const weight = 724 + unprunedHtlcs.length * 172;
    const baseFee = (BigInt(weight) * feePerKw) / BigInt(1000);

    // 5. substract base fee from funding node
    if (isFunderLocal) {
      const newValue = localValue.sats - baseFee;
      if (newValue > BigInt(0)) {
        localValue = Value.fromSats(newValue);
      } else {
        localValue = Value.zero();
      }
    } else {
      const newValue = remoteValue.sats - baseFee;
      if (newValue > BigInt(0)) {
        remoteValue = Value.fromSats(newValue);
      } else {
        remoteValue = Value.zero();
      }
    }

    // 6/7. add unpruned offered/received HTLCs
    const txouts: Array<[TxOut, Htlc?]> = [];
    for (const htlc of unprunedHtlcs) {
      const witnessScript: Script =
        (!reverseHtlcs && htlc.direction === HtlcDirection.Offered) ||
        (reverseHtlcs && htlc.direction === HtlcDirection.Accepted)
          ? ScriptFactory.offeredHtlcScript(
              htlc.paymentHash,
              revocationPubKey,
              localHtlcPubKey,
              remoteHtlcPubKey,
            )
          : ScriptFactory.receivedHtlcScript(
              htlc.paymentHash,
              htlc.cltvExpiry,
              revocationPubKey,
              localHtlcPubKey,
              remoteHtlcPubKey,
            );
      const txout = new TxOut(htlc.value, Script.p2wshLock(witnessScript));
      txouts.push([txout, htlc]);
    }

    // 8. add local if unpruned
    if (localValue.sats >= dustLimitSatoshi.sats) {
      txouts.push([
        new TxOut(
          localValue,
          Script.p2wshLock(
            ScriptFactory.toLocalScript(
              revocationPubKey,
              delayedPubKey,
              localDelay,
            ),
          ),
        ),
      ]);
    }

    // 9. add remote if unpruned
    if (remoteValue.sats >= dustLimitSatoshi.sats) {
      txouts.push([new TxOut(remoteValue, Script.p2wpkhLock(remotePubKey))]);
    }

    // 10. sort outputs using bip69 and using cltv for htlc tiebreaks
    txouts.sort((a, b) => {
      // compare on value
      const value = Number(a[0].value.sats - b[0].value.sats);
      if (value !== 0) return value;

      // compare on script
      const scriptCompare = a[0].scriptPubKey
        .serializeCmds()
        .compare(b[0].scriptPubKey.serializeCmds());
      if (scriptCompare !== 0) return scriptCompare;

      // tie-break on htlcs
      return b[1].cltvExpiry - a[1].cltvExpiry;
    });

    // add hte outputs in sorted order
    const sortedHtlcs: Htlc[] = [];
    for (const [txout, htlc] of txouts) {
      tx.addOutput(txout);
      sortedHtlcs.push(htlc);
    }

    // return the tuple with the sorted htlcs
    return [tx, sortedHtlcs];
  }

  /**
   * Constructs an HTLC-Timeout transaction as defined in BOLT3. This
   * transaction spends an offered HTLC from the commitment transaction
   * and outputs the HTLC value less the fee. The output is spendable
   * via an RSMC that is sequence locked for the received by the
   * transaction owner. Finally this transaction has an absolute
   * locktime of the HTLC's cltv expiry.
   * @param commitmentTx
   * @param outputIndex
   * @param localDelay
   * @param revocationPubKey
   * @param delayedPubKey
   * @param feePerKw
   * @param htlc
   */
  public static createHtlcTimeout(
    commitmentTx: HashValue,
    outputIndex: number,
    localDelay: number,
    revocationPubKey: Buffer,
    delayedPubKey: Buffer,
    feePerKw: bigint,
    htlc: Htlc,
  ): TxBuilder {
    const tx = new TxBuilder();

    // Input points to the commmitment transaction and the BIP69
    // sorted index of the HTLC. nSequence is set to zero.
    tx.addInput(new OutPoint(commitmentTx, outputIndex), Sequence.zero());

    // calc value less fees for this transaction
    const weight = BigInt(663);
    const fees = (weight * feePerKw) / BigInt(1000);
    const sats = fees > htlc.value.sats ? 0 : htlc.value.sats - fees;

    // Spends a P2WSH RSMC
    tx.addOutput(
      Value.fromSats(sats),
      Script.p2wshLock(
        ScriptFactory.toLocalScript(
          revocationPubKey,
          delayedPubKey,
          localDelay,
        ),
      ),
    );

    // nLocktime is set to the cltvExpiry of the HTLC. This prevents
    // the HTLC-Timeout from being broadcast until after the expiry
    // has been reached.
    tx.locktime = new LockTime(htlc.cltvExpiry);

    return tx;
  }

  /**
   * Constructs an HTLC-Success transaction as defined in BOLT3. This
   * transaction spends a received HTLC form the commitment transaction
   * and outputs the HTLC value less the fee. The output is spendable
   * via an RSMC that is sequence locked for the received by the
   * transaction owner.
   * @param commitmentTx
   * @param outputIndex
   * @param localDelay
   * @param revocationPubKey
   * @param delayedPubKey
   * @param feePerKw
   * @param htlc
   */
  public static createHtlcSuccess(
    commitmentTx: HashValue,
    outputIndex: number,
    localDelay: number,
    revocationPubKey: Buffer,
    delayedPubKey: Buffer,
    feePerKw: bigint,
    htlc: Htlc,
  ): TxBuilder {
    const tx = new TxBuilder();

    // Input points to the commmitment transaction and the BIP69
    // sorted index of the HTLC. nSequence is set to zero.
    tx.addInput(new OutPoint(commitmentTx, outputIndex), Sequence.zero());

    // calc value less fees for this transaction
    const weight = BigInt(703);
    const fees = (weight * feePerKw) / BigInt(1000);
    const sats = fees > htlc.value.sats ? 0 : htlc.value.sats - fees;

    // Spends a P2WSH RSMC
    tx.addOutput(
      Value.fromSats(sats),
      Script.p2wshLock(
        ScriptFactory.toLocalScript(
          revocationPubKey,
          delayedPubKey,
          localDelay,
        ),
      ),
    );

    // nLockTime is zero since the tx owner can immediately spend
    // this transaction if they have the preimage
    tx.locktime = LockTime.zero();

    return tx;
  }
}
