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 const INITIAL_TRANCHE_SUPPLY = 35000;

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
        // const network = await this.provider.getNetwork();
        // const chainId = network.chainId;
        // const allowedTestChains = new Set([560048]);
        // if (allowedTestChains.has(chainId)) 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 | null> {
        if (!this.signer) return Promise.resolve(null);

        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.LiqEthToken.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));
        }
        let stakeSharesBN = BigNumber.from(0);
        for (let r of stakeReceipts) {
            stakeSharesBN = stakeSharesBN.add(BigNumber.from(r.receipt.shares.amount));
        }

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


        // 5) Calculate staking yield 
        let currentIndex = BigInt(0);
        let totalShares = BigInt(0);
        let userShares = BigInt(0);
        const indexScale = BigInt(1e27);
        try {
            // These may throw if not implemented on contract
            const [indexBn, totalSharesBn] = await Promise.all([
                this.contract.Depositor.index().catch(() => BigNumber.from(0)),
                this.contract.Depositor.totalShares().catch(() => BigNumber.from(0)),
            ]);

            const userSharesBn = stakeSharesBN;
            currentIndex = BigInt(indexBn.toString());
            totalShares = BigInt(totalSharesBn.toString());
            userShares = BigInt(userSharesBn.toString());
        } catch { }

        // sharesToTokens(userShares, currentIndex) = userShares * currentIndex / indexScale
        let estimatedClaim = BigInt(0);
        let estimatedYield = BigInt(0);

        // started work on estimating the user's personal APY - not necessary at the moment
        // let estimatedAPY: number | null = null;
        // if (userShares > BigInt(0) && currentIndex > BigInt(0)) {
        //     estimatedClaim = (userShares * currentIndex) / indexScale;
        //     if (estimatedClaim > stakeBalanceBN.toBigInt()) {
        //         estimatedYield = estimatedClaim - stakeBalanceBN.toBigInt();
        //     }

        //     estimatedAPY = null;
        // }

        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',
            },

            yield: {
                currentIndex,
                indexScale,
                totalShares,
                userShares,
                estimatedClaim,
                estimatedYield,
            },
            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
    // ---------------------------------------------------------------------

    // Protocol-wide ETH staking APY in percent, e.g. 3.0 => "3.00%"
    async getSystemAPY(): Promise<number> {
        // NOTE: despite the name, this value is effectively *annual* BPS on-chain.
        // e.g. 300 => 3% APY
        const annualBpsBn = await this.contract.DepositManager.dailyRateBPS();
        const annualBps = annualBpsBn.toNumber(); // e.g. 300 for 3%

        // Convert basis points (1/100 of 1%) to percent:
        // 10,000 bps = 100% ⇒ 100 bps = 1% ⇒ divide by 100.
        const apyPercent = annualBps / 100;

        return apyPercent; // 3 => "3.00%"
    }

    // Protocol fee charged for deposit from Native to LIQ
    async getDepositFee(amountWei: bigint): Promise<bigint> {
        const feeBn: BigNumber = await this.contract.DepositManager.procFee(amountWei);
        return BigInt(feeBn.toString());
    }

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

    async getEthStats(): Promise<any> {
        let withdrawDelay = await this.contract.WithdrawalQueue.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.Ethereum,
            windowBefore,
            windowAfter,
        } = options ?? {};


        try {

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


            // Fetch all required contract data
            const [totalSharesBn, indexBn, trancheNumberBn, trancheSupplyBn, tranchePriceUsdBn, totalSupplyBn, supplyGrowthBps, priceGrowthCents, 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.priceIncrementUsd(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.MockAggregator.latestRoundData();
            //originally fetched ethPrice from aggregator - pulling from EthUsdPriceConsumer instead - this is where the actual price gets pulled from when purchasing pretokens.
            // let ethPriceUsd: bigint = BigInt(answer.toString());)

            let ethPriceUsdBn = await this.contract.EthUsdPriceConsumer.getPrice18Decimals();
            let ethPriceUsd: bigint = BigInt(ethPriceUsdBn.toString()) / BigInt(1e10);

            // let nativePriceTimestamp: number = Number(updatedAt);

            const initialTrancheSupply = BigInt(INITIAL_TRANCHE_SUPPLY) * BigInt(1e8);

            return buildEthereumTrancheSnapshot({
                chainID,
                totalSharesBn,
                indexBn,
                trancheNumberBn,
                currentTrancheSupply,
                tranchePriceUsdBn,
                totalTrancheSupply,
                initialTrancheSupply,
                supplyGrowthBps,
                priceGrowthCents,
                minPriceUsd,
                maxPriceUsd,

                ethPriceUsd,
                // nativePriceTimestamp,
                ladderWindowBefore: windowBefore,
                ladderWindowAfter: windowAfter,
            });
        }
        catch (err: any) {
            throw new Error(`Error fetching Ethereum tranche snapshot: ${err?.message || err}`);
        }
    }

    /**
     * Estimate a conservative native ETH buffer (in wei) to leave in the wallet
     * so the user can pay gas for the current deposit and at least one more tx.
     *
     * Typical usage in UI:
     *   const buffer = await client.estimateGasBuffer();
     *   const maxSpendable = balanceWei > buffer ? balanceWei - buffer : 0n;
     *
     * @param options.txCount         How many transactions to cover (default 2: deposit + 1 more)
     * @param options.safetyMultiplier Additional safety multiplier on top of txCount (default 1.5x)
     * @param options.minBufferWei    Optional override minimum buffer (defaults ~0.002 ETH)
     */
    async getDepositBuffer(options?: {
        txCount?: number;
        safetyMultiplier?: number;
        minBufferWei?: bigint;
        balanceOverrideLamports?: bigint; 
    }): Promise<bigint> {
        this.ensureUser();

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

        // 1) Estimate a baseline gas usage using a simple self-transfer.
        //    This is cheap and doesn't depend on your contract ABI at all.
        const baseGas = await this.provider.estimateGas({
            from: walletAddress,
            to: walletAddress,
            value: ethers.constants.Zero,
        });

        // 2) Fetch current gas price / max fee per gas.
        const feeData = await this.provider.getFeeData();
        let gasPrice =
            feeData.maxFeePerGas ??
            feeData.gasPrice ??
            ethers.utils.parseUnits('20', 'gwei'); // conservative fallback

        // 3) How many txs do we want to cover?
        //    Default: 2 (deposit + one extra action such as stake or small follow-up).
        const txCount = options?.txCount ?? 2;

        // We also assume that contract interactions are more expensive than a simple transfer.
        // Use a multiplier (e.g., 5x) on baseGas to approximate a more complex tx.
        const COMPLEX_TX_MULTIPLIER = 5; // tuning knob
        const totalGasUnits = baseGas
            .mul(COMPLEX_TX_MULTIPLIER)
            .mul(txCount);

        const baseCost = totalGasUnits.mul(gasPrice);

        // 4) Safety multiplier on top of that (e.g. 1.5x).
        const safetyMultiplier = options?.safetyMultiplier ?? 1.5;
        const safetyScaled = Math.round(safetyMultiplier * 100); // e.g. 150

        const bufferedCost = baseCost
            .mul(safetyScaled)
            .div(100); // apply safety factor

        let bufferWei = bufferedCost.toBigInt();

        // 5) Enforce a minimum floor (e.g. ~0.002 ETH).
        const defaultMinBufferWei = BigInt(2_000_000_000_000_000); // 0.002 ETH
        const minBufferWei = options?.minBufferWei ?? defaultMinBufferWei;

        if (bufferWei < minBufferWei) {
            bufferWei = minBufferWei;
        }

        return bufferWei;
    }

    // ---------------------------------------------------------------------
    // 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.MockAggregator;

    //     // 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`);
    //     }
    // }
}