import { fromHex, Psbt, type PsbtInput, type Script, toSatoshi, toXOnly, Transaction, } from '@btc-vision/bitcoin';
import { type UniversalSigner } from '@btc-vision/ecpair';
import { TransactionType } from '../enums/TransactionType.js';
import { MINIMUM_AMOUNT_REWARD, TransactionBuilder } from './TransactionBuilder.js';
import { HashCommitmentGenerator } from '../../generators/builders/HashCommitmentGenerator.js';
import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js';
import type {
    IConsolidatedInteractionParameters,
    IConsolidatedInteractionResult,
    IHashCommittedP2WSH,
    IRevealTransactionResult,
    ISetupTransactionResult,
} from '../interfaces/IConsolidatedTransactionParameters.js';
import type { IP2WSHAddress } from '../mineable/IP2WSHAddress.js';
import { TimeLockGenerator } from '../mineable/TimelockGenerator.js';
import type { IChallengeSolution } from '../../epoch/interfaces/IChallengeSolution.js';
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
import { Compressor } from '../../bytecode/Compressor.js';
import { type Feature, FeaturePriority, Features } from '../../generators/Features.js';
import { AddressGenerator } from '../../generators/AddressGenerator.js';

/**
 * Consolidated Interaction Transaction
 *
 * Drop-in replacement for InteractionTransaction that bypasses BIP110/Bitcoin Knots censorship.
 *
 * Uses the same parameters and sends the same data on-chain as InteractionTransaction,
 * but embeds data in hash-committed P2WSH witnesses instead of Tapscript.
 *
 * Data is split into 80-byte chunks (P2WSH stack item limit), with up to 14 chunks
 * batched per P2WSH output (~1,120 bytes per output). Each output's witness script
 * commits to all its chunks via HASH160. When spent, all data chunks are revealed
 * in the witness and verified at consensus level.
 *
 * Policy limits respected:
 * - MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80 bytes per chunk
 * - g_script_size_policy_limit = 1650 bytes total witness size (serialized)
 * - MAX_STANDARD_P2WSH_STACK_ITEMS = 100 items per witness
 *
 * Data integrity is consensus-enforced: if any data is stripped or modified,
 * HASH160(data) != committed_hash and the transaction is INVALID.
 *
 * Capacity: ~1.1KB per P2WSH output, ~220 outputs per reveal tx, ~242KB max.
 *
 * Usage:
 * ```typescript
 * // Same parameters as InteractionTransaction
 * const tx = new ConsolidatedInteractionTransaction({
 *     calldata: myCalldata,
 *     to: contractAddress,
 *     contract: contractSecret,
 *     challenge: myChallenge,
 *     utxos: myUtxos,
 *     signer: mySigner,
 *     network: networks.bitcoin,
 *     feeRate: 10,
 *     priorityFee: 0n,
 *     gasSatFee: 330n,
 * });
 *
 * const result = await tx.build();
 * // Broadcast setup first, then reveal (can use CPFP)
 * broadcast(result.setup.txHex);
 * broadcast(result.reveal.txHex);
 * ```
 */
export class ConsolidatedInteractionTransaction extends TransactionBuilder<TransactionType.INTERACTION> {
    public readonly type: TransactionType.INTERACTION = TransactionType.INTERACTION;
    /** Random bytes for interaction (same as InteractionTransaction) */
    public readonly randomBytes: Uint8Array;
    /** The contract address (same as InteractionTransaction.to) */
    protected readonly contractAddress: string;
    /** The contract secret - 32 bytes (same as InteractionTransaction) */
    protected readonly contractSecret: Uint8Array;
    /** The compressed calldata (same as InteractionTransaction) */
    protected readonly calldata: Uint8Array;
    /** Challenge solution for epoch (same as InteractionTransaction) */
    protected readonly challenge: IChallengeSolution;
    /** Epoch challenge P2WSH address (same as InteractionTransaction) */
    protected readonly epochChallenge: IP2WSHAddress;
    /** Script signer for interaction (same as InteractionTransaction) */
    protected readonly scriptSigner: UniversalSigner;

    /** Calldata generator - produces same output as InteractionTransaction */
    protected readonly calldataGenerator: CalldataGenerator;

    /** Hash commitment generator for CHCT */
    protected readonly hashCommitmentGenerator: HashCommitmentGenerator;

    /** The compiled operation data - SAME as InteractionTransaction's compiledTargetScript */
    protected readonly compiledTargetScript: Uint8Array;

    /** Generated hash-committed P2WSH outputs */
    protected readonly commitmentOutputs: IHashCommittedP2WSH[];

    /** Disable auto refund (same as InteractionTransaction) */
    protected readonly disableAutoRefund: boolean;

    /** Maximum chunk size (default: 80 bytes per P2WSH stack item limit) */
    protected readonly maxChunkSize: number;

    /** Cached value per output (calculated once, used by setup and reveal) */
    private cachedValuePerOutput: bigint | null = null;

    constructor(parameters: IConsolidatedInteractionParameters) {
        super(parameters);

        // Same validation as InteractionTransaction
        if (!parameters.to) {
            throw new Error('Contract address (to) is required');
        }

        if (!parameters.contract) {
            throw new Error('Contract secret (contract) is required');
        }

        if (!parameters.calldata) {
            throw new Error('Calldata is required');
        }

        if (!parameters.challenge) {
            throw new Error('Challenge solution is required');
        }

        this.contractAddress = parameters.to;
        this.contractSecret = fromHex(parameters.contract.startsWith('0x') ? parameters.contract.slice(2) : parameters.contract);
        this.disableAutoRefund = parameters.disableAutoRefund || false;
        this.maxChunkSize = parameters.maxChunkSize ?? HashCommitmentGenerator.MAX_CHUNK_SIZE;

        // Validate contract secret (same as InteractionTransaction)
        if (this.contractSecret.length !== 32) {
            throw new Error('Invalid contract secret length. Expected 32 bytes.');
        }

        // Compress calldata (same as SharedInteractionTransaction)
        this.calldata = Compressor.compress(parameters.calldata);

        // Generate random bytes and script signer (same as SharedInteractionTransaction)
        this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes();
        this.scriptSigner = EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network);

        // Generate epoch challenge address (same as SharedInteractionTransaction)
        this.challenge = parameters.challenge;
        this.epochChallenge = TimeLockGenerator.generateTimeLockAddress(
            this.challenge.publicKey.originalPublicKeyBuffer(),
            this.network,
        );

        // Create calldata generator (same as SharedInteractionTransaction)
        this.calldataGenerator = new CalldataGenerator(
            this.signer.publicKey,
            toXOnly(this.scriptSigner.publicKey),
            this.network,
        );

        // Compile the target script - SAME as InteractionTransaction
        if (parameters.compiledTargetScript) {
            if (parameters.compiledTargetScript instanceof Uint8Array) {
                this.compiledTargetScript = parameters.compiledTargetScript;
            } else if (typeof parameters.compiledTargetScript === 'string') {
                this.compiledTargetScript = fromHex(parameters.compiledTargetScript);
            } else {
                throw new Error('Invalid compiled target script format.');
            }
        } else {
            this.compiledTargetScript = this.calldataGenerator.compile(
                this.calldata,
                this.contractSecret,
                this.challenge,
                this.priorityFee,
                this.generateFeatures(parameters),
            );
        }

        // Create hash commitment generator
        this.hashCommitmentGenerator = new HashCommitmentGenerator(
            this.signer.publicKey,
            this.network,
        );

        // Split compiled data into hash-committed chunks
        this.commitmentOutputs = this.hashCommitmentGenerator.prepareChunks(
            this.compiledTargetScript,
            this.maxChunkSize,
        );

        // Validate output count
        this.validateOutputCount();

        const totalChunks = this.commitmentOutputs.reduce(
            (sum, output) => sum + output.dataChunks.length,
            0,
        );
        this.log(
            `ConsolidatedInteractionTransaction: ${this.commitmentOutputs.length} outputs, ` +
                `${totalChunks} chunks from ${this.compiledTargetScript.length} bytes compiled data`,
        );

        this.internalInit();
    }

    /**
     * Get the compiled target script (same as InteractionTransaction).
     */
    public exportCompiledTargetScript(): Uint8Array {
        return this.compiledTargetScript;
    }

    /**
     * Get the contract secret (same as InteractionTransaction).
     */
    public getContractSecret(): Uint8Array {
        return this.contractSecret;
    }

    /**
     * Get the random bytes (same as InteractionTransaction).
     */
    public getRndBytes(): Uint8Array {
        return this.randomBytes;
    }

    /**
     * Get the challenge solution (same as InteractionTransaction).
     */
    public getChallenge(): IChallengeSolution {
        return this.challenge;
    }

    /**
     * Get the commitment outputs for the setup transaction.
     */
    public getCommitmentOutputs(): IHashCommittedP2WSH[] {
        return this.commitmentOutputs;
    }

    /**
     * Get the number of P2WSH outputs.
     */
    public getOutputCount(): number {
        return this.commitmentOutputs.length;
    }

    /**
     * Get the total number of 80-byte chunks across all outputs.
     */
    public getTotalChunkCount(): number {
        return this.commitmentOutputs.reduce((sum, output) => sum + output.dataChunks.length, 0);
    }

    /**
     * Build both setup and reveal transactions.
     *
     * @returns Complete result with both transactions
     */
    public async build(): Promise<IConsolidatedInteractionResult> {
        // Build and sign setup transaction using base class flow
        const setupTx = await this.signTransaction();
        const setupTxId = setupTx.getId();

        const setup: ISetupTransactionResult = {
            txHex: setupTx.toHex(),
            txId: setupTxId,
            outputs: this.commitmentOutputs,
            feesPaid: this.transactionFee,
            chunkCount: this.getTotalChunkCount(),
            totalDataSize: this.compiledTargetScript.length,
        };

        this.log(`Setup transaction: ${setup.txId}`);

        // Build reveal transaction
        const reveal = this.buildRevealTransaction(setupTxId);

        return {
            setup,
            reveal,
            totalFees: setup.feesPaid + reveal.feesPaid,
        };
    }

    /**
     * Build the reveal transaction.
     * Spends the P2WSH commitment outputs, revealing the compiled data in witnesses.
     *
     * Output structure matches InteractionTransaction:
     * - Output to epochChallenge.address (miner reward)
     * - Change output (if any)
     *
     * @param setupTxId The transaction ID of the setup transaction
     */
    public buildRevealTransaction(setupTxId: string): IRevealTransactionResult {
        const revealPsbt = new Psbt({ network: this.network });

        // Get the value per output (same as used in setup transaction)
        const valuePerOutput = this.calculateValuePerOutput();

        // Add commitment outputs as inputs (from setup tx)
        for (let i = 0; i < this.commitmentOutputs.length; i++) {
            const commitment = this.commitmentOutputs[i] as IHashCommittedP2WSH;

            revealPsbt.addInput({
                hash: setupTxId,
                index: i,
                witnessUtxo: {
                    script: commitment.scriptPubKey as Script,
                    value: toSatoshi(valuePerOutput),
                },
                witnessScript: commitment.witnessScript,
            });
        }

        // Calculate input value from commitments
        const inputValue = BigInt(this.commitmentOutputs.length) * valuePerOutput;

        // Calculate OPNet fee (same as InteractionTransaction)
        const opnetFee = this.getTransactionOPNetFee();
        const feeAmount = opnetFee < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : opnetFee;

        // Add output to epoch challenge address (same as InteractionTransaction)
        revealPsbt.addOutput({
            address: this.epochChallenge.address,
            value: toSatoshi(feeAmount),
        });

        // Estimate reveal transaction fee
        const estimatedVBytes = this.estimateRevealVBytes();
        const revealFee = BigInt(Math.ceil(estimatedVBytes * this.feeRate));

        // Add change output if there's enough left
        const changeValue = inputValue - feeAmount - revealFee;
        if (changeValue > TransactionBuilder.MINIMUM_DUST) {
            const refundAddress = this.getRefundAddress();
            revealPsbt.addOutput({
                address: refundAddress,
                value: toSatoshi(changeValue),
            });
        }

        // Sign all commitment inputs
        for (let i = 0; i < this.commitmentOutputs.length; i++) {
            revealPsbt.signInput(i, this.signer);
        }

        // Finalize all inputs with hash-commitment finalizer
        for (let i = 0; i < this.commitmentOutputs.length; i++) {
            const commitment = this.commitmentOutputs[i] as IHashCommittedP2WSH;
            revealPsbt.finalizeInput(i, (_inputIndex: number, input: PsbtInput) => {
                return this.finalizeCommitmentInput(input, commitment);
            });
        }

        const revealTx: Transaction = revealPsbt.extractTransaction();

        const result: IRevealTransactionResult = {
            txHex: revealTx.toHex(),
            txId: revealTx.getId(),
            dataSize: this.compiledTargetScript.length,
            feesPaid: revealFee,
            inputCount: this.commitmentOutputs.length,
        };

        this.log(`Reveal transaction: ${result.txId}`);

        return result;
    }

    /**
     * Get the value per commitment output (for external access).
     */
    public getValuePerOutput(): bigint {
        return this.calculateValuePerOutput();
    }

    /**
     * Build the setup transaction.
     * Creates P2WSH outputs with hash commitments to the compiled data chunks.
     * This is called by signTransaction() in the base class.
     */
    protected override async buildTransaction(): Promise<void> {
        // Add funding UTXOs as inputs
        this.addInputsFromUTXO();

        // Calculate value per output (includes reveal fee + OPNet fee)
        const valuePerOutput = this.calculateValuePerOutput();

        // Add each hash-committed P2WSH as an output
        for (const commitment of this.commitmentOutputs) {
            this.addOutput({
                value: toSatoshi(valuePerOutput),
                address: commitment.address,
            });
        }

        // Calculate total spent on commitment outputs
        const totalCommitmentValue = BigInt(this.commitmentOutputs.length) * valuePerOutput;

        // Add optional outputs
        const optionalAmount = this.addOptionalOutputsAndGetAmount();

        // Add refund/change output
        await this.addRefundOutput(totalCommitmentValue + optionalAmount);
    }

    /**
     * Finalize a commitment input.
     *
     * Witness stack: [signature, data_1, data_2, ..., data_N, witnessScript]
     *
     * The witness script verifies each data chunk against its committed hash.
     * If any data is wrong or missing, the transaction is INVALID at consensus level.
     */
    private finalizeCommitmentInput(
        input: PsbtInput,
        commitment: IHashCommittedP2WSH,
    ): {
        finalScriptSig: Uint8Array | undefined;
        finalScriptWitness: Uint8Array | undefined;
    } {
        if (!input.partialSig || input.partialSig.length === 0) {
            throw new Error('No signature for commitment input');
        }

        if (!input.witnessScript) {
            throw new Error('No witness script for commitment input');
        }

        // Witness stack for hash-committed P2WSH with multiple chunks
        // Order: [signature, data_1, data_2, ..., data_N, witnessScript]
        const witnessStack: Uint8Array[] = [
            (input.partialSig[0] as { signature: Uint8Array }).signature, // Signature for OP_CHECKSIG
            ...commitment.dataChunks, // All data chunks for OP_HASH160 verification
            input.witnessScript, // The witness script
        ];

        return {
            finalScriptSig: undefined,
            finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack),
        };
    }

    /**
     * Estimate reveal transaction vBytes.
     */
    private estimateRevealVBytes(): number {
        // Calculate actual witness weight based on chunks per output
        let witnessWeight = 0;
        for (const commitment of this.commitmentOutputs) {
            // Per input: 41 bytes base (× 4) + witness data
            // Witness: signature (~72) + chunks (N × 80) + script (N × 23 + 35) + overhead (~20)
            const numChunks = commitment.dataChunks.length;
            const chunkDataWeight = numChunks * 80; // actual data
            const scriptWeight = numChunks * 23 + 35; // witness script
            const sigWeight = 72;
            const overheadWeight = 20;

            witnessWeight += 164 + chunkDataWeight + scriptWeight + sigWeight + overheadWeight;
        }

        const weight = 40 + witnessWeight + 200; // tx overhead + witnesses + outputs
        return Math.ceil(weight / 4);
    }

    /**
     * Calculate the required value per commitment output.
     * This must cover: dust minimum + share of reveal fee + share of OPNet fee
     */
    private calculateValuePerOutput(): bigint {
        // Return cached value if already calculated
        if (this.cachedValuePerOutput !== null) {
            return this.cachedValuePerOutput;
        }

        const numOutputs = this.commitmentOutputs.length;

        // Calculate OPNet fee
        const opnetFee = this.getTransactionOPNetFee();
        const feeAmount = opnetFee < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : opnetFee;

        // Calculate reveal fee
        const estimatedVBytes = this.estimateRevealVBytes();
        const revealFee = BigInt(Math.ceil(estimatedVBytes * this.feeRate));

        // Total needed: OPNet fee + reveal fee + dust for change
        const totalNeeded = feeAmount + revealFee + TransactionBuilder.MINIMUM_DUST;

        // Distribute across outputs, ensuring at least MIN_OUTPUT_VALUE per output
        const valuePerOutput = BigInt(Math.ceil(Number(totalNeeded) / numOutputs));
        const minValue = HashCommitmentGenerator.MIN_OUTPUT_VALUE;

        this.cachedValuePerOutput = valuePerOutput > minValue ? valuePerOutput : minValue;
        return this.cachedValuePerOutput;
    }

    /**
     * Get refund address.
     */
    private getRefundAddress(): string {
        if (this.from) {
            return this.from;
        }

        return AddressGenerator.generatePKSH(this.signer.publicKey, this.network);
    }

    /**
     * Generate features (same as InteractionTransaction).
     */
    private generateFeatures(parameters: IConsolidatedInteractionParameters): Feature<Features>[] {
        const features: Feature<Features>[] = [];

        if (parameters.loadedStorage) {
            features.push({
                priority: FeaturePriority.ACCESS_LIST,
                opcode: Features.ACCESS_LIST,
                data: parameters.loadedStorage,
            });
        }

        const submission = parameters.challenge.getSubmission();
        if (submission) {
            features.push({
                priority: FeaturePriority.EPOCH_SUBMISSION,
                opcode: Features.EPOCH_SUBMISSION,
                data: submission,
            });
        }

        if (parameters.revealMLDSAPublicKey && !parameters.linkMLDSAPublicKeyToAddress) {
            throw new Error(
                'To reveal the MLDSA public key, you must set linkMLDSAPublicKeyToAddress to true.',
            );
        }

        if (parameters.linkMLDSAPublicKeyToAddress) {
            this.generateMLDSALinkRequest(parameters, features);
        }

        return features;
    }

    /**
     * Validate output count is within standard tx limits.
     */
    private validateOutputCount(): void {
        const maxInputs = HashCommitmentGenerator.calculateMaxInputsPerTx();

        if (this.commitmentOutputs.length > maxInputs) {
            const maxData = HashCommitmentGenerator.calculateMaxDataPerTx();
            throw new Error(
                `Data too large: ${this.commitmentOutputs.length} P2WSH outputs needed, ` +
                    `max ${maxInputs} per standard transaction (~${Math.floor(maxData / 1024)}KB). ` +
                    `Compiled data: ${this.compiledTargetScript.length} bytes.`,
            );
        }
    }
}
