import type { Script } from '@btc-vision/bitcoin';
import bitcoin, {
    equals,
    fromHex,
    getFinalScripts,
    type Network,
    opcodes,
    Psbt,
    type PsbtInputExtended,
    type PsbtOutputExtended,
    script,
    type Signer,
    toSatoshi,
    toXOnly,
    Transaction,
} from '@btc-vision/bitcoin';
import { witnessStackToScriptWitness } from '../utils/WitnessUtils.js';
import type { UpdateInput } from '../interfaces/Tap.js';
import { TransactionType } from '../enums/TransactionType.js';
import type {
    IFundingTransactionParameters,
    ITransactionParameters,
} from '../interfaces/ITransactionParameters.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
import type { UTXO } from '../../utxo/interfaces/IUTXO.js';
import { type UniversalSigner } from '@btc-vision/ecpair';
import { AddressVerificator } from '../../keypair/AddressVerificator.js';
import { TweakedTransaction } from '../shared/TweakedTransaction.js';
import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
import type { IP2WSHAddress } from '../mineable/IP2WSHAddress.js';
import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
import {
    type Feature,
    FeaturePriority,
    Features,
    type MLDSALinkRequest,
} from '../../generators/Features.js';
import { BITCOIN_PROTOCOL_ID, getChainId } from '../../chain/ChainData.js';
import { BinaryWriter } from '../../buffer/BinaryWriter.js';
import { MLDSASecurityLevel } from '@btc-vision/bip32';
import { MessageSigner } from '../../keypair/MessageSigner.js';
import { getLevelFromPublicKeyLength } from '../../generators/MLDSAData.js';

export const MINIMUM_AMOUNT_REWARD: bigint = 330n; //540n;
export const MINIMUM_AMOUNT_CA: bigint = 297n;
export const ANCHOR_SCRIPT = fromHex('51024e73');

/**
 * Allows to build a transaction like you would on Ethereum.
 * @description The transaction builder class
 * @abstract
 * @class TransactionBuilder
 */
export abstract class TransactionBuilder<T extends TransactionType> extends TweakedTransaction {
    public static readonly MINIMUM_DUST: bigint = 330n;

    public abstract readonly type: T;
    public override readonly logColor: string = '#785def';
    public debugFees: boolean = false;

    // Cancel script
    public LOCK_LEAF_SCRIPT: Script;

    /**
     * @description The overflow fees of the transaction
     * @public
     */
    public overflowFees: bigint = 0n;

    /**
     * @description Cost in satoshis of the transaction fee
     */
    public transactionFee: bigint = 0n;

    /**
     * @description The estimated fees of the transaction
     */
    public estimatedFees: bigint = 0n;

    /**
     * @param {ITransactionParameters} parameters - The transaction parameters
     */
    public optionalOutputs: PsbtOutputExtended[] | undefined;

    /**
     * @description The transaction itself.
     */
    protected transaction: Psbt;

    /**
     * @description Inputs to update later on.
     */
    protected readonly updateInputs: UpdateInput[] = [];

    /**
     * @description The outputs of the transaction
     */
    protected readonly outputs: PsbtOutputExtended[] = [];

    /**
     * @description Output that will be used to pay the fees
     */
    protected feeOutput: PsbtOutputExtended | null = null;

    /**
     * @description The total amount of satoshis in the inputs
     */
    protected totalInputAmount: bigint;

    /**
     * @description The signer of the transaction
     */
    protected override readonly signer: Signer | UniversalSigner | UnisatSigner;

    /**
     * @description The network where the transaction will be broadcasted
     */
    protected override readonly network: Network;

    /**
     * @description The fee rate of the transaction
     */
    protected readonly feeRate: number;

    /**
     * @description The opnet priority fee of the transaction
     */
    protected priorityFee: bigint;
    protected gasSatFee: bigint;

    /**
     * @description The utxos used in the transaction
     */
    protected utxos: UTXO[];

    /**
     * @description The inputs of the transaction
     * @protected
     */
    protected optionalInputs: UTXO[];

    /**
     * @description The address where the transaction is sent to
     * @protected
     */
    protected to: string | undefined;

    /**
     * @description The address where the transaction is sent from
     * @protected
     */
    protected from: string;

    /**
     * @description The maximum fee rate of the transaction
     */
    protected _maximumFeeRate: number = 100000000;

    /**
     * @description Is the destionation P2PK
     * @protected
     */
    protected isPubKeyDestination: boolean;

    /**
     * @description If the transaction need an anchor output
     * @protected
     */
    protected anchor: boolean;

    protected note?: Uint8Array;

    private optionalOutputsAdded: boolean = false;

    protected constructor(parameters: ITransactionParameters) {
        super(parameters);

        if (parameters.estimatedFees) {
            this.estimatedFees = parameters.estimatedFees;
        }

        this.signer = parameters.signer;
        this.network = parameters.network;
        this.feeRate = parameters.feeRate;
        this.priorityFee = parameters.priorityFee ?? 0n;
        this.gasSatFee = parameters.gasSatFee ?? 0n;
        this.utxos = parameters.utxos;
        this.optionalInputs = parameters.optionalInputs || [];
        this.to = parameters.to || undefined;
        this.debugFees = parameters.debugFees || false;

        this.LOCK_LEAF_SCRIPT = this.defineLockScript();

        if (parameters.note) {
            if (typeof parameters.note === 'string') {
                this.note = new TextEncoder().encode(parameters.note);
            } else {
                this.note = parameters.note;
            }
        }

        this.anchor = parameters.anchor ?? false;

        this.isPubKeyDestination = this.to
            ? AddressVerificator.isValidPublicKey(this.to, this.network)
            : false;

        this.optionalOutputs = parameters.optionalOutputs;

        this.from = TransactionBuilder.getFrom(parameters.from, this.signer, this.network);

        this.totalInputAmount = this.calculateTotalUTXOAmount();

        const totalVOut: bigint = this.calculateTotalVOutAmount();
        if (totalVOut < this.totalInputAmount) {
            throw new Error(`Vout value is less than the value to send`);
        }

        this.transaction = new Psbt({
            network: this.network,
            version: this.txVersion,
        });
    }

    public static getFrom(
        from: string | undefined,
        keypair: UniversalSigner | Signer,
        network: Network,
    ): string {
        return from || EcKeyPair.getTaprootAddress(keypair, network);
    }

    /**
     * @description Converts the witness stack to a script witness
     * @param {Uint8Array[]} witness - The witness stack
     * @protected
     * @returns {Uint8Array}
     */
    public static witnessStackToScriptWitness(witness: Uint8Array[]): Uint8Array {
        return witnessStackToScriptWitness(witness);
    }

    public override [Symbol.dispose](): void {
        super[Symbol.dispose]();

        this.updateInputs.length = 0;
        this.outputs.length = 0;
        this.feeOutput = null;
        this.optionalOutputs = undefined;
        this.utxos = [];
        this.optionalInputs = [];
    }

    public addOPReturn(buffer: Uint8Array): void {
        const compileScript = script.compile([opcodes.OP_RETURN, buffer]);

        this.addOutput({
            value: toSatoshi(0n),
            script: compileScript,
        });
    }

    public addAnchor(): void {
        this.addOutput({
            value: toSatoshi(0n),
            script: ANCHOR_SCRIPT as Script,
        });
    }

    public async getFundingTransactionParameters(): Promise<IFundingTransactionParameters> {
        if (!this.estimatedFees) {
            this.estimatedFees = await this.estimateTransactionFees();
        }

        return {
            utxos: this.utxos,
            to: this.getScriptAddress(),
            signer: this.signer,
            network: this.network,
            feeRate: this.feeRate,
            priorityFee: this.priorityFee ?? 0n,
            gasSatFee: this.gasSatFee ?? 0n,
            from: this.from,
            amount: this.estimatedFees,
            optionalInputs: this.optionalInputs,
            mldsaSigner: null,
            ...(this.optionalOutputs !== undefined
                ? { optionalOutputs: this.optionalOutputs }
                : {}),
        } satisfies IFundingTransactionParameters;
    }

    /**
     * Set the destination address of the transaction
     * @param {string} address - The address to set
     */
    public setDestinationAddress(address: string): void {
        this.to = address; // this.getScriptAddress()
    }

    /**
     * Set the maximum fee rate of the transaction in satoshis per byte
     * @param {number} feeRate - The fee rate to set
     * @public
     */
    public setMaximumFeeRate(feeRate: number): void {
        this._maximumFeeRate = feeRate;
    }

    /**
     * @description Signs the transaction
     * @public
     * @returns {Promise<Transaction>} - The signed transaction in hex format
     * @throws {Error} - If something went wrong
     */
    public async signTransaction(): Promise<Transaction> {
        if (!this.utxos.length) {
            throw new Error('No UTXOs specified');
        }

        if (
            this.to &&
            !this.isPubKeyDestination &&
            !EcKeyPair.verifyContractAddress(this.to, this.network)
        ) {
            throw new Error(
                'Invalid contract address. The contract address must be a taproot address.',
            );
        }

        if (this.signed) throw new Error('Transaction is already signed');
        this.signed = true;

        await this.buildTransaction();

        const builtTx = await this.internalBuildTransaction(this.transaction);
        if (builtTx) {
            if (this.regenerated) {
                throw new Error('Transaction was regenerated');
            }

            return this.transaction.extractTransaction(true, true);
        }

        throw new Error('Could not sign transaction');
    }

    /**
     * @description Generates the transaction minimal signatures
     * @public
     */
    public async generateTransactionMinimalSignatures(
        checkPartialSigs: boolean = false,
    ): Promise<void> {
        if (
            this.to &&
            !this.isPubKeyDestination &&
            !EcKeyPair.verifyContractAddress(this.to, this.network)
        ) {
            throw new Error(
                'Invalid contract address. The contract address must be a taproot address.',
            );
        }

        await this.buildTransaction();

        if (this.transaction.data.inputs.length === 0) {
            const inputs: PsbtInputExtended[] = this.getInputs();
            const outputs: PsbtOutputExtended[] = this.getOutputs();

            this.transaction.setMaximumFeeRate(this._maximumFeeRate);
            this.transaction.addInputs(inputs, checkPartialSigs);

            for (let i = 0; i < this.updateInputs.length; i++) {
                this.transaction.updateInput(i, this.updateInputs[i] as UpdateInput);
            }

            this.transaction.addOutputs(outputs);
        }
    }

    /**
     * @description Signs the transaction
     * @public
     * @returns {Promise<Psbt>} - The signed transaction in hex format
     * @throws {Error} - If something went wrong
     */
    public async signPSBT(): Promise<Psbt> {
        if (await this.signTransaction()) {
            return this.transaction;
        }

        throw new Error('Could not sign transaction');
    }

    /**
     * Add an input to the transaction.
     * @param {PsbtInputExtended} input - The input to add
     * @public
     * @returns {void}
     */
    public addInput(input: PsbtInputExtended): void {
        this.inputs.push(input);
    }

    /**
     * Add an output to the transaction.
     * @param {PsbtOutputExtended} output - The output to add
     * @param bypassMinCheck
     * @public
     * @returns {void}
     */
    public addOutput(output: PsbtOutputExtended, bypassMinCheck: boolean = false): void {
        if (output.value === toSatoshi(0n)) {
            const scriptOutput = output as {
                script: Uint8Array;
            };

            if (!scriptOutput.script || scriptOutput.script.length === 0) {
                throw new Error('Output value is 0 and no script provided');
            }

            if (scriptOutput.script.length < 2) {
                throw new Error('Output script is too short');
            }

            if (
                scriptOutput.script[0] !== opcodes.OP_RETURN &&
                !equals(scriptOutput.script, ANCHOR_SCRIPT)
            ) {
                throw new Error(
                    'Output script must start with OP_RETURN or be an ANCHOR when value is 0',
                );
            }
        } else if (!bypassMinCheck && BigInt(output.value) < TransactionBuilder.MINIMUM_DUST) {
            throw new Error(
                `Output value is less than the minimum dust ${output.value} < ${TransactionBuilder.MINIMUM_DUST}`,
            );
        }

        this.outputs.push(output);
    }

    /**
     * Returns the total value of all outputs added so far (excluding the fee/change output).
     * @public
     * @returns {bigint}
     */
    public getTotalOutputValue(): bigint {
        return this.outputs.reduce((total, output) => total + BigInt(output.value), 0n);
    }

    /**
     * Receiver address.
     * @public
     * @returns {string} - The receiver address
     */
    public toAddress(): string | undefined {
        return this.to;
    }

    /**
     * @description Returns the script address
     * @returns {string} - The script address
     */
    public address(): string | undefined {
        return this.tapData?.address;
    }

    /**
     * Estimates the transaction fees with accurate size calculation.
     *
     * @note The P2TR estimation is made for a 2-leaf tree with both a tapScriptSig and a tapInternalKey input, which is a common case for many transactions.
     * This provides a more accurate fee estimation for typical P2TR transactions, but may not be perfectly accurate for all possible script configurations.
     * Adjustments may be needed for more complex scripts or different leaf structures.
     *
     * @public
     * @returns {Promise<bigint>}
     */
    public async estimateTransactionFees(): Promise<bigint> {
        await Promise.resolve();

        const fakeTx = new Psbt({ network: this.network });
        const inputs = this.getInputs();
        const outputs = this.getOutputs();
        fakeTx.addInputs(inputs);
        fakeTx.addOutputs(outputs);

        const dummySchnorrSig = new Uint8Array(64);
        const dummyEcdsaSig = new Uint8Array(72);
        const dummyCompressedPubkey = new Uint8Array(33).fill(2);

        const finalizer = (inputIndex: number, input: PsbtInputExtended) => {
            if (input.isPayToAnchor || this.anchorInputIndices.has(inputIndex)) {
                return {
                    finalScriptSig: undefined,
                    finalScriptWitness: Uint8Array.from([0]),
                };
            }

            if (input.witnessScript && P2WDADetector.isP2WDAWitnessScript(input.witnessScript)) {
                // Create dummy witness stack for P2WDA
                const dummyDataSlots: Uint8Array[] = [];
                for (let i = 0; i < 10; i++) {
                    dummyDataSlots.push(new Uint8Array(0));
                }

                const dummyEcdsaSig = new Uint8Array(72);
                return {
                    finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                        ...dummyDataSlots,
                        dummyEcdsaSig,
                        input.witnessScript,
                    ]),
                };
            }

            if (inputIndex === 0 && this.tapLeafScript) {
                const dummySecret = new Uint8Array(32);
                const dummyScript = this.tapLeafScript.script;

                // A control block for a 2-leaf tree contains one 32-byte hash.
                // P2TR: 33 (version + internal pubkey) + 32 (merkle path) = 65 bytes
                // P2MR: 1 (version) + 32 (merkle path) = 33 bytes (no internal pubkey)
                const controlBlockSize = this.useP2MR ? 1 + 32 : 1 + 32 + 32;
                const dummyControlBlock = new Uint8Array(controlBlockSize);

                return {
                    finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                        dummySecret,
                        dummySchnorrSig, // It's a tapScriptSig, which is Schnorr
                        dummySchnorrSig, // Second Schnorr signature
                        dummyScript,
                        dummyControlBlock,
                    ]),
                };
            }

            if (input.witnessUtxo) {
                const script = input.witnessUtxo.script;
                const decompiled = bitcoin.script.decompile(script);
                if (
                    decompiled &&
                    decompiled.length === 5 &&
                    decompiled[0] === opcodes.OP_DUP &&
                    decompiled[1] === opcodes.OP_HASH160 &&
                    decompiled[3] === opcodes.OP_EQUALVERIFY &&
                    decompiled[4] === opcodes.OP_CHECKSIG
                ) {
                    return {
                        finalScriptSig: bitcoin.script.compile([
                            dummyEcdsaSig,
                            dummyCompressedPubkey,
                        ]),
                        finalScriptWitness: undefined,
                    };
                }
            }

            if (input.witnessScript) {
                if (this.csvInputIndices.has(inputIndex)) {
                    // CSV P2WSH needs: [signature, witnessScript]
                    return {
                        finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                            dummyEcdsaSig,
                            input.witnessScript,
                        ]),
                    };
                }

                if (input.redeemScript) {
                    // P2SH-P2WSH needs redeemScript in scriptSig and witness data
                    const dummyWitness = [dummyEcdsaSig, input.witnessScript];
                    return {
                        finalScriptSig: input.redeemScript,
                        finalScriptWitness:
                            TransactionBuilder.witnessStackToScriptWitness(dummyWitness),
                    };
                }

                const decompiled = bitcoin.script.decompile(input.witnessScript);
                if (decompiled && decompiled.length >= 4) {
                    const firstOp = decompiled[0];
                    const lastOp = decompiled[decompiled.length - 1];
                    // Check if it's M-of-N multisig
                    if (
                        typeof firstOp === 'number' &&
                        firstOp >= opcodes.OP_1 &&
                        lastOp === opcodes.OP_CHECKMULTISIG
                    ) {
                        const m = firstOp - opcodes.OP_1 + 1;
                        const signatures: Uint8Array[] = [];
                        for (let i = 0; i < m; i++) {
                            signatures.push(dummyEcdsaSig);
                        }

                        return {
                            finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                                new Uint8Array(0), // OP_0 due to multisig bug
                                ...signatures,
                                input.witnessScript,
                            ]),
                        };
                    }
                }

                return {
                    finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                        dummyEcdsaSig,
                        input.witnessScript,
                    ]),
                };
            } else if (input.redeemScript) {
                const decompiled = bitcoin.script.decompile(input.redeemScript);
                if (
                    decompiled &&
                    decompiled.length === 2 &&
                    decompiled[0] === opcodes.OP_0 &&
                    decompiled[1] instanceof Uint8Array &&
                    decompiled[1].length === 20
                ) {
                    // P2SH-P2WPKH
                    return {
                        finalScriptSig: input.redeemScript,
                        finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                            dummyEcdsaSig,
                            dummyCompressedPubkey,
                        ]),
                    };
                }
            }

            if (input.redeemScript && !input.witnessScript && !input.witnessUtxo) {
                // Pure P2SH needs signatures + redeemScript in scriptSig
                return {
                    finalScriptSig: bitcoin.script.compile([dummyEcdsaSig, input.redeemScript]),
                    finalScriptWitness: undefined,
                };
            }

            const inputScript = input.witnessUtxo?.script;
            if (!inputScript) return { finalScriptSig: undefined, finalScriptWitness: undefined };

            if (input.tapInternalKey) {
                return {
                    finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                        dummySchnorrSig,
                    ]),
                };
            }

            if (inputScript.length === 22 && inputScript[0] === opcodes.OP_0) {
                return {
                    finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                        dummyEcdsaSig,
                        dummyCompressedPubkey,
                    ]),
                };
            }

            if (input.redeemScript?.length === 22 && input.redeemScript[0] === opcodes.OP_0) {
                return {
                    finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness([
                        dummyEcdsaSig,
                        dummyCompressedPubkey,
                    ]),
                };
            }

            return getFinalScripts(
                inputIndex,
                input,
                inputScript as Script,
                true,
                !!input.redeemScript,
                !!input.witnessScript,
            );
        };

        try {
            for (let i = 0; i < fakeTx.data.inputs.length; i++) {
                const fullInput = inputs[i];
                if (fullInput) {
                    fakeTx.finalizeInput(i, (idx: number) => finalizer(idx, fullInput));
                }
            }
        } catch (e) {
            this.warn(`Could not finalize dummy tx: ${(e as Error).message}`);
        }

        const tx = fakeTx.extractTransaction(true, true);
        const size = tx.virtualSize();
        const fee = this.feeRate * size;
        const finalFee = BigInt(Math.ceil(fee));

        if (this.debugFees) {
            this.log(
                `Estimating fees: feeRate=${this.feeRate}, accurate_vSize=${size}, fee=${finalFee}n`,
            );
        }

        return finalFee;
    }

    public async rebuildFromBase64(base64: string): Promise<Psbt> {
        this.transaction = Psbt.fromBase64(base64, {
            network: this.network,
            version: this.txVersion,
        });

        this.signed = false;

        this.sighashTypes = [Transaction.SIGHASH_ANYONECANPAY, Transaction.SIGHASH_ALL];

        return await this.signPSBT();
    }

    public setPSBT(psbt: Psbt): void {
        this.transaction = psbt;
    }

    /**
     * Returns the inputs of the transaction.
     * @protected
     * @returns {PsbtInputExtended[]}
     */
    public getInputs(): PsbtInputExtended[] {
        return this.inputs;
    }

    /**
     * Returns the outputs of the transaction.
     * @protected
     * @returns {PsbtOutputExtended[]}
     */
    public getOutputs(): PsbtOutputExtended[] {
        const outputs: PsbtOutputExtended[] = [...this.outputs];
        if (this.feeOutput) outputs.push(this.feeOutput);

        return outputs;
    }

    public getOptionalOutputValue(): bigint {
        if (!this.optionalOutputs) return 0n;

        let total = 0n;
        for (let i = 0; i < this.optionalOutputs.length; i++) {
            total += BigInt((this.optionalOutputs[i] as PsbtOutputExtended).value);
        }

        return total;
    }

    protected async addRefundOutput(
        amountSpent: bigint,
        expectRefund: boolean = false,
    ): Promise<void> {
        if (this.note) {
            this.addOPReturn(this.note);
        }

        if (this.anchor) {
            this.addAnchor();
        }

        // Add a dummy change output to estimate fee with the change-output shape
        this.feeOutput = this.createChangeOutput(TransactionBuilder.MINIMUM_DUST);
        const feeWithChange = await this.estimateTransactionFees();

        const sendBackAmount = this.totalInputAmount - amountSpent - feeWithChange;

        if (this.debugFees) {
            this.log(
                `Fee with change: ${feeWithChange} sats, inputAmount=${this.totalInputAmount}, amountSpent=${amountSpent}, sendBackAmount=${sendBackAmount}`,
            );
        }

        if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
            // Change output is viable, set it to the real value
            this.feeOutput = this.createChangeOutput(sendBackAmount);
            this.overflowFees = sendBackAmount;
            this.transactionFee = feeWithChange;
        } else {
            // Change output not viable, remove it and re-estimate without it
            this.feeOutput = null;
            this.overflowFees = 0n;

            const feeWithoutChange = await this.estimateTransactionFees();
            this.transactionFee = feeWithoutChange;

            if (this.debugFees) {
                this.warn(
                    `Amount to send back (${sendBackAmount} sat) is less than minimum dust. Fee without change: ${feeWithoutChange} sats`,
                );
            }

            if (this.totalInputAmount <= amountSpent) {
                throw new Error(
                    `Insufficient funds: need ${amountSpent + feeWithoutChange} sats but only have ${this.totalInputAmount} sats`,
                );
            }

            if (expectRefund && sendBackAmount < 0n) {
                throw new Error(
                    `Insufficient funds: need at least ${-sendBackAmount} more sats to cover fees.`,
                );
            }
        }

        if (this.debugFees) {
            this.log(
                `Final fee: ${this.transactionFee} sats, Change output: ${this.feeOutput ? `${this.feeOutput.value} sats` : 'none'}`,
            );
        }
    }

    protected defineLockScript(): Script {
        return script.compile([toXOnly(this.signer.publicKey), opcodes.OP_CHECKSIG]);
    }

    /**
     * @description Adds the value to the output
     * @param {number | bigint} value - The value to add
     * @protected
     * @returns {void}
     */
    protected addValueToToOutput(value: number | bigint): void {
        if (BigInt(value) < TransactionBuilder.MINIMUM_DUST) {
            throw new Error(
                `Value to send is less than the minimum dust ${value} < ${TransactionBuilder.MINIMUM_DUST}`,
            );
        }

        for (let i = 0; i < this.outputs.length; i++) {
            const output = this.outputs[i] as PsbtOutputExtended;
            if ('address' in output && output.address === this.to) {
                this.outputs[i] = {
                    ...output,
                    value: toSatoshi(BigInt(output.value) + BigInt(value)),
                } as PsbtOutputExtended;
                return;
            }
        }

        throw new Error('Output not found');
    }

    protected generateLegacySignature(): Uint8Array {
        this.tweakSigner();

        if (!this.tweakedSigner) {
            throw new Error('Tweaked signer is not defined');
        }

        const tweakedKey = toXOnly(this.tweakedSigner.publicKey);
        const originalKey = this.signer.publicKey;
        if (originalKey.length !== 33) {
            throw new Error('Original public key must be compressed (33 bytes)');
        }

        const chainId = getChainId(this.network);

        const writer = new BinaryWriter();

        // ONLY SUPPORT MLDSA-44 FOR NOW.
        writer.writeU8(MLDSASecurityLevel.LEVEL2);
        writer.writeBytes(this.hashedPublicKey);
        writer.writeBytes(tweakedKey);
        writer.writeBytes(originalKey);
        writer.writeBytes(BITCOIN_PROTOCOL_ID);
        writer.writeBytes(chainId);

        const message = writer.getBuffer();
        const signature = MessageSigner.signMessage(this.tweakedSigner, message);
        const isValid = MessageSigner.verifySignature(tweakedKey, message, signature.signature);

        if (!isValid) {
            throw new Error('Could not verify generated legacy signature for MLDSA link request');
        }

        return new Uint8Array(signature.signature);
    }

    protected generateMLDSASignature(): Uint8Array {
        if (!this.mldsaSigner) {
            throw new Error('MLDSA signer is not defined');
        }

        this.tweakSigner();

        if (!this.tweakedSigner) {
            throw new Error('Tweaked signer is not defined');
        }

        const tweakedKey = toXOnly(this.tweakedSigner.publicKey);
        const originalKey = this.signer.publicKey;
        if (originalKey.length !== 33) {
            throw new Error('Original public key must be compressed (33 bytes)');
        }

        const chainId = getChainId(this.network);
        const level = getLevelFromPublicKeyLength(this.mldsaSigner.publicKey.length);

        if (level !== MLDSASecurityLevel.LEVEL2) {
            throw new Error('Only MLDSA level 2 is supported for link requests');
        }

        const writer = new BinaryWriter();
        writer.writeU8(level);
        writer.writeBytes(this.hashedPublicKey);
        writer.writeBytes(this.mldsaSigner.publicKey);
        writer.writeBytes(tweakedKey);
        writer.writeBytes(originalKey);
        writer.writeBytes(BITCOIN_PROTOCOL_ID);
        writer.writeBytes(chainId);

        const message = writer.getBuffer();
        const signature = MessageSigner.signMLDSAMessage(this.mldsaSigner, message);

        const isValid = MessageSigner.verifyMLDSASignature(
            this.mldsaSigner,
            message,
            signature.signature,
        );

        if (!isValid) {
            throw new Error('Could not verify generated MLDSA signature for link request');
        }

        return new Uint8Array(signature.signature);
    }

    protected generateMLDSALinkRequest(
        parameters: ITransactionParameters,
        features: Feature<Features>[],
    ): void {
        const mldsaSigner = this.mldsaSigner;
        const legacySignature = this.generateLegacySignature();

        let mldsaSignature: Uint8Array | null = null;
        if (parameters.revealMLDSAPublicKey) {
            mldsaSignature = this.generateMLDSASignature();
        }

        const mldsaRequest: MLDSALinkRequest = {
            priority: FeaturePriority.MLDSA_LINK_PUBKEY,
            opcode: Features.MLDSA_LINK_PUBKEY,
            data: {
                verifyRequest: !!parameters.revealMLDSAPublicKey,
                publicKey: mldsaSigner.publicKey,
                hashedPublicKey: this.hashedPublicKey,
                level: getLevelFromPublicKeyLength(mldsaSigner.publicKey.length),
                legacySignature: legacySignature,
                mldsaSignature: mldsaSignature,
            },
        };

        features.push(mldsaRequest);
    }

    /**
     * @description Returns the transaction opnet fee
     * @protected
     * @returns {bigint}
     */
    protected getTransactionOPNetFee(): bigint {
        const totalFee = this.priorityFee + this.gasSatFee;
        if (totalFee > TransactionBuilder.MINIMUM_DUST) {
            return totalFee;
        }

        return TransactionBuilder.MINIMUM_DUST;
    }

    /**
     * @description Returns the total amount of satoshis in the inputs
     * @protected
     * @returns {bigint}
     */
    protected calculateTotalUTXOAmount(): bigint {
        let total: bigint = 0n;
        for (const utxo of this.utxos) {
            total += utxo.value;
        }

        for (const utxo of this.optionalInputs) {
            total += utxo.value;
        }

        return total;
    }

    /**
     * @description Returns the total amount of satoshis in the outputs
     * @protected
     * @returns {bigint}
     */
    protected calculateTotalVOutAmount(): bigint {
        let total: bigint = 0n;
        for (const utxo of this.utxos) {
            total += utxo.value;
        }

        for (const utxo of this.optionalInputs) {
            total += utxo.value;
        }

        return total;
    }

    /**
     * @description Adds optional outputs to transaction and returns their total value in satoshi to calculate refund transaction
     * @protected
     * @returns {bigint}
     */
    protected addOptionalOutputsAndGetAmount(): bigint {
        if (!this.optionalOutputs || this.optionalOutputsAdded) return 0n;

        let refundedFromOptionalOutputs: bigint = 0n;

        for (let i = 0; i < this.optionalOutputs.length; i++) {
            this.addOutput(this.optionalOutputs[i] as PsbtOutputExtended);
            refundedFromOptionalOutputs += BigInt(
                (this.optionalOutputs[i] as PsbtOutputExtended).value,
            );
        }

        this.optionalOutputsAdded = true;

        return refundedFromOptionalOutputs;
    }

    /**
     * @description Adds the inputs from the utxos
     * @protected
     * @returns {void}
     */
    protected addInputsFromUTXO(): void {
        if (this.utxos.length) {
            //throw new Error('No UTXOs specified');

            if (this.totalInputAmount < TransactionBuilder.MINIMUM_DUST) {
                throw new Error(
                    `Total input amount is ${this.totalInputAmount} sat which is less than the minimum dust ${TransactionBuilder.MINIMUM_DUST} sat.`,
                );
            }

            for (let i = 0; i < this.utxos.length; i++) {
                const utxo = this.utxos[i] as UTXO;

                // Register signer BEFORE generating input (needed for tapInternalKey)
                this.registerInputSigner(i, utxo);

                const input = this.generatePsbtInputExtended(utxo, i);
                this.addInput(input);
            }
        }

        if (this.optionalInputs) {
            for (
                let i = this.utxos.length;
                i < this.optionalInputs.length + this.utxos.length;
                i++
            ) {
                const utxo = this.optionalInputs[i - this.utxos.length] as UTXO;

                // Register signer BEFORE generating input (needed for tapInternalKey)
                this.registerInputSigner(i, utxo);

                const input = this.generatePsbtInputExtended(utxo, i, true);
                this.addInput(input);
            }
        }
    }

    /**
     * Internal init.
     * @protected
     */
    protected override internalInit(): void {
        this.verifyUTXOValidity();

        super.internalInit();
    }

    /**
     * Builds the transaction.
     * @protected
     * @returns {Promise<void>}
     */
    protected abstract buildTransaction(): Promise<void>;

    /**
     * Add an input update
     * @param {UpdateInput} input - The input to update
     * @protected
     * @returns {void}
     */
    protected updateInput(input: UpdateInput): void {
        this.updateInputs.push(input);
    }

    /**
     * Adds the fee to the output.
     * @param amountSpent
     * @param contractAddress
     * @param epochChallenge
     * @param addContractOutput
     * @protected
     */
    protected addFeeToOutput(
        amountSpent: bigint,
        contractAddress: string,
        epochChallenge: IP2WSHAddress,
        addContractOutput: boolean,
    ): void {
        if (addContractOutput) {
            let amountToCA: bigint;
            if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
                amountToCA = MINIMUM_AMOUNT_CA;
            } else {
                amountToCA = amountSpent;
            }

            // ALWAYS THE FIRST INPUT.
            this.addOutput(
                {
                    value: toSatoshi(amountToCA),
                    address: contractAddress,
                },
                true,
            );

            // ALWAYS SECOND.
            if (
                amountToCA === MINIMUM_AMOUNT_CA &&
                amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD
            ) {
                this.addOutput(
                    {
                        value: toSatoshi(amountSpent - amountToCA),
                        address: epochChallenge.address,
                    },
                    true,
                );
            }
        } else {
            // When SEND_AMOUNT_TO_CA is false, always send to epochChallenge
            // Use the maximum of amountSpent or MINIMUM_AMOUNT_REWARD
            const amountToEpoch =
                amountSpent < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : amountSpent;

            this.addOutput(
                {
                    value: toSatoshi(amountToEpoch),
                    address: epochChallenge.address,
                },
                true,
            );
        }
    }

    /**
     * Returns the witness of the tap transaction.
     * @protected
     * @returns {Uint8Array}
     */
    protected getWitness(): Uint8Array {
        if (!this.tapData || !this.tapData.witness) {
            throw new Error('Witness is required');
        }

        if (this.tapData.witness.length === 0) {
            throw new Error('Witness is empty');
        }

        return this.tapData.witness[this.tapData.witness.length - 1] as Uint8Array;
    }

    /**
     * Returns the tap output.
     * @protected
     * @returns {Uint8Array}
     */
    protected getTapOutput(): Uint8Array {
        if (!this.tapData || !this.tapData.output) {
            throw new Error('Tap data is required');
        }

        return this.tapData.output;
    }

    /**
     * Verifies that the utxos are valid.
     * @protected
     */
    protected verifyUTXOValidity(): void {
        for (const utxo of this.utxos) {
            if (!utxo.scriptPubKey) {
                throw new Error('Address is required');
            }
        }

        for (const utxo of this.optionalInputs) {
            if (!utxo.scriptPubKey) {
                throw new Error('Address is required');
            }
        }
    }

    /**
     * Builds the transaction.
     * @param {Psbt} transaction - The transaction to build
     * @param checkPartialSigs
     * @protected
     * @returns {Promise<boolean>}
     * @throws {Error} - If something went wrong while building the transaction
     */
    protected async internalBuildTransaction(
        transaction: Psbt,
        checkPartialSigs: boolean = false,
    ): Promise<boolean> {
        if (transaction.data.inputs.length === 0) {
            const inputs: PsbtInputExtended[] = this.getInputs();
            const outputs: PsbtOutputExtended[] = this.getOutputs();

            transaction.setMaximumFeeRate(this._maximumFeeRate);
            transaction.addInputs(inputs, checkPartialSigs);

            for (let i = 0; i < this.updateInputs.length; i++) {
                transaction.updateInput(i, this.updateInputs[i] as UpdateInput);
            }

            transaction.addOutputs(outputs);
        }

        try {
            await this.signInputs(transaction);

            if (this.finalized) {
                this.transactionFee = BigInt(transaction.getFee());
            }

            return true;
        } catch (e) {
            const err: Error = e as Error;

            this.error(
                `[internalBuildTransaction] Something went wrong while getting building the transaction: ${err.stack}`,
            );
        }

        return false;
    }

    private createChangeOutput(amount: bigint): PsbtOutputExtended {
        if (AddressVerificator.isValidP2TRAddress(this.from, this.network)) {
            return {
                value: toSatoshi(amount),
                address: this.from,
                tapInternalKey: this.internalPubKeyToXOnly(),
            };
        } else if (AddressVerificator.isValidPublicKey(this.from, this.network)) {
            const pubKeyScript = script.compile([
                fromHex(this.from.startsWith('0x') ? this.from.slice(2) : this.from),
                opcodes.OP_CHECKSIG,
            ]);

            return {
                value: toSatoshi(amount),
                script: pubKeyScript,
            };
        } else {
            return {
                value: toSatoshi(amount),
                address: this.from,
            };
        }
    }
}
