import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
import {
    Commitment,
    ComputeBudgetProgram,
    PublicKey,
    SystemProgram,
    TransactionSignature,
    TransactionInstruction,
} from '@solana/web3.js';
import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';

import { buildOutpostAccounts, OutpostAccounts } from '../utils';
import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';
import { ConvertClient } from './convert.client';

const commitment: Commitment = 'confirmed';
const DEFAULT_SOLANA_EPOCH_CAP = 32;

export type SolanaInstaswapSubmitParams = {
    amountLamports: bigint;
    destinationChain: number | string | bigint;
    destinationAddress: Uint8Array;
    minOutLamports: bigint;
};

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

export type SolanaDepositToWireParams = {
    quantityLamports: bigint;
    recipient: string;
};

export type SolanaInstaswapConnectedBalances = {
    nativeLamports: string;
    liqsolLamports: string;
    liqsolDecimals: number;
};

export function buildSolanaInstaswapCrossChainAccounts(params: {
    user: PublicKey;
    accounts: OutpostAccounts;
    liqsolCoreProgram: PublicKey;
    transferHookProgram: PublicKey;
    pendingAttestations: PublicKey;
}) {
    const {
        user,
        accounts,
        liqsolCoreProgram,
        transferHookProgram,
        pendingAttestations,
    } = params;

    return {
        user,
        globalState: accounts.globalState,
        liqsolMint: accounts.liqsolMint,
        distributionState: accounts.distributionState,
        userAta: accounts.userAta,
        senderUserRecord: accounts.userUserRecord,
        bridgeAuthority: accounts.bridgeAuthority,
        bridgeVaultAta: accounts.bridgeVaultAta,
        bridgeUserRecord: accounts.bridgeUserRecord,
        bridgeState: accounts.bridgeState,
        extraAccountMetaList: accounts.extraAccountMetaList,
        liqsolCoreProgram,
        transferHookProgram,
        bucketAuthority: accounts.bucketAuthority,
        bucketTokenAccount: accounts.bucketTokenAccount,
        bucketUserRecord: accounts.bucketUserRecord,
        tokenProgram: TOKEN_2022_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
        globalConfig: accounts.globalConfig,
        oppEpochState: accounts.oppEpochState,
        pendingAttestations,
    };
}

export class SolanaInstaswapClient {
    private readonly program: Program<LiqsolCoreClientIdl>;
    private readonly convertClient: ConvertClient;

    constructor(
        private readonly provider: AnchorProvider,
        private readonly pgs: SolanaProgramService,
        private readonly submitInstructions?: (
            ix: TransactionInstruction | TransactionInstruction[],
        ) => Promise<string>,
    ) {
        this.program = pgs.getProgram('liqsolCore');
        this.convertClient = new ConvertClient(provider, pgs);
    }

    async submitCrossChain(
        params: SolanaInstaswapSubmitParams,
        hooks?: SolanaInstaswapSubmitHooks,
    ): Promise<string> {
        const program = this.program;
        const user = this.provider.wallet.publicKey;
        if (!user) {
            throw new Error('Connect a Solana wallet first.');
        }

        const accounts = await buildOutpostAccounts(this.provider.connection, user, this.pgs);
        const amount = new BN(params.amountLamports.toString());
        const destinationChain = new BN(params.destinationChain.toString());
        const destinationAddress = Buffer.from(params.destinationAddress);
        const minOut = new BN(params.minOutLamports.toString());

        let pendingAttestations = accounts.pendingAttestations;
        let signature = '';

        try {
            signature = await this.runRpcWithRecovery(() =>
                program.methods
                    .instaswapCrossChain(
                        amount,
                        destinationChain,
                        destinationAddress,
                        minOut,
                    )
                    .accounts(
                        buildSolanaInstaswapCrossChainAccounts({
                            user,
                            accounts,
                            liqsolCoreProgram: this.program.programId,
                            transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                            pendingAttestations,
                        }),
                    )
                    .preInstructions([
                        ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 }),
                    ])
                    .rpc(),
            );
        } catch (error) {
            if (!this.isEpochFinalizationRequired(error)) {
                throw error;
            }

            await this.finalizeReadyEpoch(true);
            const retryEpochNumber = await this.pgs.fetchOppEpochNumber(this.provider.connection);
            pendingAttestations = this.pgs.derivePendingAttestationsPda(retryEpochNumber);

            signature = await this.runRpcWithRecovery(() =>
                program.methods
                    .instaswapCrossChain(
                        amount,
                        destinationChain,
                        destinationAddress,
                        minOut,
                    )
                    .accounts(
                        buildSolanaInstaswapCrossChainAccounts({
                            user,
                            accounts,
                            liqsolCoreProgram: this.program.programId,
                            transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                            pendingAttestations,
                        }),
                    )
                    .preInstructions([
                        ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 }),
                    ])
                    .rpc(),
            );
        }

        await hooks?.onSubmitted?.(signature);
        return signature;
    }

    async depositToWire(
        params: SolanaDepositToWireParams,
        hooks?: SolanaInstaswapSubmitHooks,
    ): Promise<string> {
        const program: any = this.program;
        const user = this.provider.wallet.publicKey;
        if (!user) {
            throw new Error('Connect a Solana wallet first.');
        }

        const recipient = params.recipient.trim();
        if (!recipient) {
            throw new Error('Wire recipient is required.');
        }

        await this.finalizeReadyEpoch();

        const accounts = await buildOutpostAccounts(this.provider.connection, user, this.pgs);
        const quantity = new BN(params.quantityLamports.toString());
        let pendingAttestations = accounts.pendingAttestations;

        let signature: string;
        try {
            signature = await this.runRpcWithRecovery(() =>
                program.methods
                    .bridgeLiqsolToWire(recipient, quantity)
                    .accounts({
                        user,
                        liqsolMint: accounts.liqsolMint,
                        distributionState: accounts.distributionState,
                        userAta: accounts.userAta,
                        senderUserRecord: accounts.userUserRecord,
                        bridgeAuthority: accounts.bridgeAuthority,
                        bridgeVaultAta: accounts.bridgeVaultAta,
                        bridgeUserRecord: accounts.bridgeUserRecord,
                        bridgeState: accounts.bridgeState,
                        extraAccountMetaList: accounts.extraAccountMetaList,
                        liqsolCoreProgram: program.programId,
                        transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                        bucketAuthority: accounts.bucketAuthority,
                        bucketTokenAccount: accounts.bucketTokenAccount,
                        tokenProgram: TOKEN_2022_PROGRAM_ID,
                        systemProgram: SystemProgram.programId,
                        globalConfig: accounts.globalConfig,
                        oppEpochState: accounts.oppEpochState,
                        pendingAttestations,
                    })
                    .preInstructions([
                        ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 }),
                    ])
                    .rpc(),
            );
        } catch (error) {
            if (!this.isEpochFinalizationRequired(error)) {
                throw error;
            }
            await this.finalizeReadyEpoch(true);
            const retryEpochNumber = await this.pgs.fetchOppEpochNumber(this.provider.connection);
            pendingAttestations = this.pgs.derivePendingAttestationsPda(retryEpochNumber);
            signature = await this.runRpcWithRecovery(() =>
                program.methods
                    .bridgeLiqsolToWire(recipient, quantity)
                    .accounts({
                        user,
                        liqsolMint: accounts.liqsolMint,
                        distributionState: accounts.distributionState,
                        userAta: accounts.userAta,
                        senderUserRecord: accounts.userUserRecord,
                        bridgeAuthority: accounts.bridgeAuthority,
                        bridgeVaultAta: accounts.bridgeVaultAta,
                        bridgeUserRecord: accounts.bridgeUserRecord,
                        bridgeState: accounts.bridgeState,
                        extraAccountMetaList: accounts.extraAccountMetaList,
                        liqsolCoreProgram: program.programId,
                        transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                        bucketAuthority: accounts.bucketAuthority,
                        bucketTokenAccount: accounts.bucketTokenAccount,
                        tokenProgram: TOKEN_2022_PROGRAM_ID,
                        systemProgram: SystemProgram.programId,
                        globalConfig: accounts.globalConfig,
                        oppEpochState: accounts.oppEpochState,
                        pendingAttestations,
                    })
                    .preInstructions([
                        ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 }),
                    ])
                    .rpc(),
            );
        }

        await this.finalizeReadyEpoch();
        await hooks?.onSubmitted?.(signature);
        return signature;
    }

    async getConnectedBalances(owner?: PublicKey | string): Promise<SolanaInstaswapConnectedBalances> {
        const resolvedOwner = this.resolveOwner(owner);
        const liqsolMint = this.pgs.deriveLiqsolMintPda();
        const userAta = getAssociatedTokenAddressSync(
            liqsolMint,
            resolvedOwner,
            false,
            TOKEN_2022_PROGRAM_ID,
        );

        const [nativeLamports, liqsolBalance] = await Promise.all([
            this.provider.connection.getBalance(resolvedOwner, commitment),
            this.provider.connection.getTokenAccountBalance(userAta, commitment).catch(() => null),
        ]);

        return {
            nativeLamports: BigInt(nativeLamports ?? 0).toString(),
            liqsolLamports: liqsolBalance?.value?.amount ?? '0',
            liqsolDecimals: Number(liqsolBalance?.value?.decimals ?? 9),
        };
    }

    private async finalizeReadyEpoch(force = false): Promise<boolean> {
        const program = this.program;
        const epochNumber = await this.pgs.fetchOppEpochNumber(this.provider.connection);
        const pendingAttestations = await this.fetchPendingAttestations(epochNumber);
        const attestations = pendingAttestations?.list ?? [];

        if (!attestations.length) {
            return false;
        }

        if (!force) {
            const oppEpochState: any = await program.account.oppEpochState.fetch(
                this.pgs.deriveOppEpochStatePda(),
            );
            const globalConfig: any = await program.account.globalConfig.fetch(
                this.pgs.deriveGlobalConfigPda(),
            );
            const attestationCount = attestations.length;
            const maxAttestations = this.toSafeNumber(
                globalConfig.oppMaxAttestations || DEFAULT_SOLANA_EPOCH_CAP,
            );
            const maxIntervalSecs = this.toSafeNumber(globalConfig.oppMaxIntervalSecs || 0);
            const outboundEpochStart = this.toSafeNumber(oppEpochState.outboundEpochStart || 0);
            const now = Math.floor(Date.now() / 1000);
            const readyByCount = attestationCount >= maxAttestations;
            const readyByTime =
                attestationCount > 0 &&
                outboundEpochStart > 0 &&
                maxIntervalSecs > 0 &&
                now - outboundEpochStart >= maxIntervalSecs;

            if (!readyByCount && !readyByTime) {
                return false;
            }
        }

        const signer = this.provider.wallet.publicKey;
        if (!signer) {
            throw new Error('Connect a Solana wallet first.');
        }

        const remainingAccounts = Array.from(
            { length: attestations.length },
            (_, index) => ({
                pubkey: this.pgs.deriveEpochMessagePda(epochNumber, index),
                isWritable: true,
                isSigner: false,
            }),
        );

        await this.runRpcWithRecovery(() =>
            program.methods
                .finalizeEpoch()
                .accounts({
                    oppEpochState: this.pgs.deriveOppEpochStatePda(),
                    pendingAttestations: this.pgs.derivePendingAttestationsPda(epochNumber),
                    epochHeader: this.pgs.deriveEpochHeaderPda(epochNumber),
                    signer,
                    systemProgram: SystemProgram.programId,
                    globalConfig: this.pgs.deriveGlobalConfigPda(),
                })
                .preInstructions([
                    ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
                ])
                .remainingAccounts(remainingAccounts)
                .rpc(),
        );

        return true;
    }

    private async fetchPendingAttestations(epochNumber: number): Promise<any | null> {
        const program = this.program;
        try {
            return await program.account.pendingAttestations.fetch(
                this.pgs.derivePendingAttestationsPda(epochNumber),
            );
        } catch {
            return null;
        }
    }

    private isEpochFinalizationRequired(error: unknown): boolean {
        const message = error instanceof Error ? error.message : String(error ?? '');
        return message.includes('EpochFinalizationRequired')
            || message.includes('Epoch finalization required')
            || message.includes('Outbound epoch is already ready');
    }

    private async runRpcWithRecovery(
        submit: () => Promise<TransactionSignature>,
    ): Promise<TransactionSignature> {
        try {
            return await submit();
        } catch (error) {
            const signature = this.extractTimedOutSignature(error);
            if (!signature) {
                throw error;
            }

            const recovered = await this.waitForSignatureSuccess(signature);
            if (!recovered) {
                throw error;
            }

            return signature;
        }
    }

    private extractTimedOutSignature(error: unknown): string | null {
        const message = error instanceof Error
            ? error.message
            : typeof error === 'string'
                ? error
                : JSON.stringify(error ?? '');
        if (!message.includes('Transaction was not confirmed')) {
            return null;
        }
        const labeledMatch = message.match(
            /(?:signature|txSignature)["'\s:]+([1-9A-HJ-NP-Za-km-z]{32,88})/i,
        );
        if (labeledMatch?.[1]) {
            return labeledMatch[1];
        }
        const anyBase58Match = message.match(/([1-9A-HJ-NP-Za-km-z]{32,88})/);
        return anyBase58Match?.[1] ?? null;
    }

    private async waitForSignatureSuccess(signature: string): Promise<boolean> {
        for (let attempt = 0; attempt < 10; attempt += 1) {
            const response = await this.provider.connection.getSignatureStatuses(
                [signature],
                { searchTransactionHistory: true },
            );
            const status = response.value[0];
            if (status?.err) {
                throw new Error(
                    `Solana transaction ${signature} failed after timeout: ${JSON.stringify(status.err)}`,
                );
            }
            if (
                status?.confirmationStatus === commitment ||
                status?.confirmationStatus === 'finalized'
            ) {
                return true;
            }
            await this.sleep(1500);
        }
        return false;
    }

    private resolveOwner(owner?: PublicKey | string): PublicKey {
        if (owner instanceof PublicKey) {
            return owner;
        }
        if (typeof owner === 'string' && owner.trim()) {
            return new PublicKey(owner.trim());
        }
        const walletOwner = this.provider.wallet.publicKey;
        if (!walletOwner) {
            throw new Error('Connect a Solana wallet first.');
        }
        return walletOwner;
    }

    private toSafeNumber(value: unknown): number {
        if (typeof value === 'number' && Number.isFinite(value)) {
            return value;
        }
        if (typeof value === 'bigint') {
            return Number(value);
        }
        if (value && typeof value === 'object' && 'toNumber' in value) {
            return (value as { toNumber: () => number }).toNumber();
        }
        if (value && typeof value === 'object' && 'toString' in value) {
            const parsed = Number((value as { toString: () => string }).toString());
            return Number.isFinite(parsed) ? parsed : 0;
        }
        return 0;
    }

    private async sleep(ms: number): Promise<void> {
        await new Promise((resolve) => setTimeout(resolve, ms));
    }

    private async sendInstructions(
        ix: TransactionInstruction | TransactionInstruction[],
    ): Promise<string> {
        if (!this.submitInstructions) {
            throw new Error('Solana instruction submitter is not configured.');
        }
        return this.submitInstructions(ix);
    }
}
