import { BigNumber, ethers } from "ethers";
import {
    StakedEvent,
    WithdrawnStakeEvent,
    WithdrawnStakeResult,
    DepositAndStakeResult,
    DepositAndPurchaseResult,
    UnstakeAndWithdrawResult,
} from "../types";
import { EthereumContractService } from "../contract";
import { formatContractErrors, resolveContractWriteOverrides } from "../utils";

export class StakeClient {

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

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

    /**
     * 
     * @param amountWei an amount of liqETH (in WEI) to stake to the Outpost
     * @returns txHash (hash of the transaction), receipt, Staked event
     */
    async performStake(amountWei: BigNumber, signerAddress: string): Promise<any> {
        const depositor = this.contract.Depositor.address;
        const liqRead = this.contract.LiqEthToken;
        const bal = await liqRead.balanceOf(signerAddress);
        const allowance = await liqRead.allowance(signerAddress, depositor);

        const paused = await this.contract.Depositor.paused();
        if(paused) {
            throw new Error("Error - Depositor is in a paused state");
        }

        if (bal.lt(amountWei)) throw new Error("Insufficient LiqETH balance");

        // if allowance is less than the requested stake amount, request permission to spend LiqEth
        if (allowance.lt(amountWei)) {
            const liqWrite = this.contractService.getWrite('LiqEthToken');

            console.warn(`allowance insufficient (${allowance.toString()} < ${amountWei.toString()}); sending approve(${depositor}, ${amountWei.toString()})`);
            const approveTx = await liqWrite.approve(depositor, amountWei);
            await approveTx.wait(1);

            // re-read allowance to ensure approval succeeded
            const newAllowance = await liqRead.allowance(signerAddress, depositor);
            if (newAllowance.lt(amountWei)) {
                throw new Error('Approval failed or allowance still insufficient after approve');
            }
        }
        
        // simulate stake via callStatic to catch any errors before sending the transaction and spending gas
        try {
            await this.contract.Depositor.callStatic.stakeLiqETH(amountWei);
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            throw new Error(errorObj.name ?? errorObj.raw)
        }

        // send the tx to stake liqeth
        const txOverrides = await resolveContractWriteOverrides(
            this.contract.Depositor,
            'stakeLiqETH',
            [amountWei],
        );
        const tx = await this.contract.Depositor.stakeLiqETH(amountWei, txOverrides);

        // Wait for 1 confirmation 
        const receipt = await tx.wait(1);

        // Parse Staked event if present
        let staked: StakedEvent | undefined;
        const ev = receipt.events?.find((e) => e.event === 'Staked');

        if (ev && ev.args) {
            const { user, amountLiq, shares, tokenId } = ev.args;
            staked = {
                user,
                amountLiq: BigNumber.from(amountLiq),
                shares: BigNumber.from(shares),
                tokenId: BigNumber.from(tokenId)
            };
        }

        return {
            txHash: tx.hash,
            receipt,
            staked,
        };
    }


    /**
     * 
     * @param amountWei an amount of liqETH (in WEI) to stake to the Outpost
     * @param wireAccount Wire chain recipient account name. Required.
     * @returns txHash (hash of the transaction), receipt, Staked event
     */
    async performStakeToWire(amountWei: BigNumber, signerAddress: string, wireAccount?: string): Promise<any> {
        const depositor = this.contract.Depositor.address;
        const liqRead = this.contract.LiqEthToken;
        const bal = await liqRead.balanceOf(signerAddress);
        const allowance = await liqRead.allowance(signerAddress, depositor);

        const paused = await this.contract.Depositor.paused();
        if(paused) {
            throw new Error("StakeClient.performStakeToWire: Depositor is in a paused state");
        }

        if (!wireAccount?.trim()) {
            throw new Error('StakeClient.performStakeToWire: wireAccount is required');
        }

        if (bal.lt(amountWei)) throw new Error("StakeClient.performStakeToWire: Insufficient LiqETH balance");

        // if allowance is less than the requested stake amount, request permission to spend LiqEth
        if (allowance.lt(amountWei)) {
            const liqWrite = this.contractService.getWrite('LiqEthToken');

            const approveTx = await liqWrite.approve(depositor, amountWei);
            await approveTx.wait(1);

            // re-read allowance to ensure approval succeeded
            const newAllowance = await liqRead.allowance(signerAddress, depositor);
            if (newAllowance.lt(amountWei)) {
                throw new Error('StakeClient.performStakeToWire: Liq allowance approval failed or allowance still insufficient after approve');
            }
        }
        
        // simulate stake via callStatic to catch any errors before sending the transaction and spending gas
        try {
            await this.contract.Depositor.callStatic.stakeLiqETHToWire(amountWei, wireAccount);
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            throw new Error(`StakeClient.performStakeToWire: ${errorObj.name ?? errorObj.raw}`);
        }

        // send the tx to stake liqeth
        const txOverrides = await resolveContractWriteOverrides(
            this.contract.Depositor,
            'stakeLiqETHToWire',
            [amountWei, wireAccount],
        );
        const tx = await this.contract.Depositor.stakeLiqETHToWire(amountWei, wireAccount, txOverrides);

        // Wait for 1 confirmation 
        const receipt = await tx.wait(1);

        // Parse Staked event if present
        let staked: StakedEvent | undefined;
        const ev = receipt.events?.find((e) => e.event === 'Staked');

        if (ev && ev.args) {
            const { user, amountLiq, shares, tokenId } = ev.args;
            staked = {
                user,
                amountLiq: BigNumber.from(amountLiq),
                shares: BigNumber.from(shares),
                tokenId: BigNumber.from(tokenId)
            };
        }

        return {
            txHash: tx.hash,
            receipt,
            staked,
        };
    }


    /**
     * Burn the STAKE ReceiptNFT and send principal liqETH to `recipient` (Depositor.withdrawStake).
     * @param tokenId  ReceiptNFT token id for the stake position
     * @param recipient  Address that receives the liqETH from the pool
     * @returns txHash, receipt, and parsed WithdrawnStake event if present
     */
    async performWithdrawStake(tokenId: BigNumber, recipient: string): Promise<WithdrawnStakeResult> {
        // Send the payable tx
        const tx = await this.contract.Depositor.withdrawStake(tokenId, recipient);

        // Wait for 1 confirmation
        const receipt = await tx.wait(1);

        // Parse WithdrawnStake event if present
        let withdrawnstake: WithdrawnStakeEvent | undefined;
        const ev = receipt.events?.find((e) => e.event === 'WithdrawnStake');

        if (ev && ev.args) {
            const { user, tokenOut, tokenId } = ev.args;
            withdrawnstake = {
                user: user,
                tokenOut: BigNumber.from(tokenOut),
                tokenId: BigNumber.from(tokenId)
            };
        }

        return {
            txHash: tx.hash,
            receipt,
            withdrawnstake,
        };
    }

    /**
     * Deposit ETH to liqETH, then stake liqETH in a single transaction.
     * @param amountWei  Amount of ETH in wei to deposit and stake
     * @returns txHash (hash of the transaction), receipt, Staked event
     */
    async performDepositAndStake(amountWei: BigNumber): Promise<DepositAndStakeResult> {
        try {
            await this.contract.Depositor.callStatic.depositAndStake({ value: amountWei });
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            throw new Error(errorObj.name ?? errorObj.raw);
        }

        const txOverrides = await resolveContractWriteOverrides(
            this.contract.Depositor,
            'depositAndStake',
            [],
            { value: amountWei },
        );
        const tx = await this.contract.Depositor.depositAndStake(txOverrides);
        const receipt = await tx.wait(1);
        let staked: StakedEvent | undefined;
        const ev = receipt.events?.find((e: any) => e.event === 'Staked');
        if (ev && ev.args) {
            const { user, amountLiq, shares, tokenId } = ev.args;
            staked = {
                user,
                amountLiq: BigNumber.from(amountLiq),
                shares: BigNumber.from(shares),
                tokenId: BigNumber.from(tokenId),
            };
        }

        return { txHash: tx.hash, receipt, staked };
    }

    /**
     * Deposit ETH to liqETH, then stake liqETH in a single transaction.
     * @param amountWei  Amount of ETH in wei to deposit and stake
     * @param wireAccount Wire chain recipient account name. Required.
     * @returns txHash (hash of the transaction), receipt, Staked event
     */
    // ! CURRENTLY NOT IMPLEMENTED AS OF 4/14/26
    async performDepositAndStakeToWire(amountWei: BigNumber, wireAccount?: string): Promise<DepositAndStakeResult> {
        if (!wireAccount?.trim()) {
            throw new Error('StakeClient.performDepositAndStakeToWire: wireAccount is required');
        }

        try {
            await this.contract.Depositor.callStatic.depositAndStake({ value: amountWei });
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            throw new Error(errorObj.name ?? errorObj.raw);
        }

        const tx = await this.contract.Depositor.depositAndStake({ value: amountWei });
        const receipt = await tx.wait(1);
        let staked: StakedEvent | undefined;
        const ev = receipt.events?.find((e: any) => e.event === 'Staked');
        if (ev && ev.args) {
            const { user, amountLiq, shares, tokenId } = ev.args;
            staked = {
                user,
                amountLiq: BigNumber.from(amountLiq),
                shares: BigNumber.from(shares),
                tokenId: BigNumber.from(tokenId),
            };
        }

        return { txHash: tx.hash, receipt, staked };
    }

    /**
     * Deposit ETH to liqETH, then purchase pretokens in a single transaction.
     * @param amountWei  Amount of ETH in wei to deposit and use for pretoken purchase
     * @returns txHash (hash of the transaction), receipt
     */
    async performDepositAndPurchase(amountWei: BigNumber): Promise<DepositAndPurchaseResult> {
        try {
            await this.contract.Depositor.callStatic.depositAndPurchase({ value: amountWei });
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            throw new Error(errorObj.name ?? errorObj.raw);
        }

        const txOverrides = await resolveContractWriteOverrides(
            this.contract.Depositor,
            'depositAndPurchase',
            [],
            { value: amountWei },
        );
        const tx = await this.contract.Depositor.depositAndPurchase(txOverrides);
        const receipt = await tx.wait(1);
        return { txHash: tx.hash, receipt };
    }

    /**
     * Unstake liqETH, then request a withdrawal in a single transaction.
     * @param tokenId  The ReceiptNFT token ID for the stake position
     * @returns txHash (hash of the transaction), receipt
     */
    async performUnstakeAndRequestWithdraw(tokenId: BigNumber): Promise<UnstakeAndWithdrawResult> {
        try {
            await this.contract.Depositor.callStatic.unstakeAndRequestWithdraw(tokenId);
        } catch (err: any) {
            let errorObj = formatContractErrors(err);
            throw new Error(errorObj.name ?? errorObj.raw);
        }

        const txOverrides = await resolveContractWriteOverrides(
            this.contract.Depositor,
            'unstakeAndRequestWithdraw',
            [tokenId],
        );
        const tx = await this.contract.Depositor.unstakeAndRequestWithdraw(tokenId, txOverrides);
        const receipt = await tx.wait(1);
        return { txHash: tx.hash, receipt };
    }
}
