import {
    Address,
    AddressType,
    GroupToken,
    Networkish,
    Networks, Output, Script,
    ScriptFactory,
    Transaction,
    TransactionBuilder
} from "libnexa-ts";
import {PermissionLabel, TokenAction, TxOptions} from "../../../models/transaction.entities";
import {isValidNexaAddress} from "../../../utils/WalletUtils";
import {parseInt} from "lodash-es";
import {MAX_INT64} from "../../../utils/CommonUtils";
import {rostrumProvider} from "../../../network/RostrumProvider";

/**
 * Abstract base class for creating and managing transactions in the NEXA blockchain.
 * Provides common functionality for transaction building including token operations,
 * address validation, and output creation.
 */
export abstract class TransactionCreator {

    /** The underlying transaction builder instance */
    private _transactionBuilder!: TransactionBuilder;
    /** Set of token actions to be performed in this transaction */
    private _tokens!: Set<TokenAction>;
    /** Array of async functions to execute when building the transaction */
    private _builder: (() => Promise<any>)[] = [];
    /** Total value of NEXA being sent in this transaction */
    private _totalValue: bigint = BigInt(0);
    /** Network this transaction will be broadcast on */
    private _network: Networkish = Networks.mainnet
    /** Transaction options for customizing behavior */
    private _txOptions: TxOptions = {}

    /**
     * Creates a new TransactionCreator instance
     * @param tx Optional existing TransactionBuilder, hex string, or buffer
     */
    protected constructor(tx?: TransactionBuilder | string | Buffer) {
        if (tx instanceof TransactionBuilder) {
            this.transactionBuilder = tx;
        }
        this.tokens = new Set<TokenAction>()
        this.transactionBuilder = new TransactionBuilder();
    }

    /** Parse transaction from hex string - must be implemented by subclasses */
    public abstract parseTxHex(tx:string): this
    /** Parse transaction from buffer - must be implemented by subclasses */
    public abstract parseTxBuffer(tx: Buffer): this

    /**
     * Sets the network for this transaction
     * @param network Network name or Networkish object
     * @returns This instance for chaining
     */
    public onNetwork(network: string | Networkish) {
        this.network = Networks.get(network)!
        return this
    }

    /** Gets transaction options */
    get txOptions(): TxOptions {
        return this._txOptions;
    }

    /** Sets transaction options */
    set txOptions(value: TxOptions) {
        this._txOptions = value;
    }

    /** Gets the network for this transaction */
    get network(): Networkish {
        return this._network;
    }

    /** Sets the network for this transaction */
    set network(value: Networkish) {
        this._network = value;
    }

    /** Gets the builder function array */
    get builder(): (() => Promise<any>)[] {
        return this._builder;
    }

    /** Sets the builder function array */
    set builder(value: (() => Promise<any>)[]) {
        this._builder = value;
    }

    /** Gets the underlying transaction builder */
    get transactionBuilder(): TransactionBuilder {
        return this._transactionBuilder;
    }

    /** Sets the underlying transaction builder */
    set transactionBuilder(value: TransactionBuilder) {
        this._transactionBuilder = value;
    }

    /** Gets the set of token actions */
    get tokens(): Set<TokenAction> {
        return this._tokens;
    }

    /** Sets the set of token actions */
    set tokens(value: Set<TokenAction>) {
        this._tokens = value;
    }

    /** Gets the total NEXA value being sent */
    get totalValue(): bigint {
        return this._totalValue;
    }

    /** Sets the total NEXA value being sent */
    set totalValue(value: bigint) {
        this._totalValue = value;
    }

    /**
     * Validates and creates a token action
     * @param toAddr Destination address
     * @param amount Amount to send
     * @param token Token ID
     * @param action Action type (mint, melt, send, etc.)
     * @throws Error if validation fails
     */
    protected tokenAction(toAddr: string, amount: string, token: string, action: string){
        // Validate destination address
        if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
            throw new Error('Invalid Address.');
        }

        // Validate amount ranges
        if ((token && BigInt(amount) < 1n) || (!token && parseInt(amount) < Transaction.DUST_AMOUNT)) {
            throw new Error("The amount is too low.");
        }
        if ((token && BigInt(amount) > MAX_INT64) || (!token && parseInt(amount) > Transaction.MAX_MONEY)) {
            throw new Error("The amount is too high.");
        }

        // Validate token ID
        if (!isValidNexaAddress(token, this.network, AddressType.GroupIdAddress)) {
            throw new Error('Invalid Token ID');
        }

        // Ensure tokens are sent to script template addresses
        if (Address.getOutputType(toAddr) === 0) {
            throw new Error('Token must be sent to script template address');
        }

        // Add output to transaction
        this.transactionBuilder.to(toAddr, Transaction.DUST_AMOUNT, token, BigInt(amount))

        // Record the token action
        this.tokens.add({
            token: token,
            amount: BigInt(amount),
            action: action
        })
    }

    /**
     * Configures transaction to consolidate UTXOs to a single address
     * @param toAddr Address to consolidate funds to
     * @returns This instance for chaining
     */
    public consolidate(toAddr: string): this {
        this.builder.push(async () => {
            if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
                throw new Error('Invalid Address.');
            }
            this._txOptions.isConsolidate = true
            this._txOptions.toChange = toAddr
        })
        return this
    }

    /**
     * Configures transaction to deduct fee from the send amount
     * @returns This instance for chaining
     */
    public feeFromAmount(): this{
        this.builder.push(async () => {
            this._txOptions.feeFromAmount = true
        })
        return this
    }

    /**
     * Adds a token send operation to the transaction
     * @param toAddr Destination address
     * @param amount Amount of tokens to send
     * @param token Token ID
     * @returns This instance for chaining
     */
    public sendToToken(toAddr: string, amount: string, token: string): this {
        this.builder.push(async () => {
            this.tokenAction(toAddr, amount, token, 'send')
        })

        return this;
    }

    /**
     * Adds a NEXA send operation to the transaction
     * @param toAddr Destination address
     * @param amount Amount of NEXA to send
     * @returns This instance for chaining
     */
    public sendTo(toAddr: string, amount: string){
        this.builder.push(async () => {
            if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
                throw new Error('Invalid Address.');
            }
            this.transactionBuilder.to(toAddr, amount);
            this.totalValue = BigInt(this.totalValue + amount)
        })
        return this;
    }

    /**
     * Adds a token authority renewal operation
     * @param token Token ID to renew authority for
     * @param perms Permissions to renew
     * @param toAddr
     * @returns This instance for chaining
     */
    public renewAuthority(token: string, perms: PermissionLabel[], toAddr?: string): this {
        this.builder.push(async() => {
            if(toAddr != null) {
                if (!isValidNexaAddress(toAddr, this.network) && !isValidNexaAddress(toAddr, this.network, AddressType.PayToPublicKeyHash)) {
                    throw new Error('Invalid Address.');
                }
            }

            this.tokens.add({
                token: token,
                action: 'renew',
                amount: BigInt(Transaction.DUST_AMOUNT),
                parentToken: undefined,
                extraData: {
                    perms: perms,
                    address: toAddr
                }
            })
        })
        return this
    }

    /**
     * Adds a token authority deletion operation
     * @param token Token ID to delete authority for
     * @param outpoint Outpoint of the authority to delete
     * @returns This instance for chaining
     */
    public deleteAuthority(token:string, outpoint: string): this {
        this.builder.push(async () => {
            this.tokens.add({
                token: token,
                action: 'delete',
                amount: BigInt(Transaction.DUST_AMOUNT),
                parentToken: undefined,
                extraData: {
                    outpoint: outpoint
                }
            })
        })
        return this
    }

    /**
     * Creates a legacy token (not implemented)
     * @returns This instance for chaining
     */
    public legacyToken(name: string, ticker: string, decimals: number, docUrl: string, docHash: string): this {
        this.builder.push(async () => {
            const opReturn = ScriptFactory.buildTokenDescriptionLegacy(
                ticker,
                name,
                docUrl,
                docHash,
                decimals
            )
            this.transactionBuilder.addData(opReturn, true)
            this.tokens.add({
                action: 'group',
                amount: BigInt(Transaction.DUST_AMOUNT),
                extraData: {
                    opReturnData: opReturn.toHex()
                }
            })
        })
        return this
    }

    /**
     * Creates a legacy group (not implemented)
     * @returns This instance for chaining
     */
    public legacyGroup(name: string, ticker: string, docUrl: string, docHash: string): this {
        this.builder.push(async () => {
            const opReturn = ScriptFactory.buildTokenDescriptionLegacy(
                ticker,
                name,
                docUrl,
                docHash,
            )
            this.transactionBuilder.addData(opReturn, true)
            this.tokens.add({
                action: 'group',
                amount: BigInt(Transaction.DUST_AMOUNT),
                extraData: {
                    opReturnData: opReturn.toHex()
                }
            })
        })
        return this
    }

    /**
     * Creates a token with metadata
     * @param name Token name
     * @param ticker Token ticker symbol
     * @param decimals Number of decimal places
     * @param docUrl URL to token documentation
     * @param docHash Hash of token documentation
     * @returns This instance for chaining
     */
    public token(name: string, ticker: string, decimals: number, docUrl: string, docHash: string): this {
        this.builder.push(async () => {
            const opReturn = ScriptFactory.buildTokenDescription(
                ticker,
                name,
                docUrl,
                docHash,
                decimals
            )
            this.transactionBuilder.addData(opReturn, true)
            this.tokens.add({
                action: 'group',
                amount: BigInt(Transaction.DUST_AMOUNT),
                extraData: {
                    opReturnData: opReturn.toHex()
                }
            })
        })
        return this
    }

    /**
     * Creates an NFT collection with metadata
     * @param name Collection name
     * @param ticker Collection ticker symbol
     * @param docUrl URL to collection documentation
     * @param docHash Hash of collection documentation
     * @returns This instance for chaining
     */
    public collection(name: string, ticker: string, docUrl: string, docHash: string): this {
        this.builder.push(async () => {
            const opReturn = ScriptFactory.buildNFTCollectionDescription(
                ticker,
                name,
                docUrl,
                docHash
            )
            this.transactionBuilder.addData(opReturn, true)
            this.tokens.add({
                action: 'group',
                amount: BigInt(Transaction.DUST_AMOUNT),
                extraData: {
                    opReturnData: opReturn.toHex()
                }
            })
        })

        return this
    }

    /**
     * Creates an NFT within a collection
     * @param parent Parent collection token ID
     * @param zipUrl URL to NFT content ZIP file
     * @param zipHash Hash of NFT content ZIP file
     * @returns This instance for chaining
     */
    public nft(parent: string, zipUrl: string, zipHash: string) {
        this.builder.push(async () => {
            // add op_return for the nft
            let opReturn = ScriptFactory.buildNFTDescription(zipUrl, zipHash);
            this.transactionBuilder.addData(opReturn, true);
            // generate subgroup ID
            const subGroup = GroupToken.generateSubgroupId(parent, opReturn.toBuffer()).toString('hex')
            this.tokens.add({
                token: subGroup,
                parentToken: parent,
                amount: BigInt(Transaction.DUST_AMOUNT),
                action: 'subgroup'
            })
        })
        return this
    }

    /**
     * Adds an OP_RETURN output to the transaction
     * @param data Data to include in the OP_RETURN
     * @param isFullScript Whether the data is already a complete script
     * @returns This instance for chaining
     */
    public addOpReturn(data: Buffer | string | Script, isFullScript = false) {
        this.builder.push(async () => {
            let script = isFullScript ? new Script(data) : ScriptFactory.buildDataOut(data);
            let output = new Output(0, script);
            this.transactionBuilder.transaction.addOutput(output);
        })

        return this;
    }

    /** Populates the transaction with inputs and outputs - must be implemented by subclasses */
    abstract populate(): this

    /**
     * Builds the transaction by executing all queued operations
     * @returns Promise resolving to the serialized transaction hex
     */
    public async build(): Promise<string>{
        for (const task of this.builder) {
            await task();
        }
        return this.transactionBuilder.transaction.serialize(({disableAll: true}));
    }

}
