import { BigNumber } from "ethers";
import { preLaunchReceipt } from "../types";
import { WithdrawReceipt, ReceiptNFTKind } from "../../../types";
import { EthereumContractService } from "../contract";

export class ReceiptClient {

    private readonly contractService: EthereumContractService;
    
    get contract() { return this.contractService.contract; }

    constructor(contract: EthereumContractService) {
        this.contractService = contract;
    }

    // NOTE: Stake/pretoken receipts intentionally ignored for withdraw view.
    async allReceipts(address: string): Promise<preLaunchReceipt[]> {
        return this.fetchPreLaunchReceipts(address);
    }

    async stakeReceipts(address: string): Promise<preLaunchReceipt[]> {
        try {
            const receipts = await this.fetchPreLaunchReceipts(address, ReceiptNFTKind.STAKE);
            return receipts;
        } catch (err) {
            return [];
        }
    }
    
    async pretokenReceipts(address: string): Promise<preLaunchReceipt[]> {
        return this.fetchPreLaunchReceipts(address, ReceiptNFTKind.PRETOKEN_PURCHASE);
    }

    /**
     * Fetch ReceiptNFT data (stake/pretoken) for an address, optionally filtered by kind.
     */
    async fetchPreLaunchReceipts(address: string, type?: ReceiptNFTKind): Promise<preLaunchReceipt[]> {
        const receiptContract = this.contract.ReceiptNFT;

        const tokenIds = await this.getOwnedReceiptNFTsFor(address);
        const results: preLaunchReceipt[] = [];

        for (const idBN of tokenIds) {
            try {
                const receiptData = await receiptContract.getReceipt(idBN);

                if (type !== undefined && receiptData.kind !== type) continue;

                results.push({ 
                    tokenId: idBN.toBigInt(), 
                    receipt: {
                        account: receiptData.account,
                        currency: receiptData.currency,
                        kind: receiptData.kind,
                        indexAtMint: receiptData.indexAtMint.toBigInt(),
                        principal: {
                            amount: receiptData.principal.toBigInt(),
                            decimals: 18,
                            symbol: "LiqETH"
                        },
                        shares: {
                            amount: receiptData.shares.toBigInt(),
                            decimals: 18,
                            symbol: "LiqETH"
                        },
                        timestamp: new Date(Number(receiptData.timestamp.toString()) * 1000).toLocaleString(),
                    }
                });
            } catch (err) {
                console.warn(`Failed to load receipt for tokenId=${idBN.toString()}`, err);
                continue;
            }
        }

        return results;
    }


    /**
     * 
     * @param address (string) to fetch receipts for
     * @returns preLaunchReceipt[]
     */
    async fetchWithdrawReceipts(address: string): Promise<WithdrawReceipt[]> {
        // first figure out which tokenIds this address owns, from events
        const tokenIds = await this.getOwnedWithdrawReceiptsFor(address);
        const results: WithdrawReceipt[] = [];

        // next fetch on-chain token data just for those ids
        for (const idBN of tokenIds) {
            try {
                const receiptData = await this.contract.WithdrawalQueue.info(idBN);

                // status: ready if readyAt <= now; otherwise queued
                const readyAtMs = Number(receiptData.readyAt) * 1000;
                const status = readyAtMs <= Date.now() ? 'ready' : 'queued';

                results.push({
                    tokenId: idBN.toBigInt(),
                    receipt: {
                        amount: {
                            amount: receiptData.ethAmount.toBigInt(),
                            decimals: 18,
                            symbol: "ETH",
                        },
                        readyAt: readyAtMs,
                        chain: 'ETH',
                        status,
                    }
                });
            } catch (err) {
                // in case of any mismatch or race, just skip this id
                console.warn(`Failed to load receipt for tokenId=${idBN.toString()}`, err);
                continue;
            }
        }

        return results;
    }



    private async getOwnedWithdrawReceiptsFor(
        owner: string,
        fromBlock = 0,
        toBlock: number | string = "latest"
    ): Promise<BigNumber[]> {
        const contract = this.contract.WithdrawalQueue;

        // Logs where address received tokens
        const toLogs = await contract.queryFilter(
            contract.filters.Transfer(null, owner),
            fromBlock,
            toBlock
        );

        // Logs where address sent tokens (including burns from owner → 0)
        const fromLogs = await contract.queryFilter(
            contract.filters.Transfer(owner, null),
            fromBlock,
            toBlock
        );

        const owned = new Set<string>();

        // Add all received tokenIds
        for (const e of toLogs) {
            const tokenId = e.args?.tokenId;
            if (!tokenId) continue;
            owned.add(tokenId.toString());
        }

        // Remove all sent tokenIds
        for (const e of fromLogs) {
            const tokenId = e.args?.tokenId;
            if (!tokenId) continue;
            owned.delete(tokenId.toString());
        }

        // Convert to BigNumbers
        return Array.from(owned).map((id) => BigNumber.from(id));
    }

    private async getOwnedReceiptNFTsFor(
        owner: string,
        fromBlock = 0,
        toBlock: number | string = "latest"
    ): Promise<BigNumber[]> {
        const receiptContract = this.contract.ReceiptNFT;

        const toLogs = await receiptContract.queryFilter(
            receiptContract.filters.Transfer(null, owner),
            fromBlock,
            toBlock
        );

        const fromLogs = await receiptContract.queryFilter(
            receiptContract.filters.Transfer(owner, null),
            fromBlock,
            toBlock
        );

        const owned = new Set<string>();

        for (const e of toLogs) {
            const tokenId = e.args?.tokenId;
            if (!tokenId) continue;
            owned.add(tokenId.toString());
        }

        for (const e of fromLogs) {
            const tokenId = e.args?.tokenId;
            if (!tokenId) continue;
            owned.delete(tokenId.toString());
        }

        return Array.from(owned).map((id) => BigNumber.from(id));
    }
}
