import {IFromBTCWrapper} from "../IFromBTCWrapper";
import {FromBTCSwap, FromBTCSwapInit, FromBTCSwapState} from "./FromBTCSwap";
import {
    ChainSwapType,
    ChainType,
    ClaimEvent,
    InitializeEvent,
    RefundEvent,
    RelaySynchronizer,
    SwapData,
    BtcRelay, BitcoinRpcWithAddressIndex, SwapCommitState
} from "@atomiqlabs/base";
import {EventEmitter} from "events";
import {Intermediary} from "../../../../intermediaries/Intermediary";
import {ISwapPrice} from "../../../../prices/abstract/ISwapPrice";
import {ISwapWrapperOptions, WrapperCtorTokens} from "../../../ISwapWrapper";
import {Buffer} from "buffer";
import {IntermediaryError} from "../../../../errors/IntermediaryError";
import {SwapType} from "../../../../enums/SwapType";
import {
    extendAbortController, mapArrayToObject,
    randomBytes,
    throwIfUndefined
} from "../../../../utils/Utils";
import { toOutputScript} from "../../../../utils/BitcoinUtils";
import {FromBTCResponseType, IntermediaryAPI} from "../../../../intermediaries/apis/IntermediaryAPI";
import {RequestError} from "../../../../errors/RequestError";
import {BTC_NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils";
import {UnifiedSwapEventListener} from "../../../../events/UnifiedSwapEventListener";
import {UnifiedSwapStorage} from "../../../../storage/UnifiedSwapStorage";
import {ISwap} from "../../../ISwap";
import {IClaimableSwapWrapper} from "../../../IClaimableSwapWrapper";
import {IFromBTCSelfInitDefinition} from "../IFromBTCSelfInitSwap";
import {AmountData} from "../../../../types/AmountData";
import {tryWithRetries} from "../../../../utils/RetryUtils";
import {AllOptional} from "../../../../utils/TypeUtils";
import {UserError} from "../../../../errors/UserError";

export type FromBTCOptions = {
    /**
     * A flag to attach 0 watchtower fee to the swap, this would make the settlement unattractive for the watchtowers
     *  and therefore automatic settlement for such swaps will not be possible, you will have to settle manually
     *  with {@link FromBTCLNSwap.claim} or {@link FromBTCLNSwap.txsClaim} functions.
     */
    unsafeZeroWatchtowerFee?: boolean,
    /**
     * A safety factor to use when estimating the watchtower fee to attach to the swap (this has to cover the gas fee
     *  of watchtowers settling the swap). A higher multiple here would mean that a swap is more attractive for
     *  watchtowers to settle automatically.
     *
     * Uses a `1.5` multiple by default (i.e. the current network fee is multiplied by 1.5 and then used to estimate
     *  the settlement gas fee cost).
     *
     * Also accepts `bigint` for legacy reasons.
     */
    feeSafetyFactor?: number | bigint,

    /**
     * @deprecated Removed as it is deemed not necessary
     */
    blockSafetyFactor?: number
};

export type FromBTCWrapperOptions = ISwapWrapperOptions & {
    safetyFactor: number,
    blocksTillTxConfirms: number,
    maxConfirmations: number,
    minSendWindow: number,
    bitcoinNetwork: BTC_NETWORK,
    bitcoinBlocktime: number
};

export type FromBTCDefinition<T extends ChainType> = IFromBTCSelfInitDefinition<T, FromBTCWrapper<T>, FromBTCSwap<T>>;

/**
 * Legacy escrow (PrTLC) based swap for Bitcoin -> Smart chains, requires manual initiation
 *  of the swap escrow on the destination chain.
 *
 * @category Swaps/Legacy/Bitcoin → Smart chain
 */
export class FromBTCWrapper<
    T extends ChainType
> extends IFromBTCWrapper<T, FromBTCDefinition<T>, FromBTCWrapperOptions> implements IClaimableSwapWrapper<FromBTCSwap<T>> {
    public readonly TYPE: SwapType.FROM_BTC = SwapType.FROM_BTC;

    /**
     * @internal
     */
    protected readonly tickSwapState = [FromBTCSwapState.PR_CREATED, FromBTCSwapState.CLAIM_COMMITED, FromBTCSwapState.EXPIRED];

    /**
     * @internal
     */
    readonly _pendingSwapStates = [
        FromBTCSwapState.PR_CREATED,
        FromBTCSwapState.QUOTE_SOFT_EXPIRED,
        FromBTCSwapState.CLAIM_COMMITED,
        FromBTCSwapState.BTC_TX_CONFIRMED,
        FromBTCSwapState.EXPIRED
    ];
    /**
     * @internal
     */
    readonly _claimableSwapStates = [FromBTCSwapState.BTC_TX_CONFIRMED];
    /**
     * @internal
     */
    readonly _swapDeserializer = FromBTCSwap;
    /**
     * @internal
     */
    readonly _synchronizer: (version?: string) => RelaySynchronizer<any, T["TX"], any> = (version?: string) => {
        const _version = version ?? "v1";
        const data = this.versionedSynchronizer[_version];
        if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
        return data.synchronizer;
    };

    /**
     * @internal
     */
    readonly _btcRpc: BitcoinRpcWithAddressIndex<any>;

    private readonly btcRelay: (version?: string) => BtcRelay<any, T["TX"], any> = (version?: string) => {
        const _version = version ?? "v1";
        const data = this.versionedBtcRelay[_version];
        if(data==null) throw new Error(`Invalid contract version ${_version} requested`);
        return data.btcRelay;
    };

    private readonly versionedBtcRelay: {
        [version: string]: {
            btcRelay: BtcRelay<any, T["TX"], any>
        }
    }= {};

    private readonly versionedSynchronizer: {
        [version: string]: {
            synchronizer: RelaySynchronizer<any, T["TX"], any>
        }
    }= {};

    /**
     * @param chainIdentifier
     * @param unifiedStorage Storage interface for the current environment
     * @param unifiedChainEvents On-chain event listener
     * @param chain
     * @param prices Pricing to use
     * @param tokens
     * @param versionedContracts
     * @param versionedSynchronizer
     * @param btcRpc Bitcoin RPC which also supports getting transactions by txoHash
     * @param lpApi
     * @param options
     * @param events Instance to use for emitting events
     */
    constructor(
        chainIdentifier: string,
        unifiedStorage: UnifiedSwapStorage<T>,
        unifiedChainEvents: UnifiedSwapEventListener<T>,
        chain: T["ChainInterface"],
        prices: ISwapPrice,
        tokens: WrapperCtorTokens,
        versionedContracts: {
            [version: string]: {
                swapContract: T["Contract"],
                swapDataConstructor: new (data: any) => T["Data"],
                btcRelay: BtcRelay<any, T["TX"], any>
            }
        },
        versionedSynchronizer: {
            [version: string]: {
                synchronizer: RelaySynchronizer<any, T["TX"], any>
            }
        },
        btcRpc: BitcoinRpcWithAddressIndex<any>,
        lpApi: IntermediaryAPI,
        options?: AllOptional<FromBTCWrapperOptions>,
        events?: EventEmitter<{swapState: [ISwap]}>
    ) {
        super(
            chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, lpApi,
            {
                ...options,
                bitcoinNetwork: options?.bitcoinNetwork ?? TEST_NETWORK,
                safetyFactor: options?.safetyFactor ?? 2,
                blocksTillTxConfirms: options?.blocksTillTxConfirms ?? 12,
                maxConfirmations: options?.maxConfirmations ?? 6,
                minSendWindow: options?.minSendWindow ?? 30*60, //Minimum time window for user to send in the on-chain funds for From BTC swap
                bitcoinBlocktime: options?.bitcoinBlocktime ?? 10*60
            },
            versionedContracts,
            events
        );
        this._btcRpc = btcRpc;
        this.versionedBtcRelay = versionedContracts;
        this.versionedSynchronizer = versionedSynchronizer;
    }

    /**
     * @inheritDoc
     * @internal
     */
    protected processEventInitialize(swap: FromBTCSwap<T>, event: InitializeEvent<T["Data"]>): Promise<boolean> {
        if(swap._state===FromBTCSwapState.PR_CREATED || swap._state===FromBTCSwapState.QUOTE_SOFT_EXPIRED) {
            swap._state = FromBTCSwapState.CLAIM_COMMITED;
            return Promise.resolve(true);
        }
        return Promise.resolve(false);
    }

    /**
     * @inheritDoc
     * @internal
     */
    protected async processEventClaim(swap: FromBTCSwap<T>, event: ClaimEvent<T["Data"]>): Promise<boolean> {
        if(swap._state!==FromBTCSwapState.FAILED && swap._state!==FromBTCSwapState.CLAIM_CLAIMED) {
            await swap._setBitcoinTxId(Buffer.from(event.result, "hex").reverse().toString("hex")).catch(e => {
                this.logger.warn("processEventClaim(): Error setting bitcoin txId: ", e);
            });
            swap._state = FromBTCSwapState.CLAIM_CLAIMED;
            return true;
        }
        return false;
    }

    /**
     * @inheritDoc
     * @internal
     */
    protected processEventRefund(swap: FromBTCSwap<T>, event: RefundEvent<T["Data"]>): Promise<boolean> {
        if(swap._state!==FromBTCSwapState.CLAIM_CLAIMED && swap._state!==FromBTCSwapState.FAILED) {
            swap._state = FromBTCSwapState.FAILED;
            return Promise.resolve(true);
        }
        return Promise.resolve(false);
    }

    /**
     * Returns the swap expiry, leaving enough time for the user to send a transaction and for it to confirm
     *
     * @param data Swap data
     * @param requiredConfirmations Confirmations required on the bitcoin side to settle the swap
     *
     * @internal
     */
    _getOnchainSendTimeout(data: SwapData, requiredConfirmations: number): bigint {
        const tsDelta = (this._options.blocksTillTxConfirms + requiredConfirmations) * this._options.bitcoinBlocktime * this._options.safetyFactor;
        return data.getExpiry() - BigInt(tsDelta);
    }

    /**
     * Pre-fetches claimer (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
     *  provided abortController
     *
     * @param signer Smartchain signer address initiating the swap
     * @param amountData
     * @param options Options as passed to the swap creation function
     * @param abortController
     * @param contractVersion
     *
     * @private
     */
    private async preFetchClaimerBounty(
        signer: string,
        amountData: AmountData,
        options: {
            feeSafetyFactorPPM: bigint,
            blockSafetyFactor: bigint,
            unsafeZeroWatchtowerFee: boolean
        },
        abortController: AbortController,
        contractVersion: string
    ): Promise<{
        feePerBlock: bigint,
        safetyFactor: bigint,
        startTimestamp: bigint,
        addBlock: bigint,
        addFee: bigint
    } | undefined> {
        const startTimestamp = BigInt(Math.floor(Date.now()/1000));

        if(options.unsafeZeroWatchtowerFee) {
            return {
                feePerBlock: 0n,
                safetyFactor: options.blockSafetyFactor,
                startTimestamp: startTimestamp,
                addBlock: 0n,
                addFee: 0n
            }
        }

        const dummyAmount = BigInt(Math.floor(Math.random()* 0x1000000));
        const dummySwapData = await this._contract(contractVersion).createSwapData(
            ChainSwapType.CHAIN, signer, signer, amountData.token,
            dummyAmount, this._contract(contractVersion).getHashForOnchain(randomBytes(20), dummyAmount, 3).toString("hex"),
            this.getRandomSequence(), startTimestamp, false, true,
            BigInt(Math.floor(Math.random() * 0x10000)), BigInt(Math.floor(Math.random() * 0x10000))
        );

        try {
            const [feePerBlock, btcRelayData, currentBtcBlock, claimFeeRate] = await Promise.all([
                this.btcRelay(contractVersion).getFeePerBlock(),
                this.btcRelay(contractVersion).getTipData(),
                this._btcRpc.getTipHeight(),
                this._contract(contractVersion).getClaimFee(signer, dummySwapData)
            ]);

            if(btcRelayData==null) throw new Error("Btc relay not initialized!");

            const currentBtcRelayBlock = btcRelayData.blockheight;
            const addBlock = Math.max(currentBtcBlock-currentBtcRelayBlock, 0);
            return {
                feePerBlock: feePerBlock * options.feeSafetyFactorPPM / 1_000_000n,
                safetyFactor: options.blockSafetyFactor,
                startTimestamp: startTimestamp,
                addBlock: BigInt(addBlock),
                addFee: claimFeeRate * options.feeSafetyFactorPPM / 1_000_000n
            }
        } catch (e) {
            abortController.abort(e);
            return undefined;
        }
    }

    /**
     * Returns calculated claimer bounty calculated from the claimer bounty data as fetched from preFetchClaimerBounty()
     *
     * @param data Parsed swap data returned from the intermediary
     * @param options Options as passed to the swap creation function
     * @param claimerBounty Claimer bounty data as fetched from {@link preFetchClaimerBounty} function
     *
     * @private
     */
    private getClaimerBounty(
        data: T["Data"],
        options: {
            blockSafetyFactor: bigint
        },
        claimerBounty: {
            feePerBlock: bigint,
            safetyFactor: bigint,
            startTimestamp: bigint,
            addBlock: bigint,
            addFee: bigint
        }
    ) : bigint {
        const tsDelta = data.getExpiry() - claimerBounty.startTimestamp;
        const blocksDelta = tsDelta / BigInt(this._options.bitcoinBlocktime) * options.blockSafetyFactor;
        const totalBlock = blocksDelta + claimerBounty.addBlock;
        return claimerBounty.addFee + (totalBlock * claimerBounty.feePerBlock);
    }

    /**
     * Verifies response returned from intermediary
     *
     * @param signer
     * @param resp Response as returned by the intermediary
     * @param amountData
     * @param lp Intermediary
     * @param options Options as passed to the swap creation function
     * @param data Parsed swap data returned by the intermediary
     * @param sequence Required swap sequence
     * @param claimerBounty Claimer bount data as returned from the preFetchClaimerBounty() pre-fetch promise
     * @param depositToken
     *
     * @throws {IntermediaryError} in case the response is invalid
     *
     * @private
     */
    private verifyReturnedData(
        signer: string,
        resp: FromBTCResponseType,
        amountData: AmountData,
        lp: Intermediary,
        options: {
            blockSafetyFactor: bigint
        },
        data: T["Data"],
        sequence: bigint,
        claimerBounty: {
            feePerBlock: bigint,
            safetyFactor: bigint,
            startTimestamp: bigint,
            addBlock: bigint,
            addFee: bigint
        },
        depositToken: string
    ): void {
        if(amountData.exactIn) {
            if(resp.amount !== amountData.amount) throw new IntermediaryError("Invalid amount returned");
        } else {
            if(resp.total !== amountData.amount) throw new IntermediaryError("Invalid total returned");
        }

        const requiredConfirmations = resp.confirmations;
        if(requiredConfirmations>this._options.maxConfirmations) throw new IntermediaryError("Requires too many confirmations");

        const totalClaimerBounty = this.getClaimerBounty(data, options, claimerBounty);

        if(
            data.getClaimerBounty() !== totalClaimerBounty ||
            data.getType()!=ChainSwapType.CHAIN ||
            data.getSequence() !== sequence ||
            data.getAmount() !== resp.total ||
            data.isPayIn() ||
            !data.isToken(amountData.token) ||
            !data.isOfferer(lp.getAddress(this.chainIdentifier)) ||
            !data.isClaimer(signer) ||
            !data.isDepositToken(depositToken) ||
            data.hasSuccessAction()
        ) {
            throw new IntermediaryError("Invalid data returned");
        }

        //Check that we have enough time to send the TX and for it to confirm
        const expiry = this._getOnchainSendTimeout(data, requiredConfirmations);
        const currentTimestamp = BigInt(Math.floor(Date.now()/1000));
        if((expiry - currentTimestamp) < BigInt(this._options.minSendWindow)) {
            throw new IntermediaryError("Send window too low");
        }

        const version = lp.getContractVersion(this.chainIdentifier);

        const lockingScript = toOutputScript(this._options.bitcoinNetwork, resp.btcAddress);
        const desiredExtraData = this._contract(version).getExtraData(lockingScript, resp.amount, requiredConfirmations);
        const desiredClaimHash = this._contract(version).getHashForOnchain(lockingScript, resp.amount, requiredConfirmations);
        if(!desiredClaimHash.equals(Buffer.from(data.getClaimHash(), "hex"))) {
            throw new IntermediaryError("Invalid claim hash returned!");
        }
        const extraData = data.getExtraData();
        if(extraData==null || !desiredExtraData.equals(Buffer.from(extraData, "hex"))) {
            throw new IntermediaryError("Invalid extra data returned!");
        }
    }

    /**
     * Returns a newly created legacy Bitcoin -> Smart chain swap using the PrTLC based escrow swap protocol,
     *  with the passed amount.
     *
     * @param recipient Smart chain signer's address on the destination chain
     * @param amountData Amount, token and exact input/output data for to swap
     * @param lps An array of intermediaries (LPs) to get the quotes from
     * @param options Optional additional quote options
     * @param additionalParams Optional additional parameters sent to the LP when creating the swap
     * @param abortSignal Abort signal
     */
    create(
        recipient: string,
        amountData: AmountData,
        lps: Intermediary[],
        options?: FromBTCOptions,
        additionalParams?: Record<string, any>,
        abortSignal?: AbortSignal
    ): {
        quote: Promise<FromBTCSwap<T>>,
        intermediary: Intermediary
    }[] {
        let feeSafetyFactorPPM: bigint = 1_500_000n;
        if(typeof(options?.feeSafetyFactor)==="bigint") {
            feeSafetyFactorPPM = options.feeSafetyFactor * 1_000_000n;
        } else if(typeof(options?.feeSafetyFactor)==="number") {
            feeSafetyFactorPPM = BigInt(Math.floor(options.feeSafetyFactor * 1_000_000));
        }
        const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps);

        const _options = {
            blockSafetyFactor: options?.blockSafetyFactor!=null ? BigInt(options.blockSafetyFactor) : 1n,
            feeSafetyFactorPPM,
            unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false
        };

        const sequence: bigint = this.getRandomSequence();

        const _abortController = extendAbortController(abortSignal);
        const pricePrefetchPromise: Promise<bigint | undefined> = this.preFetchPrice(amountData, _abortController.signal);
        const usdPricePrefetchPromise: Promise<number | undefined> = this.preFetchUsdPrice(_abortController.signal);
        const claimerBountyPrefetchPromise = mapArrayToObject(lpVersions, (contractVersion: string) => {
            return this.preFetchClaimerBounty(recipient, amountData, _options, _abortController, contractVersion);
        });
        const nativeTokenAddress = this._chain.getNativeCurrencyAddress();
        const feeRatePromise = this.preFetchFeeRate(recipient, amountData, undefined, _abortController, lpVersions);

        const _signDataPromise = mapArrayToObject(lpVersions, (contractVersion: string) => {
            return this._contract(contractVersion).preFetchBlockDataForSignatures == null ?
                this.preFetchSignData(Promise.resolve(true), contractVersion) :
                undefined;
        });

        return lps.map(lp => {
            return {
                intermediary: lp,
                quote: (async () => {
                    if(lp.services[SwapType.FROM_BTC]==null) throw new Error("LP service for processing from btc swaps not found!");
                    const version = lp.getContractVersion(this.chainIdentifier);

                    const abortController = extendAbortController(_abortController.signal);
                    const liquidityPromise: Promise<bigint | undefined> = this.preFetchIntermediaryLiquidity(amountData, lp, abortController, version);

                    try {
                        const {signDataPromise, resp} = await tryWithRetries(async(retryCount: number) => {
                            const {signDataPrefetch, response} = this._lpApi.initFromBTC(
                                this.chainIdentifier, lp.url, nativeTokenAddress,
                                {
                                    claimer: recipient,
                                    amount: amountData.amount,
                                    token: amountData.token.toString(),

                                    exactOut: !amountData.exactIn,
                                    sequence,

                                    claimerBounty: throwIfUndefined(claimerBountyPrefetchPromise[version], "Watchtower fee pre-fetch failed!"),
                                    feeRate: throwIfUndefined(feeRatePromise[version], "Network fee rate pre-fetch failed!"),
                                    additionalParams
                                },
                                this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined
                            );

                            let signDataPromise = _signDataPromise[version];
                            if(signDataPromise==null) {
                                signDataPromise = this.preFetchSignData(signDataPrefetch, version);
                            } else signDataPrefetch.catch(() => {});

                            return {
                                signDataPromise,
                                resp: await response
                            };
                        }, undefined, e => e instanceof RequestError, abortController.signal);

                        const data: T["Data"] = new (this._swapDataDeserializer(version))(resp.data);
                        data.setClaimer(recipient);

                        const swapFeeBtc = resp.swapFee * resp.amount / (data.getAmount() + resp.swapFee);

                        this.verifyReturnedData(recipient, resp, amountData, lp, _options, data, sequence, (await claimerBountyPrefetchPromise[version])!, nativeTokenAddress);
                        const [pricingInfo, signatureExpiry] = await Promise.all([
                            //Get intermediary's liquidity
                            this.verifyReturnedPrice(
                                lp.services[SwapType.FROM_BTC], false, resp.amount, resp.total,
                                amountData.token, {swapFeeBtc}, pricePrefetchPromise, usdPricePrefetchPromise, abortController.signal
                            ),
                            this.verifyReturnedSignature(recipient, data, resp, feeRatePromise[version], signDataPromise, version, abortController.signal),
                            this.verifyIntermediaryLiquidity(data.getAmount(), throwIfUndefined(liquidityPromise, "LP liquidity pre-fetch failed!")),
                        ]);

                        const quote = new FromBTCSwap<T>(this, {
                            pricingInfo,
                            url: lp.url,
                            expiry: signatureExpiry,
                            swapFee: resp.swapFee,
                            swapFeeBtc,
                            feeRate: (await feeRatePromise[version])!,
                            signatureData: resp,
                            data,
                            address: resp.btcAddress,
                            amount: resp.amount,
                            exactIn: amountData.exactIn ?? true,
                            requiredConfirmations: resp.confirmations,
                            contractVersion: version
                        } as FromBTCSwapInit<T["Data"]>);
                        return quote;
                    } catch (e) {
                        abortController.abort(e);
                        throw e;
                    }
                })()
            }
        });
    }

    /**
     * @inheritDoc
     */
    async recoverFromSwapDataAndState(
        init: {data: T["Data"], getInitTxId: () => Promise<string>, getTxBlock: () => Promise<{blockTime: number, blockHeight: number}>},
        state: SwapCommitState,
        contractVersion: string,
        lp?: Intermediary
    ): Promise<FromBTCSwap<T> | null> {
        const data = init.data;

        const swapInit: FromBTCSwapInit<T["Data"]> = {
            pricingInfo: {
                isValid: true,
                satsBaseFee: 0n,
                swapPriceUSatPerToken: 100_000_000_000_000n,
                realPriceUSatPerToken: 100_000_000_000_000n,
                differencePPM: 0n,
                feePPM: 0n,
            },
            url: lp?.url,
            expiry: 0,
            swapFee: 0n,
            swapFeeBtc: 0n,
            feeRate: "",
            signatureData: undefined,
            data,
            exactIn: false,
            contractVersion
        }
        const swap = new FromBTCSwap(this, swapInit);
        swap._commitTxId = await init.getInitTxId();
        const blockData = await init.getTxBlock();
        swap.createdAt = blockData.blockTime * 1000;
        swap._setInitiated();
        swap._state = FromBTCSwapState.CLAIM_COMMITED;
        await swap._sync(false, false, state);
        await swap._save();
        return swap;
    }

}
