import {
    ChainEvents,
    ChainSwapType,
    ClaimEvent,
    EventListener,
    InitializeEvent,
    RefundEvent,
    SwapEvent
} from "@atomiqlabs/base";
import {StarknetSwapData} from "../swaps/StarknetSwapData";
import {
    bigNumberishToBuffer,
    bytes31SpanToBuffer, findLastIndex,
    getLogger,
    onceAsync,
    parseInitFunctionCalldata,
    timeoutPromise,
    toHex
} from "../../utils/Utils";
import {StarknetSwapContract} from "../swaps/StarknetSwapContract";
import {BigNumberish, hash, Provider} from "starknet";
import {StarknetAbiEvent} from "../contract/modules/StarknetContractEvents";
import {EscrowManagerAbiType} from "../swaps/EscrowManagerAbi";
import {ExtractAbiFunctionNames} from "abi-wan-kanabi/dist/kanabi";
import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";

export type StarknetTraceCall = {
    calldata: string[],
    contract_address: string,
    entry_point_selector: string,
    calls: StarknetTraceCall[]
};

/**
 * Solana on-chain event handler for front-end systems without access to fs, uses pure WS to subscribe, might lose
 *  out on some events if the network is unreliable, front-end systems should take this into consideration and not
 *  rely purely on events
 */
export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {

    protected readonly listeners: EventListener<StarknetSwapData>[] = [];
    protected readonly provider: Provider;
    protected readonly starknetSwapContract: StarknetSwapContract;
    protected eventListeners: number[] = [];
    protected readonly logger = getLogger("StarknetChainEventsBrowser: ");

    protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType> = "initialize";
    protected initEntryPointSelector = BigInt(hash.starknetKeccak(this.initFunctionName));

    protected stopped: boolean;
    protected pollIntervalSeconds: number;

    private timeout: number;

    constructor(starknetSwapContract: StarknetSwapContract, pollIntervalSeconds: number = 5) {
        this.provider = starknetSwapContract.provider;
        this.starknetSwapContract = starknetSwapContract;
        this.pollIntervalSeconds = pollIntervalSeconds;
    }

    findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData {
        if(
            BigInt(call.contract_address)===BigInt(this.starknetSwapContract.contract.address) &&
            BigInt(call.entry_point_selector)===this.initEntryPointSelector
        ) {
            //Found, check correct escrow hash
            const {escrow, extraData} = parseInitFunctionCalldata(call.calldata, claimHandler);
            if("0x"+escrow.getEscrowHash()===toHex(escrowHash)) {
                if(extraData.length!==0) {
                    escrow.setExtraData(bytes31SpanToBuffer(extraData, 42).toString("hex"));
                }
                return escrow;
            }
        }
        for(let _call of call.calls) {
            const found = this.findInitSwapData(_call, escrowHash, claimHandler);
            if(found!=null) return found;
        }
        return null;
    }

    /**
     * Returns async getter for fetching on-demand initialize event swap data
     *
     * @param event
     * @param claimHandler
     * @private
     * @returns {() => Promise<StarknetSwapData>} getter to be passed to InitializeEvent constructor
     */
    private getSwapDataGetter(
        event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Initialize">,
        claimHandler: IClaimHandler<any, any>
    ): () => Promise<StarknetSwapData> {
        return async () => {
            const trace: any = await this.provider.getTransactionTrace(event.txHash);
            if(trace==null) return null;
            if(trace.execute_invocation.revert_reason!=null) return null;
            return this.findInitSwapData(trace.execute_invocation as any, event.params.escrow_hash, claimHandler);
        }
    }

    protected parseInitializeEvent(
        event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Initialize">
    ): InitializeEvent<StarknetSwapData> {
        const escrowHashBuffer = bigNumberishToBuffer(event.params.escrow_hash, 32);
        const escrowHash = escrowHashBuffer.toString("hex");
        const claimHandlerHex = toHex(event.params.claim_handler);
        const claimHandler = this.starknetSwapContract.claimHandlersByAddress[claimHandlerHex];
        if(claimHandler==null) {
            this.logger.warn("parseInitializeEvent("+escrowHash+"): Unknown claim handler with claim: "+claimHandlerHex);
            return null;
        }
        const swapType: ChainSwapType = claimHandler.getType();

        this.logger.debug("InitializeEvent claimHash: "+toHex(event.params.claim_data)+" escrowHash: "+escrowHash);
        return new InitializeEvent<StarknetSwapData>(
            escrowHash,
            swapType,
            onceAsync<StarknetSwapData>(this.getSwapDataGetter(event, claimHandler))
        );
    }

    protected parseRefundEvent(
        event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Refund">
    ): RefundEvent<StarknetSwapData> {
        const escrowHashBuffer = bigNumberishToBuffer(event.params.escrow_hash, 32);
        const escrowHash = escrowHashBuffer.toString("hex");
        this.logger.debug("RefundEvent claimHash: "+toHex(event.params.claim_data)+" escrowHash: "+escrowHash);
        return new RefundEvent<StarknetSwapData>(escrowHash);
    }

    protected parseClaimEvent(
        event: StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Claim">
    ): ClaimEvent<StarknetSwapData> {
        const escrowHashBuffer = bigNumberishToBuffer(event.params.escrow_hash, 32);
        const escrowHash = escrowHashBuffer.toString("hex");
        const claimHandlerHex = toHex(event.params.claim_handler);
        const claimHandler = this.starknetSwapContract.claimHandlersByAddress[claimHandlerHex];
        if(claimHandler==null) {
            this.logger.warn("parseClaimEvent("+escrowHash+"): Unknown claim handler with claim: "+claimHandlerHex);
            return null;
        }
        const witnessResult = claimHandler.parseWitnessResult(event.params.witness_result);
        this.logger.debug("ClaimEvent claimHash: "+toHex(event.params.claim_data)+
            " witnessResult: "+witnessResult+" escrowHash: "+escrowHash);
        return new ClaimEvent<StarknetSwapData>(escrowHash, witnessResult);
    }

    /**
     * Processes event as received from the chain, parses it & calls event listeners
     *
     * @param events
     * @param currentBlockNumber
     * @param currentBlockTimestamp
     * @protected
     */
    protected async processEvents(
        events : StarknetAbiEvent<
            EscrowManagerAbiType,
            "escrow_manager::events::Initialize" | "escrow_manager::events::Refund" | "escrow_manager::events::Claim"
        >[],
        currentBlockNumber: number,
        currentBlockTimestamp: number
    ) {
        const blockTimestampsCache: {[blockNumber: string]: number} = {};
        const getBlockTimestamp: (blockNumber: number) => Promise<number> = async (blockNumber: number)=> {
            const blockNumberString = blockNumber.toString();
            blockTimestampsCache[blockNumberString] ??= (await this.provider.getBlockWithTxHashes(blockNumber)).timestamp;
            return blockTimestampsCache[blockNumberString];
        }

        const parsedEvents: SwapEvent<StarknetSwapData>[] = [];

        for(let event of events) {
            let parsedEvent: SwapEvent<StarknetSwapData>;
            switch(event.name) {
                case "escrow_manager::events::Claim":
                    parsedEvent = this.parseClaimEvent(event as any);
                    break;
                case "escrow_manager::events::Refund":
                    parsedEvent = this.parseRefundEvent(event as any);
                    break;
                case "escrow_manager::events::Initialize":
                    parsedEvent = this.parseInitializeEvent(event as any);
                    break;
            }
            const timestamp = (event.blockNumber==null || event.blockNumber===currentBlockNumber) ? currentBlockTimestamp : await getBlockTimestamp(event.blockNumber);
            parsedEvent.meta = {
                blockTime: timestamp,
                txId: event.txHash,
                timestamp //Maybe deprecated
            } as any;
            parsedEvents.push(parsedEvent);
        }

        for(let listener of this.listeners) {
            await listener(parsedEvents);
        }
    }

    protected async checkEvents(lastBlockNumber: number, lastTxHash: string): Promise<{txHash: string, blockNumber: number}> {
        //Get pending events
        let pendingEvents = await this.starknetSwapContract.Events.getContractBlockEvents(
            ["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
            []
        );
        if(lastTxHash!=null) {
            const latestProcessedEventIndex = findLastIndex(pendingEvents, val => val.txHash===lastTxHash);
            if(latestProcessedEventIndex!==-1) pendingEvents.splice(0, latestProcessedEventIndex+1);
        }
        if(pendingEvents.length>0) {
            await this.processEvents(pendingEvents, null, Math.floor(Date.now()/1000));
            lastTxHash = pendingEvents[pendingEvents.length-1].txHash;
        }

        const currentBlock = await this.provider.getBlockWithTxHashes("latest");
        const currentBlockNumber: number = (currentBlock as any).block_number;
        if(lastBlockNumber!=null && currentBlockNumber>lastBlockNumber) {
            const events = await this.starknetSwapContract.Events.getContractBlockEvents(
                ["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
                [],
                lastBlockNumber+1,
                currentBlockNumber
            );
            if(lastTxHash!=null) {
                const latestProcessedEventIndex = findLastIndex(events, val => val.txHash === lastTxHash);
                if (latestProcessedEventIndex !== -1) events.splice(0, latestProcessedEventIndex + 1);
            }
            if(events.length>0) {
                await this.processEvents(events, currentBlockNumber, currentBlock.timestamp);
                lastTxHash = events[events.length - 1].txHash;
            }
        }
        return {
            txHash: lastTxHash,
            blockNumber: currentBlockNumber
        };
    }

    /**
     * Sets up event handlers listening for swap events over websocket
     *
     * @protected
     */
    protected async setupPoll(
        lastBlockNumber?: number,
        lastTxHash?: string,
        saveLatestProcessedBlockNumber?: (blockNumber: number, lastTxHash: string) => Promise<void>
    ) {
        this.stopped = false;
        let func;
        func = async () => {
            await this.checkEvents(lastBlockNumber, lastTxHash).then(({blockNumber, txHash}) => {
                lastBlockNumber = blockNumber;
                lastTxHash = txHash;
                if(saveLatestProcessedBlockNumber!=null) return saveLatestProcessedBlockNumber(blockNumber, lastTxHash);
            }).catch(e => {
                this.logger.error("setupPoll(): Failed to fetch starknet log: ", e);
            });
            if(this.stopped) return;
            this.timeout = setTimeout(func, this.pollIntervalSeconds*1000);
        };
        await func();
    }

    init(): Promise<void> {
        this.setupPoll();
        return Promise.resolve();
    }

    async stop(): Promise<void> {
        this.stopped = true;
        if(this.timeout!=null) clearTimeout(this.timeout);
        this.eventListeners = [];
    }

    registerListener(cbk: EventListener<StarknetSwapData>): void {
        this.listeners.push(cbk);
    }

    unregisterListener(cbk: EventListener<StarknetSwapData>): boolean {
        const index = this.listeners.indexOf(cbk);
        if(index>=0) {
            this.listeners.splice(index, 1);
            return true;
        }
        return false;
    }
}
