import {
    AddressType,
    TransactionBuilder,
} from "libnexa-ts";
import {WatchOnlyAddress} from "../../models/wallet.entities";
import {TransactionCreator} from "./interfaces/TransactionCreator";
import {
    watchOnlyBuildCreateGroupTransaction, watchOnlyPopulateAndDuplicateTokenAuths,
    watchOnlyPopulateNexaInputsAndChange,
    watchOnlyPopulateTokenAuth, watchOnlyPopulateTokenInputsAndChange, watchOnlyPrepareDeleteTransaction
} from "../../utils/WatchOnlyTXUtils";
import {isString} from "lodash-es";
import {rostrumProvider} from "../../network/RostrumProvider";
import {PermissionLabel} from "../../models/transaction.entities";
import {isValidNexaAddress} from "../../utils/WalletUtils";

/**
 * WatchOnlyTransactionCreator extends TransactionCreator to handle transaction creation
 * for watch-only wallets. It manages addresses without private keys and delegates
 * UTXO selection and input population to specialized utility functions.
 */
export default class WatchOnlyTransactionCreator extends TransactionCreator {


    /** Addresses that need to be signed with (populated during transaction building) */
    private _addressesToSignWith: string[] = [];
    /** Available addresses for input selection and change */
    private _availableAddresses: WatchOnlyAddress[] = []

    /**
     * Creates a new WatchOnlyTransactionCreator
     * @param tx Optional existing transaction builder or transaction data
     */
    constructor(tx?: TransactionBuilder | string | Buffer) {
        super(tx)
    }

    /**
     * Sets the source addresses for transaction inputs
     * @param address Single address string, array of addresses, or WatchOnlyAddress objects
     * @returns This instance for chaining
     */
    public from(address: string | string[] | WatchOnlyAddress[] | WatchOnlyAddress): this {
        if (isString(address)) {

            if (!isValidNexaAddress(address, this.network) && !isValidNexaAddress(address, this.network, AddressType.PayToPublicKeyHash)) {
                throw new Error('Invalid Address.');
            }
            // Single address string
            this._availableAddresses.push({address: address});
        } else if(Array.isArray(address)) {
            // Array of addresses or WatchOnlyAddress objects
            address.forEach((addr) => {
                if (isString(addr)) {
                    if (!isValidNexaAddress(addr, this.network) && !isValidNexaAddress(addr, this.network, AddressType.PayToPublicKeyHash)) {
                        throw new Error('Invalid Address.');
                    }
                    // String address
                    this._availableAddresses.push({address: addr});
                } else if (addr && typeof addr === 'object' && 'address' in addr) {
                    // WatchOnlyAddress object
                    this._availableAddresses.push(<WatchOnlyAddress>addr);
                }
            })
        } else if(address.address != null) {
            // Single WatchOnlyAddress object
            this._availableAddresses.push(<WatchOnlyAddress>address)
        }
        return this
    }

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

        return this
    }

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

        return this
    }

    /**
     * Populates the transaction with inputs and outputs based on the configured actions.
     * Handles different token operations (mint, melt, group creation, etc.) and
     * populates NEXA inputs for transaction fees.
     * @returns This instance for chaining
     */
    public populate(): this {
        this.builder.push(async () => {
            let tokenAddresses: string[] = []
            let nexaAddresses: string[] = []

            // Process token operations if any are configured
            if(this.tokens.size > 0) {
                for (const tokenAction of this.tokens) {
                    if(tokenAction.action == 'mint' || tokenAction.action == 'melt') {
                        // Handle token minting/melting - requires authority
                        tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateTokenAuth(this.transactionBuilder, this._availableAddresses, tokenAction.token!, tokenAction.action))
                    } else if (tokenAction.action == 'group') {
                        // Handle group token creation
                        tokenAddresses = tokenAddresses.concat(await watchOnlyBuildCreateGroupTransaction(this.transactionBuilder, this._availableAddresses, tokenAction.token!, this.network))
                    } else if (tokenAction.action == 'subgroup') {
                        // Handle subgroup token creation
                        tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateTokenAuth(this.transactionBuilder, this._availableAddresses, tokenAction.parentToken!, tokenAction.action, tokenAction.token));
                    } else if(tokenAction.action == 'renew') {
                        // Handle authority renewal
                        tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateAndDuplicateTokenAuths(this.transactionBuilder, this._availableAddresses, tokenAction.token!, tokenAction.extraData!.perms!, tokenAction.extraData?.address!))
                    } else if(tokenAction.action == 'delete') {
                        // Handle authority deletion
                        tokenAddresses = tokenAddresses.concat(await watchOnlyPrepareDeleteTransaction(this.transactionBuilder, this._availableAddresses, tokenAction.extraData!.outpoint!))
                    } else {
                        // Handle regular token transfers
                        tokenAddresses = tokenAddresses.concat(await watchOnlyPopulateTokenInputsAndChange(this.transactionBuilder, this._availableAddresses, tokenAction.token!, tokenAction.amount))
                    }

                    // Accumulate addresses that need signing
                    this._addressesToSignWith!.concat(tokenAddresses)
                }
            }

            // Populate NEXA inputs for transaction fees and change
            nexaAddresses = nexaAddresses.concat(await watchOnlyPopulateNexaInputsAndChange(this.transactionBuilder, this._availableAddresses, this.totalValue, this.txOptions))

            // Combine all addresses that need signing
            this._addressesToSignWith! = tokenAddresses.concat(nexaAddresses)
        })

        return this
    }

    /**
     * Parse transaction from buffer (not implemented for watch-only)
     * @param tx Transaction buffer
     * @returns This instance for chaining
     * @throws Error indicating method not implemented
     */
    public parseTxBuffer(tx: Buffer): this {
        this.builder.push(async () => {
            this.transactionBuilder = new TransactionBuilder(tx);
        })
        return this
    }

    /**
     * Parse transaction from hex string (not implemented for watch-only)
     * @param tx Transaction hex string
     * @returns This instance for chaining
     * @throws Error indicating method not implemented
     */
    public parseTxHex(tx: string) {
        this.builder.push(async () => {
            const txBuilder = new TransactionBuilder(tx)
            const newTxBuilder = new TransactionBuilder()
            const oldInputs = txBuilder.transaction.inputs
            for (const input of oldInputs) {
                const utxo = await rostrumProvider.getUtxo(input.outpoint.toString('hex'))
                newTxBuilder.from({
                    outpoint: utxo.tx_hash,
                    amount: utxo.amount,
                    scriptPubKey: utxo.scriptpubkey
                })
            }
            newTxBuilder.transaction.outputs = txBuilder.transaction.outputs
            this.transactionBuilder = newTxBuilder;
        })

        return this
    }
}
