import { BigNumber, ethers } from 'ethers';
import { ChainID, EvmChainID, PublicKey as WirePubKey } from '@wireio/core';
import {
    IStakingClient,
    OPPAssertion,
    Portfolio,
    StakerConfig,
    TrancheSnapshot
} from '../../types';
import { EthereumContractService } from './contract';
import { preLaunchReceipt } from './types';
import { buildEthereumTrancheSnapshot } from './utils';
import { ConvertClient } from './clients/convert.client';
import { StakeClient } from './clients/stake.client';
import { PretokenClient } from './clients/pretoken.client';
import { OPPClient } from './clients/opp.client';
import { ReceiptClient } from './clients/receipt.client';

export class EthereumStakingClient implements IStakingClient {
    private readonly provider: ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider;
    public readonly pubKey?: WirePubKey;
    private readonly signer?: ethers.Signer;
    private readonly contractService: EthereumContractService;

    private convertClient: ConvertClient;
    private pretokenClient: PretokenClient;
    private stakeClient: StakeClient;
    private oppClient: OPPClient;
    private receiptClient: ReceiptClient;


    get contract() { return this.contractService.contract; }
    get network() { return this.config.network; }

    constructor(private config: StakerConfig) {
        try {
            if (config.provider) {
                this.provider = config.provider as ethers.providers.Web3Provider;
                this.signer = this.provider.getSigner();
            }
            else {
                this.provider = new ethers.providers.JsonRpcProvider(config.network.rpcUrls[0]);
            }
            this.pubKey = config.pubKey;

            this.contractService = new EthereumContractService({
                provider: this.provider,
                signer: this.signer,
            });

            this.convertClient = new ConvertClient(this.contractService);
            this.pretokenClient = new PretokenClient(this.contractService);
            this.stakeClient = new StakeClient(this.contractService);
            this.oppClient = new OPPClient(this.contractService);
            this.receiptClient = new ReceiptClient(this.contractService);
        }
        catch (error) {
            // console.error('Error initializing EthereumStakingClient:', error);
            throw error;
        }
    }

    // ---------------------------------------------------------------------
    // Public IStakingClient Interface Methods
    // ---------------------------------------------------------------------

    /**
     * Deposit native ETH into the liqETH protocol via DepositManager.
     * @param amount  Amount in wei (or something convertible to BigNumber).
     *                Keep this as a bigint / string in the caller; avoid JS floats.
     * @returns transaction hash
     */
    async deposit(amount: number | string | bigint | BigNumber): Promise<string> {
        this.ensureUser();

        const amountWei = BigNumber.isBigNumber(amount)
            ? amount
            : BigNumber.from(amount);

        const result = await this.convertClient.performDeposit(amountWei);
        return result.txHash;
    }

    /**
     * Withdraw native ETH from the liqETH protocol via the liqeth safeBurn function, which burns the LiqETH and adds the user to the withdrawal queue.
     * @param amount  Amount in wei (or something convertible to BigNumber).
     * @returns transaction hash
     */
    async withdraw(amount: bigint): Promise<string> {
        this.ensureUser();

        const address = await this.signer!.getAddress();
        const amountWei = BigNumber.from(amount);

        const result = await this.convertClient.performWithdraw(address, amountWei)
        return result.txHash;
    }


    /**
     * Stake liqETH via DepositManager.
     * @param amount  Amount in wei - Keep this as a bigint / string in the caller; avoid JS floats.
     * @returns transaction hash
     */
    async stake(amount: bigint): Promise<string> {
        this.ensureUser();

        const walletAddress = await this.signer!.getAddress();
        const amountWei = BigNumber.from(amount);

        const result = await this.stakeClient.performStake(amountWei, walletAddress);
        return result.txHash;
    }


    // TODO
    async unstake(): Promise<string> {
        throw new Error("Method not yet implemented.");
    }


    /**
     * ETH Prelaunch function to unstake liqEth
     * @param tokenId ReceiptNFT tokenId for the owned NFT that will be burned
     * @param recipient Address to receive the liqEth funds linked to the burned NFT
     * @returns the transaction hash
     */
    async unstakePrelaunch(tokenId: bigint, recipient: string): Promise<string> {
        this.ensureUser();

        const tokenIdBigNum = BigNumber.from(tokenId)
        const result = await this.stakeClient.performWithdrawStake(tokenIdBigNum, recipient);
        return result.txHash;
    }


    async buy(amount: bigint): Promise<string> {
        this.ensureUser();

        const buyer = await this.signer!.getAddress();

        // ! Hoodi only - check if the mock aggregator price is stale, and if so, update it before submitting the buy request
        await this.updateMockAggregatorPrice();

        let result = await this.pretokenClient.purchasePretokensWithLiqETH(amount, buyer);
        return result && result.txHash ? result.txHash : "Error - no resulting txHash";
    }


    /**
     * Resolve the user's ETH + liqETH balances.
     *
     * native  = ETH in wallet
     * actual  = liqETH token balance (ERC-20)
     * tracked = liqETH tracked balance (protocol/accounting view)
     */
    async getPortfolio(): Promise<Portfolio> {
        this.ensureUser();

        const walletAddress = await this.signer!.getAddress();

        // 1) Native ETH balance
        const nativeBalance = await this.provider.getBalance(walletAddress);
        const nativeDecimals = this.network?.nativeCurrency?.decimals ?? 18;
        const nativeSymbol = this.network?.nativeCurrency?.symbol ?? 'ETH';

        // 2) liqETH ERC-20 balance (actual)
        const liqBalance: ethers.BigNumber = await this.contract.LiqEth.balanceOf(walletAddress);
        const liqSymbol = 'Liq' + (this.network?.nativeCurrency?.symbol ?? 'ETH');

        // 3) staked liqEth ERC-20 balance (calculate from receipts)
        let stakeReceipts = await this.receiptClient.stakeReceipts(walletAddress);
        let stakeBalanceBN = BigNumber.from(0);
        for (let r of stakeReceipts) {
            stakeBalanceBN = stakeBalanceBN.add(BigNumber.from(r.receipt.principal.amount));
        }

        // 4) WIRE pretoken balance
        const wireBalance: ethers.BigNumber = await this.contract.Pretoken.balanceOf(walletAddress);

        const portfolio: Portfolio = {
            native: {
                amount: nativeBalance.toBigInt(),
                decimals: nativeDecimals,
                symbol: nativeSymbol,
            },
            liq: {
                amount: liqBalance.toBigInt(),
                decimals: nativeDecimals,
                symbol: liqSymbol,
            },
            staked: {
                amount: stakeBalanceBN.toBigInt(),
                decimals: nativeDecimals,
                symbol: liqSymbol,
            },
            wire: {
                amount: wireBalance.toBigInt(),
                decimals: 18,
                symbol: '$WIRE',
            },
            chainID: this.network.chainId
        }
        return portfolio;
    }

    /**
     * ETH Prelaunch function to list the Stake ReceiptNFTs owned by a specific user
     * @param address address to query the receipts for
     * @returns array of receipts
     */
    async fetchPrelaunchReceipts(address?: string): Promise<preLaunchReceipt[]> {
        this.ensureUser();

        if (address === undefined) address = await this.signer!.getAddress();

        //default to stake receipts
        return await this.receiptClient.stakeReceipts(address);
    }

    async getOPPMessages(address?: string): Promise<OPPAssertion[]> {
        this.ensureUser();

        if (!address) address = await this.signer!.getAddress();

        return await this.oppClient.getMessages(address);
    }

    // Ensure that signer wallet is available for write operations
    private ensureUser() {
        if (!this.signer) {
            throw new Error(
                'EthereumStakingClient: write operation requires a wallet-connected Web3 provider',
            );
        }
    }


    // ---------------------------------------------------------------------
    // READ-ONLY Public Methods
    // ---------------------------------------------------------------------

    // Estimated total APY for staking yeild
    getSystemAPY(): Promise<number> {
        // TODO
        return Promise.resolve(0);
    }

    // Protocol fee charged for deposit from Native to LIQ
    getDepositFee(amount: bigint): Promise<bigint> {
        // TODO
        return Promise.resolve(BigInt(0));
    }

    async getOPPStatus(): Promise<any> {
        return await this.oppClient.getStatus();
    }

    async getEthStats(): Promise<any> {
        let withdrawDelay = await this.contract.DepositManager.withdrawDelay();
        let minDeposit = await this.contract.DepositManager.minDeposit();
        let rewardCooldown = await this.contract.DepositManager.rewardCooldown();

        return {
            withdrawDelay,
            minDeposit,
            rewardCooldown,
        }
    }

    /**
     * Program-level prelaunch WIRE / tranche snapshot for Ethereum
     * 
     * SUPPORTS READ-ONLY ACcESS
     */
    async getTrancheSnapshot(options?: {
        chainID?: ChainID;
        windowBefore?: number;
        windowAfter?: number;
    }): Promise<TrancheSnapshot> {
        const {
            chainID = EvmChainID.Hoodi,
            windowBefore,
            windowAfter,
        } = options ?? {};


        const blockNumber = await this.provider.getBlockNumber();
        const blockTag = { blockTag: blockNumber };

        // Fetch all required contract data
        const [totalSharesBn, indexBn, trancheNumberBn, trancheSupplyBn, tranchePriceWadBn, totalSupplyBn, supplyGrowthBps, priceGrowthBps, minPriceUsd, maxPriceUsd] = await Promise.all([
            this.contract.Depositor.totalShares(blockTag),
            this.contract.Depositor.index(blockTag),
            this.contract.Pretoken.trancheNumber(blockTag),
            this.contract.Pretoken.trancheSupply(blockTag),
            this.contract.Pretoken.tranchePriceUsd(blockTag),
            this.contract.Pretoken.totalSupply(blockTag),
            this.contract.Pretoken.supplyGrowthBps(blockTag),
            this.contract.Pretoken.priceGrowthBps(blockTag),
            this.contract.EthUsdPriceConsumer.MIN_PRICE(),
            this.contract.EthUsdPriceConsumer.MAX_PRICE(),
        ]);


        const totalTrancheSupply = BigInt(totalSupplyBn.toString()) / BigInt(1e10);
        const currentTrancheSupply = BigInt(trancheSupplyBn.toString()) / BigInt(1e10);


        // fetch price and timestamp from aggregator
        const [roundId, answer, startedAt, updatedAt, answeredInRound] = await this.contract.Aggregator.latestRoundData();
        let ethPriceUsd: bigint = BigInt(answer.toString());
        let nativePriceTimestamp: number = Number(updatedAt);

        // ! Placeholder from hoodi deployment - don't think this can be fetched dynamically
        const initialTrancheSupply = BigInt(60000) * BigInt(1e8);

        return buildEthereumTrancheSnapshot({
            chainID,
            totalSharesBn,
            indexBn,
            trancheNumberBn,
            currentTrancheSupply,
            tranchePriceWadBn,
            totalTrancheSupply,
            initialTrancheSupply,
            supplyGrowthBps,
            priceGrowthBps,
            minPriceUsd,
            maxPriceUsd,

            ethPriceUsd,
            nativePriceTimestamp,
            ladderWindowBefore: windowBefore,
            ladderWindowAfter: windowAfter,
        });
    }

    // ---------------------------------------------------------------------
    // Internal ETH Staking client helper functions
    // ---------------------------------------------------------------------

    // ! This is a temporary measure for Hoodi testnet because there is no aggregator deployed
    private async updateMockAggregatorPrice() {
        const aggregator = this.contract.Aggregator;

        // read latest round & compute age
        const [roundId, answer, startedAt, updatedAt, answeredInRound] = await aggregator.latestRoundData();
        const now = (await this.provider.getBlock("latest")).timestamp;
        const ageSec = Number(now) - Number(updatedAt);

        const ONE_HOUR = 1 * 3600;
        // const ONE_HOUR = 10;
        if (ageSec > ONE_HOUR) {
            // safety check - only run in non-production contexts
            const network = await this.provider.getNetwork();
            const chainId = network.chainId;
            const allowedTestChains = new Set([560048]);
            if (!allowedTestChains.has(chainId)) {
                console.warn(`MockAggregator is stale (${ageSec}s) but chainId ${chainId} is not a test/local network — skipping update.`);
                return;
            }


            //fetch the current ETH / USD price
            const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
            const data = await res.json();
            const ethUsd = data.ethereum.usd;

            const currentEthPrice = ethers.utils.parseUnits(ethUsd.toString(), 8);

            try {
                //update to be intentionally stale
                // const alttx = await aggregator.updateStale(currentEthPrice, now - 7200);
                // const altreceipt = await alttx.wait(1);
                // console.log('stale update receipt', altreceipt)

                //update answer with current timestamp
                const tx = await aggregator.updateAnswer(currentEthPrice);
                const txreceipt = await tx.wait(1);
                // console.log('MockAggregator answer updated - receipt:', txreceipt)
            } catch (err: any) {
                console.error('MockAggregator updateAnswer failed', err?.message || err);
            }
        } else {
            console.log(`MockAggregator updated ${ageSec}s ago — no update needed`);
        }
    }
}