import { BigNumber, ethers } from 'ethers';

import { EthereumContractService } from '../contract';
import { resolveContractWriteOverrides, sendOPPFinalize } from '../utils';
import HoodiLiqEthBridgeArtifact from '../../../assets/ethereum/hoodi/outpost/LiqEthBridge.sol/LiqEthBridge.json';

export type EthereumInstaswapSubmitParams = {
    amountWei: ethers.BigNumberish;
    destinationChain: number | string | bigint;
    destinationAddress: Uint8Array;
    minOutWei: ethers.BigNumberish;
};

export type EthereumInstaswapSubmitHooks = {
    onSubmitted?: (txHash: string) => void | Promise<void>;
};

export type EthereumEnsureLiqEthBalanceParams = {
    targetAmountWei: ethers.BigNumberish;
    bufferBps?: number;
    extraNativeWei?: ethers.BigNumberish;
    maxAttempts?: number;
};

export type EthereumDepositToWireParams = {
    quantityWei: ethers.BigNumberish;
    recipient: string;
    topUpBufferBps?: number;
};

export type EthereumInstaswapOutpostAddresses = {
    liqEthToken: string;
    depositManager: string;
    depositor: string;
    opp: string;
};

export type EthereumInstaswapConnectedBalances = {
    nativeWei: string;
    liqEthWei: string;
};

type ParsedContractError = {
    name: string;
    signature?: string;
    args?: unknown[];
};

type EnsureLiqEthBalanceResult = {
    balance: BigNumber;
    lastTxHash: string;
};

type EthereumBridgeConfig = {
    oppAddress: string;
    liqEthBridgeAddress: string;
};

const DEFAULT_BUFFER_BPS = 100;
const DEFAULT_MAX_TOP_UP_ATTEMPTS = 3;
const LOCAL_ETH_BRIDGE_BY_CHAIN: Partial<Record<number, EthereumBridgeConfig>> = {
    [31337]: {
        oppAddress: '0x7969c5eD335650692Bc04293B07F5BF2e7A673C0',
        liqEthBridgeAddress: '0x162A433068F51e18b7d13932F27e66a3f99E6890',
    },
};

export class EthereumInstaswapClient {
    constructor(
        private readonly contractService: EthereumContractService,
        private readonly signer?: ethers.Signer,
        private readonly chainId?: number,
        private readonly rpcUrl?: string,
    ) { }

    async submitCrossChain(
        params: EthereumInstaswapSubmitParams,
        hooks?: EthereumInstaswapSubmitHooks,
    ): Promise<string> {
        const signer = this.requireSigner();
        const owner = await signer.getAddress();
        const amountIn = BigNumber.from(params.amountWei);
        const minOut = BigNumber.from(params.minOutWei);
        const destinationChain = BigNumber.from(params.destinationChain.toString());
        const destinationAddress = Uint8Array.from(params.destinationAddress);

        const liqEthRead = this.contractService.getReadOnly('LiqEthToken');
        const liqEthWrite = this.contractService.getWrite('LiqEthToken');
        const depositorWrite = this.contractService.getWrite('Depositor');

        const currentBalance = await liqEthRead['balanceOf'](owner);
        if (currentBalance.lt(amountIn)) {
            throw new Error('Insufficient LIQETH balance for the outpost swap.');
        }

        const depositorAddress = this.contractService.getAddress('Depositor');
        const allowance = await liqEthRead['allowance'](owner, depositorAddress);
        if (allowance.lt(amountIn)) {
            const approvalTx = await liqEthWrite['approve'](depositorAddress, amountIn);
            const approvalReceipt = await approvalTx.wait();
            if (!approvalReceipt || approvalReceipt.status === 0) {
                throw new Error(`Ethereum approval transaction ${approvalTx.hash} reverted.`);
            }
        }

        await this.runPreflight(
            depositorWrite,
            owner,
            amountIn,
            destinationChain,
            destinationAddress,
            minOut,
        );

        const txOverrides = await resolveContractWriteOverrides(
            depositorWrite,
            'instaswapCrossChain',
            [amountIn, destinationChain, destinationAddress, minOut],
        );

        const swapTx = await depositorWrite['instaswapCrossChain'](
            amountIn,
            destinationChain,
            destinationAddress,
            minOut,
            txOverrides,
        );
        await hooks?.onSubmitted?.(swapTx.hash);

        const receipt = await swapTx.wait();
        if (!receipt || receipt.status === 0) {
            throw new Error(`Ethereum transaction ${swapTx.hash} reverted.`);
        }

        return swapTx.hash;
    }

    async ensureLiqEthBalance(params: EthereumEnsureLiqEthBalanceParams): Promise<EnsureLiqEthBalanceResult> {
        const signer = this.requireSigner();
        const owner = await signer.getAddress();
        const targetAmount = BigNumber.from(params.targetAmountWei);
        const bufferBps = params.bufferBps ?? 0;
        const extraNativeWei = params.extraNativeWei == null
            ? ethers.constants.Zero
            : BigNumber.from(params.extraNativeWei);
        const maxAttempts = params.maxAttempts ?? DEFAULT_MAX_TOP_UP_ATTEMPTS;
        let lastTxHash = '';

        let currentBalance = await this.getLiqEthBalance(owner);
        for (let attempt = 0; attempt < maxAttempts && currentBalance.lt(targetAmount); attempt += 1) {
            const shortfall = targetAmount.sub(currentBalance);
            const buffered = shortfall.mul(10_000 + bufferBps).div(10_000);
            const topUpValue = buffered.add(extraNativeWei);
            lastTxHash = await this.depositNativeForLiqEth(topUpValue);
            currentBalance = await this.getLiqEthBalance(owner);
        }

        if (currentBalance.lt(targetAmount)) {
            throw new Error('Insufficient LIQETH balance after topping up from native ETH.');
        }

        return {
            balance: currentBalance,
            lastTxHash,
        };
    }

    async depositToWire(
        params: EthereumDepositToWireParams,
        hooks?: EthereumInstaswapSubmitHooks,
    ): Promise<string> {
        const signer = this.requireSigner();
        const quantity = BigNumber.from(params.quantityWei);
        const recipient = params.recipient.trim();
        if (!recipient) {
            throw new Error('Wire recipient is required.');
        }

        await this.ensureLiqEthBalance({
            targetAmountWei: quantity,
            bufferBps: params.topUpBufferBps ?? DEFAULT_BUFFER_BPS,
        });

        const owner = await signer.getAddress();
        const liqEthRead = this.contractService.getReadOnly('LiqEthToken');
        const bridge = this.getLiqEthBridgeContract();
        const bridgeAddress = bridge.address;

        const allowance = await liqEthRead['allowance'](owner, bridgeAddress);
        if (allowance.lt(quantity)) {
            const approvalTx = await this.contractService.getWrite('LiqEthToken')['approve'](
                bridgeAddress,
                quantity,
            );
            const approvalReceipt = await approvalTx.wait();
            if (!approvalReceipt || approvalReceipt.status === 0) {
                throw new Error(`Ethereum approval transaction ${approvalTx.hash} reverted.`);
            }
        }

        const depositTx = await bridge['depositToWire'](quantity, recipient);
        await hooks?.onSubmitted?.(depositTx.hash);
        const depositReceipt = await depositTx.wait();
        if (!depositReceipt || depositReceipt.status === 0) {
            throw new Error(`Ethereum transaction ${depositTx.hash} reverted.`);
        }

        await this.reconcileOppEpochAtAddress(this.requireBridgeConfig().oppAddress);
        return depositTx.hash;
    }

    getOutpostAddresses(): EthereumInstaswapOutpostAddresses {
        return {
            liqEthToken: this.contractService.getAddress('LiqEthToken'),
            depositManager: this.contractService.getAddress('DepositManager'),
            depositor: this.contractService.getAddress('Depositor'),
            opp: this.contractService.getAddress('OPP'),
        };
    }

    async getConnectedBalances(address?: string): Promise<EthereumInstaswapConnectedBalances> {
        const owner = await this.resolveAddress(address);
        const provider = this.getLocalRpcProvider();
        const liqEthRead = new ethers.Contract(
            this.contractService.getAddress('LiqEthToken'),
            this.contractService.getInterface('LiqEthToken'),
            provider,
        );

        const [nativeBalance, liqEthBalance] = await Promise.all([
            provider.getBalance(owner),
            liqEthRead['balanceOf'](owner),
        ]);

        return {
            nativeWei: nativeBalance.toString(),
            liqEthWei: BigNumber.from(liqEthBalance).toString(),
        };
    }

    private async runPreflight(
        depositorWrite: ethers.Contract,
        owner: string,
        amountIn: BigNumber,
        destinationChain: BigNumber,
        destinationAddress: Uint8Array,
        minOut: BigNumber,
    ): Promise<void> {
        try {
            await depositorWrite.callStatic['instaswapCrossChain'](
                amountIn,
                destinationChain,
                destinationAddress,
                minOut,
                { from: owner },
            );
            return;
        } catch (error) {
            const decoded = this.decodeContractError(depositorWrite, error);
            if (!decoded?.name || !this.isRecoverableOppError(decoded.name)) {
                throw error;
            }
        }

        await this.reconcileOppEpochAtAddress(this.contractService.getAddress('OPP'));
        await depositorWrite.callStatic['instaswapCrossChain'](
            amountIn,
            destinationChain,
            destinationAddress,
            minOut,
            { from: owner },
        );
    }

    private async reconcileOppEpochAtAddress(oppAddress: string): Promise<void> {
        const oppRead = new ethers.Contract(
            oppAddress,
            this.contractService.getInterface('OPP'),
            this.getLocalRpcProvider(),
        );
        const oppWrite = new ethers.Contract(oppAddress, this.contractService.getInterface('OPP'), this.requireSigner());

        if (this.supportsFunction(oppWrite, 'testAdvanceEpochClock')) {
            const epochSecs = await oppRead['MESSAGE_EPOCH_SECS']();
            const advanceTx = await oppWrite['testAdvanceEpochClock'](
                BigNumber.from(epochSecs).add(1),
            );
            const advanceReceipt = await advanceTx.wait();
            if (!advanceReceipt || advanceReceipt.status === 0) {
                throw new Error(`Ethereum transaction ${advanceTx.hash} reverted.`);
            }
        }

        await sendOPPFinalize(oppWrite);
    }

    private async depositNativeForLiqEth(amountWei: ethers.BigNumberish): Promise<string> {
        const tx = await this.contractService.getWrite('DepositManager')['deposit']({
            value: BigNumber.from(amountWei),
        });
        const receipt = await tx.wait();
        if (!receipt || receipt.status === 0) {
            throw new Error(`Ethereum transaction ${tx.hash} reverted.`);
        }
        return tx.hash;
    }

    private async getLiqEthBalance(address: string): Promise<BigNumber> {
        return this.contractService.getReadOnly('LiqEthToken')['balanceOf'](address);
    }

    private async resolveAddress(address?: string): Promise<string> {
        if (address?.trim()) {
            return address.trim();
        }
        return this.requireSigner().getAddress();
    }

    private requireSigner(): ethers.Signer {
        if (!this.signer) {
            throw new Error('Connect an Ethereum wallet first.');
        }
        return this.signer;
    }

    private getLocalRpcProvider(): ethers.providers.JsonRpcProvider {
        if (!this.rpcUrl) {
            throw new Error('Ethereum RPC URL is not configured.');
        }
        return new ethers.providers.JsonRpcProvider(this.rpcUrl);
    }

    private getLiqEthBridgeContract(): ethers.Contract {
        const { liqEthBridgeAddress } = this.requireBridgeConfig();
        return new ethers.Contract(
            liqEthBridgeAddress,
            HoodiLiqEthBridgeArtifact.abi,
            this.requireSigner(),
        );
    }

    private requireBridgeConfig(): EthereumBridgeConfig {
        const chainId = this.chainId ?? 0;
        const config = LOCAL_ETH_BRIDGE_BY_CHAIN[chainId];
        if (!config) {
            throw new Error(`No Ethereum bridge config is registered for chainId=${chainId}.`);
        }
        return config;
    }

    private supportsFunction(contract: ethers.Contract, name: string): boolean {
        try {
            contract.interface.getFunction(name);
            return true;
        } catch {
            return false;
        }
    }

    private decodeContractError(
        contract: ethers.Contract,
        error: unknown,
    ): ParsedContractError | null {
        if (error && typeof error === 'object') {
            const record = error as Record<string, unknown>;
            const directName = record['errorName'];
            const directArgs = record['errorArgs'];
            const directSignature = record['errorSignature'];
            if (typeof directName === 'string' && directName) {
                return {
                    name: directName,
                    signature: typeof directSignature === 'string' ? directSignature : undefined,
                    args: Array.isArray(directArgs) ? directArgs : undefined,
                };
            }
        }

        const revertData = this.extractRevertData(error);
        if (!revertData) {
            return null;
        }

        try {
            const parsed = contract.interface.parseError(revertData);
            return {
                name: parsed.name,
                signature: parsed.signature,
                args: Array.from(parsed.args ?? []),
            };
        } catch {
            try {
                const parsed = this.contractService.parseError(revertData);
                return {
                    name: parsed.name,
                    signature: parsed.signature,
                    args: Array.from(parsed.args ?? []),
                };
            } catch {
                return null;
            }
        }
    }

    private extractRevertData(error: unknown): string | null {
        if (!error || typeof error !== 'object') {
            return null;
        }

        const record = error as Record<string, unknown>;
        const candidates = [
            record['data'],
            (record['error'] as Record<string, unknown> | undefined)?.['data'],
            (record['error'] as Record<string, unknown> | undefined)?.['error'],
            (record['receipt'] as Record<string, unknown> | undefined)?.['revertReason'],
        ];

        for (const candidate of candidates) {
            if (typeof candidate === 'string' && candidate.startsWith('0x')) {
                return candidate;
            }
            if (candidate && typeof candidate === 'object') {
                const nested = (candidate as Record<string, unknown>)['data'];
                if (typeof nested === 'string' && nested.startsWith('0x')) {
                    return nested;
                }
            }
        }

        return null;
    }

    private isRecoverableOppError(name: string): boolean {
        return [
            'OPP_PreviousEpochUnsent',
            'OPP_PreviousEpochPendingMessages',
            'OPP_PreviousEpochSent',
        ].includes(name);
    }
}
