import { crypto } from "bitcoinjs-lib";
import { secp256k1 } from "@noble/curves/secp256k1";
import { BufferWriter, PsbtV2 } from "@ledgerhq/psbtv2";
import { HASH_SIZE, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160 } from "../constants";
import { hashPublicKey } from "../hashPublicKey";
import { DefaultDescriptorTemplate } from "./policy";

// Helper function to convert bytes to bigint for scalar operations
function bytesToBigInt(bytes: Uint8Array): bigint {
  const hex = Array.from(bytes)
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");
  return BigInt("0x" + hex);
}

// Replacement for pointAddScalar from tiny-secp256k1
function pointAddScalar(point: Uint8Array, scalar: Uint8Array): Uint8Array | null {
  try {
    const p = secp256k1.ProjectivePoint.fromHex(point);
    const s = bytesToBigInt(scalar);
    const result = p.add(secp256k1.ProjectivePoint.BASE.multiply(s));
    return result.toRawBytes(point.length === 33);
  } catch {
    return null;
  }
}

/**
 * Computes the taproot output key (BIP341) from an internal x-only pubkey.
 * Standalone so derivation population can build a hash-based lookup without AccountType.
 */
export function computeTaprootOutputKey(internalXonlyPubkey: Buffer): Buffer {
  if (internalXonlyPubkey.length !== 32) {
    throw new Error("Expected 32 byte pubkey. Got " + internalXonlyPubkey.length);
  }
  const h = crypto.sha256(Buffer.from("TapTweak", "utf-8"));
  const tweak = crypto.sha256(Buffer.concat([h, h, internalXonlyPubkey]));
  const evenEcdsaPubkey = Buffer.concat([Buffer.from([0x02]), internalXonlyPubkey]);
  const tweakedKey = pointAddScalar(evenEcdsaPubkey, tweak);
  if (!tweakedKey) throw new Error("Point addition failed");
  return Buffer.from(tweakedKey).subarray(1);
}

export type SpendingCondition = {
  scriptPubKey: Buffer;
  redeemScript?: Buffer;
  // Possible future extension:
  // witnessScript?: Buffer; // For p2wsh witnessScript
  // tapScript?: {tapPath: Buffer[], script: Buffer} // For taproot
};

export type SpentOutput = { cond: SpendingCondition; amount: Buffer };

/**
 * Encapsulates differences between account types, for example p2wpkh,
 * p2wpkhWrapped, p2tr.
 */
export interface AccountType {
  /**
   * Generates a scriptPubKey (output script) from a list of public keys. If a
   * p2sh redeemScript or a p2wsh witnessScript is needed it will also be set on
   * the returned SpendingCondition.
   *
   * The pubkeys are expected to be 33 byte ecdsa compressed pubkeys.
   */
  spendingCondition(pubkeys: Buffer[]): SpendingCondition;

  /**
   * Populates the psbt with account type-specific data for an input.
   * @param i The index of the input map to populate
   * @param inputTx The full transaction containing the spent output. This may
   * be omitted for taproot.
   * @param spentOutput The amount and spending condition of the spent output
   * @param pubkeys The 33 byte ecdsa compressed public keys involved in the input
   * @param pathElems The paths corresponding to the pubkeys, in same order.
   */
  setInput(
    i: number,
    inputTx: Buffer | undefined,
    spentOutput: SpentOutput,
    pubkeys: Buffer[],
    pathElems: number[][],
  ): void;

  /**
   * Populates the psbt with account type-specific data for an output. This is typically
   * done for change outputs and other outputs that goes to the same account as
   * being spent from.
   * @param i The index of the output map to populate
   * @param cond The spending condition for this output
   * @param pubkeys The 33 byte ecdsa compressed public keys involved in this output
   * @param paths The paths corresponding to the pubkeys, in same order.
   */
  setOwnOutput(i: number, cond: SpendingCondition, pubkeys: Buffer[], paths: number[][]): void;

  /**
   * Returns the descriptor template for this account type. Currently only
   * DefaultDescriptorTemplates are allowed, but that might be changed in the
   * future. See class WalletPolicy for more information on descriptor
   * templates.
   */
  getDescriptorTemplate(): DefaultDescriptorTemplate;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface BaseAccount extends AccountType {}

abstract class BaseAccount implements AccountType {
  constructor(
    protected psbt: PsbtV2,
    protected masterFp: Buffer,
  ) {}
}

/**
 * Superclass for single signature accounts. This will make sure that the pubkey
 * arrays and path arrays in the method arguments contains exactly one element
 * and calls an abstract method to do the actual work.
 */
abstract class SingleKeyAccount extends BaseAccount {
  spendingCondition(pubkeys: Buffer[]): SpendingCondition {
    if (pubkeys.length != 1) {
      throw new Error("Expected single key, got " + pubkeys.length);
    }
    return this.singleKeyCondition(pubkeys[0]);
  }
  protected abstract singleKeyCondition(pubkey: Buffer): SpendingCondition;

  setInput(
    i: number,
    inputTx: Buffer | undefined,
    spentOutput: SpentOutput,
    pubkeys: Buffer[],
    pathElems: number[][],
  ) {
    if (pubkeys.length != 1) {
      throw new Error("Expected single key, got " + pubkeys.length);
    }
    if (pathElems.length != 1) {
      throw new Error("Expected single path, got " + pathElems.length);
    }
    this.setSingleKeyInput(i, inputTx, spentOutput, pubkeys[0], pathElems[0]);
  }
  protected abstract setSingleKeyInput(
    i: number,
    inputTx: Buffer | undefined,
    spentOutput: SpentOutput,
    pubkey: Buffer,
    path: number[],
  );

  setOwnOutput(i: number, cond: SpendingCondition, pubkeys: Buffer[], paths: number[][]) {
    if (pubkeys.length != 1) {
      throw new Error("Expected single key, got " + pubkeys.length);
    }
    if (paths.length != 1) {
      throw new Error("Expected single path, got " + paths.length);
    }
    this.setSingleKeyOutput(i, cond, pubkeys[0], paths[0]);
  }
  protected abstract setSingleKeyOutput(
    i: number,
    cond: SpendingCondition,
    pubkey: Buffer,
    path: number[],
  );
}

export class p2pkh extends SingleKeyAccount {
  singleKeyCondition(pubkey: Buffer): SpendingCondition {
    const buf = new BufferWriter();
    const pubkeyHash = hashPublicKey(pubkey);
    buf.writeSlice(Buffer.from([OP_DUP, OP_HASH160, HASH_SIZE]));
    buf.writeSlice(pubkeyHash);
    buf.writeSlice(Buffer.from([OP_EQUALVERIFY, OP_CHECKSIG]));
    return { scriptPubKey: buf.buffer() };
  }

  setSingleKeyInput(
    i: number,
    inputTx: Buffer | undefined,
    _spentOutput: SpentOutput,
    pubkey: Buffer,
    path: number[],
  ) {
    if (!inputTx) {
      throw new Error("Full input base transaction required");
    }
    this.psbt.setInputNonWitnessUtxo(i, inputTx);
    this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
  }

  setSingleKeyOutput(i: number, _cond: SpendingCondition, pubkey: Buffer, path: number[]) {
    this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
  }

  getDescriptorTemplate(): DefaultDescriptorTemplate {
    return "pkh(@0/**)";
  }
}

export class p2tr extends SingleKeyAccount {
  singleKeyCondition(pubkey: Buffer): SpendingCondition {
    const xonlyPubkey = pubkey.subarray(1); // x-only pubkey
    const buf = new BufferWriter();
    const outputKey = this.getTaprootOutputKey(xonlyPubkey);
    buf.writeSlice(Buffer.from([0x51, 32])); // push1, pubkeylen
    buf.writeSlice(outputKey);
    return { scriptPubKey: buf.buffer() };
  }

  setSingleKeyInput(
    i: number,
    _inputTx: Buffer | undefined,
    spentOutput: SpentOutput,
    pubkey: Buffer,
    path: number[],
  ) {
    const xonly = pubkey.subarray(1);
    this.psbt.setInputTapBip32Derivation(i, xonly, [], this.masterFp, path);
    this.psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.cond.scriptPubKey);
  }

  setSingleKeyOutput(i: number, _cond: SpendingCondition, pubkey: Buffer, path: number[]) {
    const xonly = pubkey.subarray(1);
    this.psbt.setOutputTapBip32Derivation(i, xonly, [], this.masterFp, path);
  }

  getDescriptorTemplate(): DefaultDescriptorTemplate {
    return "tr(@0/**)";
  }

  /**
   * Calculates a taproot output key from an internal key (BIP341).
   *
   * @param internalPubkey A 32 byte x-only taproot internal key
   * @returns The output key
   */
  getTaprootOutputKey(internalPubkey: Buffer): Buffer {
    return computeTaprootOutputKey(internalPubkey);
  }
}

export class p2wpkhWrapped extends SingleKeyAccount {
  singleKeyCondition(pubkey: Buffer): SpendingCondition {
    const buf = new BufferWriter();
    const redeemScript = this.createRedeemScript(pubkey);
    const scriptHash = hashPublicKey(redeemScript);
    buf.writeSlice(Buffer.from([OP_HASH160, HASH_SIZE]));
    buf.writeSlice(scriptHash);
    buf.writeUInt8(OP_EQUAL);
    return { scriptPubKey: buf.buffer(), redeemScript: redeemScript };
  }

  setSingleKeyInput(
    i: number,
    inputTx: Buffer | undefined,
    spentOutput: SpentOutput,
    pubkey: Buffer,
    path: number[],
  ) {
    if (!inputTx) {
      throw new Error("Full input base transaction required");
    }
    this.psbt.setInputNonWitnessUtxo(i, inputTx);
    this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);

    const userSuppliedRedeemScript = spentOutput.cond.redeemScript;
    const expectedRedeemScript = this.createRedeemScript(pubkey);
    if (userSuppliedRedeemScript && !expectedRedeemScript.equals(userSuppliedRedeemScript)) {
      // At what point might a user set the redeemScript on its own?
      throw new Error(`User-supplied redeemScript ${userSuppliedRedeemScript.toString(
        "hex",
      )} doesn't
       match expected ${expectedRedeemScript.toString("hex")} for input ${i}`);
    }
    this.psbt.setInputRedeemScript(i, expectedRedeemScript);
    this.psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.cond.scriptPubKey);
  }

  setSingleKeyOutput(i: number, cond: SpendingCondition, pubkey: Buffer, path: number[]) {
    if (cond.redeemScript) this.psbt.setOutputRedeemScript(i, cond.redeemScript);
    this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
  }

  getDescriptorTemplate(): DefaultDescriptorTemplate {
    return "sh(wpkh(@0/**))";
  }

  private createRedeemScript(pubkey: Buffer): Buffer {
    const pubkeyHash = hashPublicKey(pubkey);
    return Buffer.concat([Buffer.from("0014", "hex"), pubkeyHash]);
  }
}

export class p2wpkh extends SingleKeyAccount {
  singleKeyCondition(pubkey: Buffer): SpendingCondition {
    const buf = new BufferWriter();
    const pubkeyHash = hashPublicKey(pubkey);
    buf.writeSlice(Buffer.from([0, HASH_SIZE]));
    buf.writeSlice(pubkeyHash);
    return { scriptPubKey: buf.buffer() };
  }

  setSingleKeyInput(
    i: number,
    inputTx: Buffer | undefined,
    spentOutput: SpentOutput,
    pubkey: Buffer,
    path: number[],
  ) {
    if (!inputTx) {
      throw new Error("Full input base transaction required");
    }
    this.psbt.setInputNonWitnessUtxo(i, inputTx);
    this.psbt.setInputBip32Derivation(i, pubkey, this.masterFp, path);
    this.psbt.setInputWitnessUtxo(i, spentOutput.amount, spentOutput.cond.scriptPubKey);
  }

  setSingleKeyOutput(i: number, cond: SpendingCondition, pubkey: Buffer, path: number[]) {
    this.psbt.setOutputBip32Derivation(i, pubkey, this.masterFp, path);
  }

  getDescriptorTemplate(): DefaultDescriptorTemplate {
    return "wpkh(@0/**)";
  }
}
