import { BufferWriter, psbtIn, PsbtV2 } from "@ledgerhq/psbtv2";

/**
 * This roughly implements the "input finalizer" role of BIP370 (PSBTv2
 * https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki). However
 * the role is documented in BIP174 (PSBTv0
 * https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki).
 *
 * Verify that all inputs have a signature, and set inputFinalScriptwitness
 * and/or inputFinalScriptSig depending on the type of the spent outputs. Clean
 * fields that aren't useful anymore, partial signatures, redeem script and
 * derivation paths.
 *
 * @param psbt The psbt with all signatures added as partial sigs, either
 * through PSBT_IN_PARTIAL_SIG or PSBT_IN_TAP_KEY_SIG
 */
export function finalize(psbt: PsbtV2): void {
  // First check that each input has a signature
  const inputCount = psbt.getGlobalInputCount();
  for (let i = 0; i < inputCount; i++) {
    const legacyPubkeys = psbt.getInputKeyDatas(i, psbtIn.PARTIAL_SIG);
    const taprootSig = psbt.getInputTapKeySig(i);
    if (legacyPubkeys.length == 0 && !taprootSig) {
      throw Error(`No signature for input ${i} present`);
    }
    if (legacyPubkeys.length > 0) {
      if (legacyPubkeys.length > 1) {
        throw Error(`Expected exactly one signature, got ${legacyPubkeys.length}`);
      }
      if (taprootSig) {
        throw Error("Both taproot and non-taproot signatures present.");
      }

      const isSegwitV0 = !!psbt.getInputWitnessUtxo(i);
      const redeemScript = psbt.getInputRedeemScript(i);
      const isWrappedSegwit = !!redeemScript;
      const signature = psbt.getInputPartialSig(i, legacyPubkeys[0]);
      if (!signature) throw new Error("Expected partial signature for input " + i);
      if (isSegwitV0) {
        const witnessBuf = new BufferWriter();
        witnessBuf.writeVarInt(2);
        witnessBuf.writeVarInt(signature.length);
        witnessBuf.writeSlice(signature);
        witnessBuf.writeVarInt(legacyPubkeys[0].length);
        witnessBuf.writeSlice(legacyPubkeys[0]);
        psbt.setInputFinalScriptwitness(i, witnessBuf.buffer());
        if (isWrappedSegwit) {
          if (!redeemScript || redeemScript.length == 0) {
            throw new Error("Expected non-empty redeemscript. Can't finalize intput " + i);
          }
          const scriptSigBuf = new BufferWriter();
          // Push redeemScript length
          scriptSigBuf.writeUInt8(redeemScript.length);
          scriptSigBuf.writeSlice(redeemScript);
          psbt.setInputFinalScriptsig(i, scriptSigBuf.buffer());
        }
      } else {
        // Legacy input
        const scriptSig = new BufferWriter();
        writePush(scriptSig, signature);
        writePush(scriptSig, legacyPubkeys[0]);
        psbt.setInputFinalScriptsig(i, scriptSig.buffer());
      }
    } else {
      // Taproot input
      const signature = psbt.getInputTapKeySig(i);
      if (!signature) {
        throw Error("No taproot signature found");
      }
      if (signature.length != 64 && signature.length != 65) {
        throw Error("Unexpected length of schnorr signature.");
      }
      const witnessBuf = new BufferWriter();
      witnessBuf.writeVarInt(1);
      witnessBuf.writeVarSlice(signature);
      psbt.setInputFinalScriptwitness(i, witnessBuf.buffer());
    }
    clearFinalizedInput(psbt, i);
  }
}

/**
 * Deletes fields that are no longer neccesary from the psbt.
 *
 * Note, the spec doesn't say anything about removing ouput fields
 * like PSBT_OUT_BIP32_DERIVATION_PATH and others, so we keep them
 * without actually knowing why. I think we should remove them too.
 */
function clearFinalizedInput(psbt: PsbtV2, inputIndex: number) {
  const keyTypes = [
    psbtIn.BIP32_DERIVATION,
    psbtIn.PARTIAL_SIG,
    psbtIn.TAP_BIP32_DERIVATION,
    psbtIn.TAP_KEY_SIG,
  ];
  const witnessUtxoAvailable = !!psbt.getInputWitnessUtxo(inputIndex);
  const nonWitnessUtxoAvailable = !!psbt.getInputNonWitnessUtxo(inputIndex);
  if (witnessUtxoAvailable && nonWitnessUtxoAvailable) {
    // Remove NON_WITNESS_UTXO for segwit v0 as it's only needed while signing.
    // Segwit v1 doesn't have NON_WITNESS_UTXO set.
    // See https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#cite_note-7
    keyTypes.push(psbtIn.NON_WITNESS_UTXO);
  }
  psbt.deleteInputEntries(inputIndex, keyTypes);
}

/**
 * Writes a script push operation to buf, which looks different
 * depending on the size of the data. See
 * https://en.bitcoin.it/wiki/Script#Constants
 *
 * @param buf the BufferWriter to write to
 * @param data the Buffer to be pushed.
 */
function writePush(buf: BufferWriter, data: Buffer) {
  if (data.length <= 75) {
    buf.writeUInt8(data.length);
  } else if (data.length <= 256) {
    buf.writeUInt8(76);
    buf.writeUInt8(data.length);
  } else if (data.length <= 256 * 256) {
    buf.writeUInt8(77);
    const b = Buffer.alloc(2);
    b.writeUInt16LE(data.length, 0);
    buf.writeSlice(b);
  }
  buf.writeSlice(data);
}
