import { Connection } from '@solana/web3.js';
import { BigNumber, ethers } from 'ethers';

import { EthereumStakingClient } from './networks/ethereum/ethereum';
import { formatContractErrors, resolveContractWriteOverrides } from './networks/ethereum/utils';
import { SolanaStakingClient } from './networks/solana/solana';

export type StakingTxChain = 'eth' | 'sol';
export type StakingTxState = 'pending' | 'confirmed' | 'failed' | 'not_found';

export interface StakingTransactionStatus {
    chain: StakingTxChain;
    txId: string;
    state: StakingTxState;
    timestampMs: number | null;
    blockNumber?: number;
    slot?: number;
    confirmations?: number | null;
    errorMessage?: string;
}

export interface EthereumSubmitStepResult {
    stepId: 'deposit' | 'approve' | 'source';
    label: string;
    txId: string;
    timestampMs: number | null;
}

export interface EthereumStakeSubmission {
    finalTxId: string;
    steps: EthereumSubmitStepResult[];
}

type EthereumLikeProvider =
    | ethers.providers.Web3Provider
    | ethers.providers.JsonRpcProvider;

export async function getEthereumTransactionStatus(
    source: EthereumStakingClient | EthereumLikeProvider,
    txId: string,
): Promise<StakingTransactionStatus> {
    const provider = isEthereumStakingClient(source)
        ? getEthereumProvider(source)
        : source;
    const tx = await provider.getTransaction(txId).catch(() => null);
    const receipt = await provider.getTransactionReceipt(txId).catch(() => null);

    if (!tx && !receipt) {
        return {
            chain: 'eth',
            txId,
            state: 'not_found',
            timestampMs: null,
        };
    }

    if (!receipt) {
        return {
            chain: 'eth',
            txId,
            state: 'pending',
            timestampMs: null,
            confirmations: tx?.confirmations ?? null,
        };
    }

    const block = receipt.blockNumber != null
        ? await provider.getBlock(receipt.blockNumber).catch(() => null)
        : null;

    return {
        chain: 'eth',
        txId,
        state: receipt.status === 0 ? 'failed' : 'confirmed',
        timestampMs: block?.timestamp ? block.timestamp * 1000 : null,
        blockNumber: receipt.blockNumber,
        confirmations: tx?.confirmations ?? null,
    };
}

export async function getSolanaTransactionStatus(
    source: SolanaStakingClient | Connection,
    txId: string,
): Promise<StakingTransactionStatus> {
    const connection = isSolanaStakingClient(source)
        ? source.connection
        : source;

    const statusResponse = await connection.getSignatureStatuses(
        [txId],
        { searchTransactionHistory: true },
    ).catch(() => ({ value: [null] }));
    const status = statusResponse.value[0];
    const transaction = await connection.getTransaction(txId, {
        commitment: 'confirmed',
        maxSupportedTransactionVersion: 0,
    }).catch(() => null);

    if (!status && !transaction) {
        return {
            chain: 'sol',
            txId,
            state: 'not_found',
            timestampMs: null,
        };
    }

    const errorMessage = status?.err ? JSON.stringify(status.err) : undefined;
    const isConfirmed = !!transaction || status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized';

    return {
        chain: 'sol',
        txId,
        state: status?.err ? 'failed' : isConfirmed ? 'confirmed' : 'pending',
        timestampMs: transaction?.blockTime ? transaction.blockTime * 1000 : null,
        slot: transaction?.slot ?? status?.slot,
        confirmations: status?.confirmations ?? null,
        errorMessage,
    };
}

export async function getStakingClientTransactionStatus(
    client: EthereumStakingClient | SolanaStakingClient,
    txId: string,
): Promise<StakingTransactionStatus> {
    if (isEthereumStakingClient(client)) {
        return getEthereumTransactionStatus(client, txId);
    }
    return getSolanaTransactionStatus(client, txId);
}

export async function submitEthereumStakeToWireFlow(
    client: EthereumStakingClient,
    amount: bigint | string | number | BigNumber,
    wireAccount: string,
): Promise<EthereumStakeSubmission> {
    ensureEthereumUser(client);

    const amountWei = BigNumber.isBigNumber(amount)
        ? amount
        : BigNumber.from(amount);
    const signerAddress = await client.address;
    if (!signerAddress) {
        throw new Error('Ethereum signer address is unavailable.');
    }

    const contractService = getEthereumContractService(client);
    const depositor = client.contract.Depositor.address;
    const liqRead = client.contract.LiqEthToken;
    const liqWrite = contractService.getWrite('LiqEthToken');
    const steps: EthereumSubmitStepResult[] = [];

    const [balance, allowance, paused] = await Promise.all([
        liqRead.balanceOf(signerAddress),
        liqRead.allowance(signerAddress, depositor),
        client.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 (balance.lt(amountWei)) {
        throw new Error('StakeClient.performStakeToWire: Insufficient LiqETH balance');
    }

    if (allowance.lt(amountWei)) {
        const approveTx = await liqWrite.approve(depositor, amountWei);
        const approveReceipt = await approveTx.wait(1);
        const updatedAllowance = await liqRead.allowance(signerAddress, depositor);
        if (updatedAllowance.lt(amountWei)) {
            throw new Error('StakeClient.performStakeToWire: Liq allowance approval failed or allowance still insufficient after approve');
        }
        steps.push({
            stepId: 'approve',
            label: 'Approve LIQETH spend',
            txId: approveTx.hash,
            timestampMs: approveReceipt?.blockNumber != null
                ? await getEthereumBlockTimestampMs(getEthereumProvider(client), approveReceipt.blockNumber)
                : null,
        });
    }

    try {
        await client.contract.Depositor.callStatic.stakeLiqETHToWire(amountWei, wireAccount);
    } catch (error: any) {
        const formatted = formatContractErrors(error);
        throw new Error(`StakeClient.performStakeToWire: ${formatted.name ?? formatted.raw}`);
    }

    const txOverrides = await resolveContractWriteOverrides(
        client.contract.Depositor,
        'stakeLiqETHToWire',
        [amountWei, wireAccount],
    );
    const stakeTx = await client.contract.Depositor.stakeLiqETHToWire(
        amountWei,
        wireAccount,
        txOverrides,
    );
    const stakeReceipt = await stakeTx.wait(1);
    steps.push({
        stepId: 'source',
        label: 'Stake LIQETH to Wire',
        txId: stakeTx.hash,
        timestampMs: stakeReceipt?.blockNumber != null
            ? await getEthereumBlockTimestampMs(getEthereumProvider(client), stakeReceipt.blockNumber)
            : null,
    });

    return {
        finalTxId: stakeTx.hash,
        steps,
    };
}

export async function submitEthereumDepositAndStakeToWireFlow(
    client: EthereumStakingClient,
    amount: bigint | string | number | BigNumber,
    wireAccount: string,
): Promise<EthereumStakeSubmission> {
    ensureEthereumUser(client);

    const amountWei = BigNumber.isBigNumber(amount)
        ? amount
        : BigNumber.from(amount);
    if (amountWei.lte(0)) {
        throw new Error('Amount must be greater than zero.');
    }

    const convertClient = getEthereumConvertClient(client);
    const depositResult = await convertClient.performDeposit(amountWei);
    const steps: EthereumSubmitStepResult[] = [{
        stepId: 'deposit',
        label: 'Deposit native ETH',
        txId: depositResult.txHash,
        timestampMs: depositResult.receipt?.blockNumber != null
            ? await getEthereumBlockTimestampMs(getEthereumProvider(client), depositResult.receipt.blockNumber)
            : null,
    }];

    const stake = await submitEthereumStakeToWireFlow(client, amountWei, wireAccount);
    return {
        finalTxId: stake.finalTxId,
        steps: [...steps, ...stake.steps],
    };
}

function isEthereumStakingClient(value: unknown): value is EthereumStakingClient {
    return value instanceof EthereumStakingClient;
}

function isSolanaStakingClient(value: unknown): value is SolanaStakingClient {
    return value instanceof SolanaStakingClient;
}

function getEthereumProvider(client: EthereumStakingClient): EthereumLikeProvider {
    const provider = (client as unknown as { provider?: EthereumLikeProvider }).provider;
    if (!provider) {
        throw new Error('Ethereum provider is unavailable.');
    }
    return provider;
}

function getEthereumContractService(client: EthereumStakingClient): {
    getWrite: (name: string) => any;
} {
    const contractService = (client as unknown as { contractService?: { getWrite: (name: string) => any } }).contractService;
    if (!contractService) {
        throw new Error('Ethereum contract service is unavailable.');
    }
    return contractService;
}

function getEthereumConvertClient(client: EthereumStakingClient): {
    performDeposit: (amountWei: BigNumber) => Promise<{
        txHash: string;
        receipt?: { blockNumber?: number };
    }>;
} {
    const convertClient = (client as unknown as { convertClient?: any }).convertClient;
    if (!convertClient?.performDeposit) {
        throw new Error('Ethereum convert client is unavailable.');
    }
    return convertClient;
}

function ensureEthereumUser(client: EthereumStakingClient): void {
    const ensureUser = (client as unknown as { ensureUser?: () => void }).ensureUser;
    ensureUser?.call(client);
}

async function getEthereumBlockTimestampMs(
    provider: EthereumLikeProvider,
    blockNumber: number,
): Promise<number | null> {
    const block = await provider.getBlock(blockNumber).catch(() => null);
    return block?.timestamp ? block.timestamp * 1000 : null;
}
