import { rostrumProvider } from '../network/RostrumProvider';
import { WatchOnlyAddress } from '../models/wallet.entities';
import { MAX_INT64, isNullOrEmpty } from './CommonUtils';
import {
    Address,
    AddressType,
    GroupToken,
    Networkish,
    Transaction,
    TransactionBuilder,
    UnitUtils,
    UTXO
} from "libnexa-ts";
import { PermissionLabel, TxOptions } from "../models/transaction.entities";
import {dupAuthority, isAuthFit} from "./TokenUtils";

const MAX_INPUTS_OUTPUTS = 250;

export async function watchOnlyPopulateNexaInputsAndChange(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], totalTxValue: bigint, options: TxOptions): Promise<string[]> {
    if (isNullOrEmpty(addresses)) {
        throw new Error("Not enough Nexa balance.");
    }

    let usedAddresses = new Set<string>();
    let origAmount = options.isConsolidate ? 0 : Number(totalTxValue);

    for (let item of addresses) {
        let utxos = await rostrumProvider.getNexaUtxos(item.address);
        for (let utxo of utxos) {
            let input: UTXO = {
                outpoint: utxo.outpoint_hash,
                address: item.address,
                satoshis: utxo.value,
                templateData: options.templateData
            }
            txBuilder.from(input);

            if (!usedAddresses.has(item.address)) {
                usedAddresses.add(item.address);
            }

            if (options.isConsolidate) {
                // need to handle change
                txBuilder.change(options.toChange ?? item.address);

                if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) {
                    return Array.from(usedAddresses.values());
                }
            } else {
                let tx = txBuilder.transaction;
                if (tx.inputs.length > MAX_INPUTS_OUTPUTS) {
                    throw new Error("Too many inputs. Consider consolidate transactions or reduce the send amount.");
                }

                let unspent = tx.getUnspentValue();
                if (unspent < 0n) {
                    continue;
                }

                if (unspent == 0n && options.feeFromAmount) {
                    let txFee = tx.estimateRequiredFee();
                    tx.updateOutputAmount(0, origAmount - txFee);
                    return Array.from(usedAddresses.values());
                }

                txBuilder.change(options.toChange ?? item.address);
                if (options.feeFromAmount) {
                    let hasChange = tx.getChangeOutput();
                    let txFee = tx.estimateRequiredFee();
                    tx.updateOutputAmount(0, origAmount - txFee);

                    // edge case where change added after update
                    if (!hasChange && tx.getChangeOutput()) {
                        txFee = tx.estimateRequiredFee();
                        tx.updateOutputAmount(0, origAmount - txFee);
                    }
                }

                // check again after change output manipulation
                if (tx.getUnspentValue() < tx.estimateRequiredFee()) {
                    // try to add more utxos to satisfy the minimum fee
                    continue;
                }
                return Array.from(usedAddresses.values());
            }
        }
    }

    if (options.isConsolidate) {
        if (usedAddresses.size > 0) {
            return Array.from(usedAddresses.values());
        }
        throw new Error("Not enough Nexa balance.");
    }

    let err = {
        errorMsg: "Not enough Nexa balance.",
        amount: UnitUtils.formatNEXA(Number(totalTxValue)),
        fee: UnitUtils.formatNEXA(txBuilder.transaction.estimateRequiredFee())
    }

    throw new Error(JSON.stringify(err));
}

export async function watchOnlyPopulateTokenInputsAndChange(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], token: string, outTokenAmount: bigint): Promise<string[]> {
    if (isNullOrEmpty(addresses)) {
        throw new Error("Not enough token balance.");
    }

    let usedKeys = new Set<string>();
    let inTokenAmount = 0n;

    for (let item of addresses) {
        let utxos = await rostrumProvider.getTokenUtxos(item.address, token);
        for (let utxo of utxos) {
            if (utxo.token_amount < 0) {
                continue;
            }
            txBuilder.from({
                outpoint: utxo.outpoint_hash,
                address: item.address,
                satoshis: utxo.value,
                groupId: utxo.group,
                groupAmount: BigInt(utxo.token_amount),
            });

            inTokenAmount = inTokenAmount + BigInt(utxo.token_amount);
            if (!usedKeys.has(item.address)) {
                usedKeys.add(item.address);
            }

            if (inTokenAmount > MAX_INT64) {
                throw new Error("Token inputs exceeded max amount. Consider sending in small chunks");
            }
            if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) {
                throw new Error("Too many inputs. Consider consolidating transactions or reduce the send amount.");
            }

            if (inTokenAmount == outTokenAmount) {
                return Array.from(usedKeys.values());
            }
            if (inTokenAmount > outTokenAmount) {
                // change
                txBuilder.to(item.address, Transaction.DUST_AMOUNT, token, inTokenAmount - outTokenAmount);
                return Array.from(usedKeys.values());
            }
        }
    }

    throw new Error("Not enough token balance");
}

export async function watchOnlyBuildCreateGroupTransaction(
    txBuilder: TransactionBuilder,
    addresses: WatchOnlyAddress[],
    opReturnData: string,
    network: Networkish
): Promise<string[]> {
    let outpoint = '', idHex = '';
    let usedKeys: string[] = [];
    for (let item of addresses) {
        let utxos = await rostrumProvider.getNexaUtxos(item.address);
        for (let utxo of utxos) {
            txBuilder.from({
                outpoint: utxo.outpoint_hash,
                address: item.address,
                satoshis: utxo.value
            });

            if (isNullOrEmpty(outpoint)) {
                outpoint = utxo.outpoint_hash;
                let id = GroupToken.findGroupId(Buffer.from(outpoint, 'hex'), Buffer.from(opReturnData, 'hex'), GroupToken.authFlags.ACTIVE_FLAG_BITS);
                const groupId = new Address(id.hashBuffer, network, AddressType.GroupIdAddress).toString()
                txBuilder.to(item.address, Transaction.DUST_AMOUNT, groupId, GroupToken.authFlags.ACTIVE_FLAG_BITS | id.nonce)
                idHex = id.hashBuffer.toString('hex');
                usedKeys.push(item.address);
                return usedKeys
            }
        }
    }

    throw new Error("Not enough Nexa balance.");
}

export async function watchOnlyPrepareDeleteTransaction(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], outpoint: string): Promise<string[]> {
    let utxo = await rostrumProvider.getUtxo(outpoint);
    let address = utxo.addresses[0];

    txBuilder.from({
        outpoint: outpoint,
        address: address,
        satoshis: utxo.amount
    });

    let addrKey = addresses.find(k => k.address === address);

    if (!addrKey) {
        throw new Error('UTXO associated key not found in the wallet');
    }
    return [addrKey.address];
}

export async function watchOnlyPopulateTokenAuth(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], token: string, perm: PermissionLabel, subgroup = ''): Promise<string[]> {
    for (let item of addresses) {
        let utxos = await rostrumProvider.getTokenUtxos(item.address, token);
        for (let utxo of utxos) {
            if (!isAuthFit(utxo.token_amount, perm)) {
                continue;
            }

            txBuilder.from({
                outpoint: utxo.outpoint_hash,
                address: item.address,
                satoshis: utxo.value
            });

            if (perm === 'subgroup') {
                txBuilder.to(item.address, Transaction.DUST_AMOUNT, subgroup, dupAuthority(utxo.token_amount, false));
            }

            // if renew flag included, we don't want to burn it
            if (GroupToken.allowsRenew(BigInt.asUintN(64, BigInt(utxo.token_amount)))) {
                txBuilder.to(item.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount));
            }

            return [item.address];
        }
    }

    throw new Error("The requested authority not found");
}

export async function watchOnlyPopulateAndDuplicateTokenAuths(txBuilder: TransactionBuilder, addresses: WatchOnlyAddress[], token: string, perms: PermissionLabel[], toAddr?: string): Promise<string[]> {
    let usedAddresses: string[] = [];

    let reqiredPerms = new Set(perms);
    reqiredPerms.add('authorise');

    for (let item of addresses) {
        let utxos = await rostrumProvider.getTokenUtxos(item.address, token);
        for (let utxo of utxos) {
            if (utxo.token_amount > 0) {
                continue;
            }

            let found = false;
            for (let perm of reqiredPerms) {
                if (isAuthFit(utxo.token_amount, perm)) {
                    reqiredPerms.delete(perm);
                    found = true;
                }
            }

            if (!found) {
                continue;
            }

            txBuilder.from({
                outpoint: utxo.outpoint_hash,
                address: item.address,
                satoshis: utxo.value
            });
            usedAddresses.push(item.address);

            // duplicate
            txBuilder.to(toAddr != null ? toAddr : item.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount));

            if (reqiredPerms.size === 0) {
                return usedAddresses;
            }
        }
    }

    throw new Error("The required authorities not found");
}
