import {ChainEvent, ChainType, isAbstractSigner} from "@atomiqlabs/base";
import {EventEmitter} from "events";
import {ISwap} from "./ISwap";
import {ISwapPrice} from "../prices/abstract/ISwapPrice";
import {IntermediaryError} from "../errors/IntermediaryError";
import {ChainIds, MultiChain} from "../swapper/Swapper";
import {UnifiedSwapEventListener} from "../events/UnifiedSwapEventListener";
import {SwapType} from "../enums/SwapType";
import {UnifiedSwapStorage} from "../storage/UnifiedSwapStorage";
import {SCToken} from "../types/Token";
import {getLogger} from "../utils/Logger";
import {PriceInfoType} from "../types/PriceInfoType";
import {fromHumanReadableString} from "../utils/TokenUtils";
import {UserError} from "../errors/UserError";
import {IntermediaryAPI} from "../intermediaries/apis/IntermediaryAPI";

export const DEFAULT_MAX_PARALLEL_SWAP_TICKS = 50;
export const DEFAULT_MAX_PARALLEL_SWAP_SYNCS = 50;

/**
 * Options for swap wrapper configuration
 *
 * @category Swaps/Base
 */
export type ISwapWrapperOptions = {
    getRequestTimeout?: number,
    postRequestTimeout?: number,
    /**
     * How many swaps to call `_tick()` for in parallel
     */
    maxParallelSwapTicks?: number,
    /**
     * How many swaps to call `_sync()` for in parallel
     */
    maxParallelSwapSyncs?: number,
    /**
     * Whether to save swaps that are not initialized into the persistent storage
     */
    saveUninitializedSwaps?: boolean
};

/**
 * Token configuration for wrapper constructors
 *
 * @category Swaps/Base
 */
export type WrapperCtorTokens<T extends ChainType = ChainType> = {
    [tokenAddress: string]: SCToken<T["ChainId"]>
};

/**
 * Type definition linking wrapper and swap types
 *
 * @category Swaps/Base
 */
export type SwapTypeDefinition<T extends ChainType, W extends ISwapWrapper<T, any>, S extends ISwap<T>> = {
    Wrapper: W;
    Swap: S;
};

/**
 * Base abstract class for swap handler implementations
 *
 * @category Swaps/Base
 */
export abstract class ISwapWrapper<
    T extends ChainType,
    D extends SwapTypeDefinition<T, ISwapWrapper<T, D>, ISwap<T, D>>,
    O extends ISwapWrapperOptions = ISwapWrapperOptions
> {
    /**
     * Swap type
     */
    abstract readonly TYPE: SwapType;
    /**
     * Function for deserializing swaps
     * @internal
     */
    abstract readonly _swapDeserializer: new (wrapper: D["Wrapper"], data: any) => D["Swap"];


    /**
     * Logger instance
     * @internal
     */
    protected readonly logger = getLogger(this.constructor.name+": ");
    /**
     * Persistent storage backend for the swaps
     * @internal
     */
    protected readonly unifiedStorage: UnifiedSwapStorage<T>;
    /**
     * Smart chain events listener for listening to and parsing of on-chain events
     * @internal
     */
    protected readonly unifiedChainEvents: UnifiedSwapEventListener<T>;
    /**
     * States of the swaps where {@link ISwap._tick} should be called every second
     * @internal
     */
    protected readonly tickSwapState?: Array<D["Swap"]["_state"]>;
    /**
     * In-memory mapping of pending (not initiated) swaps, utilizing weak references to automatically
     *  free memory when swaps are dereferenced in not initiated state
     * @internal
     */
    protected readonly pendingSwaps: Map<string, WeakRef<D["Swap"]>> = new Map();


    /**
     * Whether this wrapper is initialized (have to call {@link init} to initialize a wrapper)
     * @internal
     */
    protected isInitialized: boolean = false;
    /**
     * An interval for calling tick functions on the underlying swaps
     * @internal
     */
    protected tickInterval?: NodeJS.Timeout;
    /**
     * An internal abort controller for the running tick handler
     * @internal
     */
    protected tickAbortController?: AbortController;


    /**
     * States of the swaps in pending (non-final state), these are checked automatically on initial swap synchronization
     * @internal
     */
    abstract readonly _pendingSwapStates: Array<D["Swap"]["_state"]>;
    /**
     * Chain interface of the underlying smart chain
     * @internal
     */
    readonly _chain: T["ChainInterface"];
    /**
     * Pricing API
     * @internal
     */
    readonly _prices: ISwapPrice;
    /**
     * Wrapper options
     * @internal
     */
    readonly _options: O;
    /**
     * Tokens indexed by their token address
     * @internal
     */
    readonly _tokens: {
        [tokenAddress: string]: SCToken<T["ChainId"]>
    };
    /**
     * LP API Used to communicate with the LPs
     * @internal
     */
    readonly _lpApi: IntermediaryAPI;


    /**
     * Chain identifier string of this wrapper
     */
    readonly chainIdentifier: T["ChainId"];
    /**
     * Event emitter emitting `"swapState"` event when swap's state changes
     */
    readonly events: EventEmitter<{swapState: [D["Swap"]]}>;


    constructor(
        chainIdentifier: T["ChainId"],
        unifiedStorage: UnifiedSwapStorage<T>,
        unifiedChainEvents: UnifiedSwapEventListener<T>,
        chain: T["ChainInterface"],
        prices: ISwapPrice,
        tokens: WrapperCtorTokens,
        lpApi: IntermediaryAPI,
        options: O,
        events?: EventEmitter<{swapState: [ISwap]}>
    ) {
        if(options?.maxParallelSwapTicks!=null && options.maxParallelSwapTicks < 1)
            throw new Error("maxParallelSwapTicks must be at least 1!");
        if(options?.maxParallelSwapSyncs!=null && options.maxParallelSwapSyncs < 1)
            throw new Error("maxParallelSwapSyncs must be at least 1!");

        this.unifiedStorage = unifiedStorage;
        this.unifiedChainEvents = unifiedChainEvents;

        this.chainIdentifier = chainIdentifier;
        this._chain = chain;
        this._prices = prices;
        this.events = events || new EventEmitter();
        this._lpApi = lpApi;
        this._options = options;
        this._tokens = tokens;
    }

    /**
     * Parses the provided gas amount from its `string` or `bigint` representation to `bigint` base units.
     *
     * Defaults to `0n` if no gasAmount is provided
     *
     * @param gasAmount
     * @internal
     */
    protected parseGasAmount(gasAmount?: string | bigint): bigint {
        let result: bigint | undefined | null;
        if(typeof(gasAmount)==="string") {
            result = fromHumanReadableString(gasAmount, this._getNativeToken());
            if(result==null) throw new UserError("Invalid `gasAmount` option provided, not a numerical string!");
        } else {
            result = gasAmount;
        }
        return result ?? 0n;
    }

    /**
     * Pre-fetches swap price for a given swap
     *
     * @param amountData Amount data
     * @param abortSignal Abort signal
     * @returns Price of the token in uSats (micro sats)
     * @internal
     */
    protected preFetchPrice(amountData: { token: string }, abortSignal?: AbortSignal): Promise<bigint | undefined> {
        return this._prices.preFetchPrice(this.chainIdentifier, amountData.token, abortSignal).catch(e => {
            this.logger.error("preFetchPrice.token(): Error: ", e);
            return undefined;
        });
    }

    /**
     * Pre-fetches bitcoin's USD price
     *
     * @param abortSignal Abort signal
     * @internal
     */
    protected preFetchUsdPrice(abortSignal?: AbortSignal): Promise<number | undefined> {
        return this._prices.preFetchUsdPrice(abortSignal).catch(e => {
            this.logger.error("preFetchPrice.usd(): Error: ", e);
            return undefined;
        })
    }

    /**
     * Verifies returned price for swaps
     *
     * @param lpServiceData Service data for the service in question (TO_BTCLN, TO_BTC, etc.) of the given intermediary
     * @param send Whether this is a send (Smart chain -> Bitcoin) or receive (Bitcoin -> Smart chain) swap
     * @param amountSats Amount in BTC
     * @param amountToken Amount in token
     * @param token Token used in the swap
     * @param feeData Fee data as returned by the intermediary
     * @param pricePrefetchPromise Optional price pre-fetch promise
     * @param usdPricePrefetchPromise Optiona USD price pre-fetch promise
     * @param abortSignal Abort signal
     * @returns Price info object
     * @throws {IntermediaryError} if the calculated fee is too high
     *
     * @internal
     */
    protected async verifyReturnedPrice(
        lpServiceData: {swapBaseFee: number, swapFeePPM: number},
        send: boolean,
        amountSats: bigint,
        amountToken: bigint,
        token: string,
        feeData: {
            networkFee?: bigint,
            swapFeeBtc?: bigint
        },
        pricePrefetchPromise: Promise<bigint | undefined> = Promise.resolve(undefined),
        usdPricePrefetchPromise: Promise<number | undefined> = Promise.resolve(undefined),
        abortSignal?: AbortSignal
    ): Promise<PriceInfoType> {
        const swapBaseFee = BigInt(lpServiceData.swapBaseFee);
        const swapFeePPM = BigInt(lpServiceData.swapFeePPM);
        if(send && feeData.networkFee!=null) amountToken = amountToken - feeData.networkFee;

        const [isValidAmount, usdPrice] = await Promise.all([
            send ?
                this._prices.isValidAmountSend(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc) :
                this._prices.isValidAmountReceive(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc),
            usdPricePrefetchPromise.then(value => {
                if(value!=null) return value;
                return this._prices.preFetchUsdPrice(abortSignal);
            })
        ]);
        if(!isValidAmount.isValid) throw new IntermediaryError("Fee too high");
        isValidAmount.realPriceUsdPerBitcoin = usdPrice;

        return isValidAmount;
    }

    /**
     * Processes a single smart chain on-chain event
     *
     * @param event Smart chain event to process
     * @param swap A swap related to the event
     * @internal
     */
    protected abstract processEvent?(event: ChainEvent<T["Data"]>, swap: D["Swap"]): Promise<void>;

    /**
     * Starts the interval calling the {@link ISwap._tick} on all the known swaps in tick-enabled states
     * @internal
     */
    protected startTickInterval(): void {
        if(this.tickSwapState==null || this.tickSwapState.length===0) return;
        if(this.tickAbortController!=null) this.tickAbortController.abort("New tick interval has been started!");
        const abortController = this.tickAbortController = new AbortController();
        let run: () => Promise<void>;
        run = async () => {
            if(!this.isInitialized) return;
            await this.tick(undefined, abortController.signal).catch(e => {
                if(abortController.signal.aborted) return;
                this.logger.warn("startTickInterval(): Tick on swaps failed, error: ", e);
            });
            if(abortController.signal.aborted) return;
            if(!this.isInitialized) return;
            this.tickInterval = setTimeout(run, 1000);
        }
        run();
    }

    /**
     * Runs checks on passed swaps, syncing their state from on-chain data
     *
     * @param pastSwaps Swaps to check
     * @internal
     */
    protected async _checkPastSwaps(pastSwaps: D["Swap"][]): Promise<{changedSwaps: D["Swap"][], removeSwaps: D["Swap"][]}> {
        const changedSwaps: D["Swap"][] = [];
        const removeSwaps: D["Swap"][] = [];

        await Promise.all(pastSwaps.map((swap: D["Swap"]) =>
            swap._sync(false).then(changed => {
                if(swap.isQuoteExpired()) {
                    removeSwaps.push(swap);
                    this.logger.debug("_checkPastSwaps(): Removing expired swap: "+swap.getId());
                } else {
                    if(changed) changedSwaps.push(swap);
                }
            }).catch(e => this.logger.error("_checkPastSwaps(): Error when checking swap "+swap.getId()+": ", e))
        ));

        return {changedSwaps, removeSwaps};
    }


    /**
     * Initializes the swap wrapper, needs to be called before any other action can be taken
     *
     * @param noTimers Whether to skip scheduling a tick timer for the swaps, if the tick timer is not initiated
     *  the swap states depending on e.g. expiry can be out of sync with the actual expiration of the swap
     * @param noCheckPastSwaps Whether to skip checking past swaps on initialization (by default all pending swaps
     *  are re-checked on init, and their state is synchronized from the on-chain data)
     */
    public async init(noTimers: boolean = false, noCheckPastSwaps: boolean = false): Promise<void> {
        if(this.isInitialized) return;

        if(!noCheckPastSwaps) {
            //Save events received in the meantime into the event queue and process them only after we've checked and
            // processed all the past swaps
            let eventQueue: {
                event: ChainEvent<T["Data"]>,
                swap: D["Swap"]
            }[] = [];
            const initListener = (event: ChainEvent<T["Data"]>, swap: D["Swap"]) => {
                eventQueue.push({event, swap});
                return Promise.resolve();
            }
            if(this.processEvent!=null) this.unifiedChainEvents.registerListener(this.TYPE, initListener, this._swapDeserializer.bind(null, this));

            await this.checkPastSwaps();

            if(this.processEvent!=null) {
                //Process accumulated event queue
                for(let event of eventQueue) {
                    await this.processEvent(event.event, event.swap);
                }

                //Unregister the temporary event handler
                this.unifiedChainEvents.unregisterListener(this.TYPE);
            }
        }

        if(this.processEvent!=null) this.unifiedChainEvents.registerListener(this.TYPE, this.processEvent.bind(this), this._swapDeserializer.bind(null, this));

        this.isInitialized = true;

        if(!noTimers) this.startTickInterval();

        // this.logger.info("init(): Swap wrapper initialized");
    }

    /**
     * Un-subscribes from event listeners on the smart chain, terminates the tick interval and stops this wrapper
     */
    public async stop() {
        this.isInitialized = false;
        this.unifiedChainEvents.unregisterListener(this.TYPE);
        this.logger.info("stop(): Swap wrapper stopped");
        if(this.tickInterval!=null) {
            clearTimeout(this.tickInterval);
            delete this.tickInterval;
        }
        if(this.tickAbortController!=null) {
            this.tickAbortController.abort("Wrapper instance stopped!");
            delete this.tickAbortController;
        }
    }

    /**
     * Runs checks on all the known pending swaps, syncing their state from on-chain data
     *
     * @remarks Doesn't work properly if you pass non-persisted swaps
     *
     * @param pastSwaps Optional array of past swaps to check, otherwise all relevant swaps will be fetched
     *  from the persistent storage
     * @param noSave Whether to skip saving the swap changes in the persistent storage
     */
    public async checkPastSwaps(pastSwaps?: D["Swap"][], noSave?: boolean): Promise<{ removeSwaps: D["Swap"][], changedSwaps: D["Swap"][] }> {
        if (pastSwaps == null) pastSwaps = await this.unifiedStorage.query<D["Swap"]>(
            [[{key: "type", value: this.TYPE}, {key: "state", value: this._pendingSwapStates}]],
            (val: any) => new this._swapDeserializer(this, val)
        );

        const maxParallelSyncs = this._options.maxParallelSwapSyncs ?? DEFAULT_MAX_PARALLEL_SWAP_SYNCS;

        const totalRemoveSwaps: D["Swap"][] = [];
        const totalChangedSwaps: D["Swap"][] = [];
        for(let i=0; i<pastSwaps.length; i+=maxParallelSyncs) {
            const {removeSwaps, changedSwaps} = await this._checkPastSwaps(pastSwaps.slice(i, i+maxParallelSyncs));
            if (!noSave) {
                await this.unifiedStorage.removeAll(removeSwaps, true);
                await this.unifiedStorage.saveAll(changedSwaps, true);
                changedSwaps.forEach(swap => swap._emitEvent());
                removeSwaps.forEach(swap => swap._emitEvent());
            }
            totalRemoveSwaps.push(...removeSwaps);
            totalChangedSwaps.push(...changedSwaps);
        }

        return {
            removeSwaps: totalRemoveSwaps,
            changedSwaps: totalChangedSwaps
        }
    }

    /**
     * Invokes {@link ISwap._tick} on all the known swaps
     *
     * @param swaps Optional array of swaps to invoke `_tick()` on, otherwise all relevant swaps will be fetched
     *  from the persistent storage
     * @param abortSignal Abort signal
     */
    public async tick(swaps?: D["Swap"][], abortSignal?: AbortSignal): Promise<void> {
        if(swaps==null) swaps = await this.unifiedStorage.query<D["Swap"]>(
            [[{key: "type", value: this.TYPE}, {key: "state", value: this.tickSwapState}]],
            (val: any) => new this._swapDeserializer(this, val)
        );
        abortSignal?.throwIfAborted();

        const parallelTicks = this._options.maxParallelSwapTicks ?? DEFAULT_MAX_PARALLEL_SWAP_TICKS;

        let promises: Promise<any>[] = [];
        for(let pendingSwap of this.pendingSwaps.values()) {
            const value = pendingSwap.deref();
            if(value != null) promises.push(value._tick(true).catch(e => {
                this.logger.warn(`tick(): Error ticking swap ${value.getId()}: `, e);
            }));
            if(promises.length >= parallelTicks) {
                await Promise.all(promises);
                abortSignal?.throwIfAborted();
                promises = [];
            }
        }

        for(let value of swaps) {
            promises.push(value._tick(true).catch(e => {
                this.logger.warn(`tick(): Error ticking swap ${value.getId()}: `, e);
            }));
            if(promises.length >= parallelTicks) {
                await Promise.all(promises);
                abortSignal?.throwIfAborted();
                promises = [];
            }
        }

        if(promises.length>0) await Promise.all(promises);
        abortSignal?.throwIfAborted();
    }

    /**
     * Returns the smart chain's native token used to pay for fees
     * @internal
     */
    _getNativeToken(): SCToken<T["ChainId"]> {
        return this._tokens[this._chain.getNativeCurrencyAddress()];
    }

    /**
     * Saves the swap, if it is not initiated it is only saved to pending swaps
     *
     * @param swap Swap to save
     *
     * @internal
     */
    _saveSwapData(swap: D["Swap"]): Promise<void> {
        if(!this._options.saveUninitializedSwaps) {
            if(!swap.isInitiated()) {
                this.logger.debug("saveSwapData(): Swap "+swap.getId()+" not initiated, saving to pending swaps");
                this.pendingSwaps.set(swap.getId(), new WeakRef<D["Swap"]>(swap));
                return Promise.resolve();
            } else {
                this.pendingSwaps.delete(swap.getId());
            }
        }
        return this.unifiedStorage.save(swap);
    }

    /**
     * Removes the swap from the persistent storage and pending swaps
     *
     * @param swap Swap to remove
     *
     * @internal
     */
    _removeSwapData(swap: D["Swap"]): Promise<void> {
        this.pendingSwaps.delete(swap.getId());
        if(!swap._persisted) return Promise.resolve();
        return this.unifiedStorage.remove(swap);
    }

    /**
     * Retrieves a swap by its ID from the pending swap mapping
     *
     * @param id
     *
     * @internal
     */
    _getPendingSwap(id: string): D["Swap"] | null {
        return this.pendingSwaps.get(id)?.deref() ?? null;
    }

    /**
     * @internal
     */
    async _getSignerAddress(signer?: string | T["Signer"] | T["NativeSigner"]): Promise<string | undefined> {
        let address: string | undefined = undefined;
        if(signer!=null) {
            if (typeof (signer) === "string") {
                address = signer;
            } else if (isAbstractSigner(signer)) {
                address = signer.getAddress();
            } else {
                address = (await this._chain.wrapSigner(signer)).getAddress();
            }
        }
        return address;
    }

}