import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
import {
    SystemProgram,
    TransactionInstruction,
    StakeProgram,
    SYSVAR_INSTRUCTIONS_PUBKEY,
    SYSVAR_CLOCK_PUBKEY,
    SYSVAR_RENT_PUBKEY,
    SYSVAR_STAKE_HISTORY_PUBKEY,
    Connection,
    PublicKey,
} from '@solana/web3.js';

import {
    TOKEN_2022_PROGRAM_ID,
    ASSOCIATED_TOKEN_PROGRAM_ID,
    getAssociatedTokenAddressSync,
} from '@solana/spl-token';

import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';
import { GlobalAccount, ReceiptData, WalletLike } from '../types';
import { BalanceView, WithdrawReceipt } from '../../../types';
import { normalizeToBigInt } from '../utils';

/**
 * ConvertClient (Solana):
 * - deposit SOL -> liqSOL
 * - request withdraw (liqSOL burn -> NFT receipt)
 * - list withdrawal receipts owned by a user
 * - build claim_withdraw instruction
 */
export class ConvertClient {
    private program: Program<LiqsolCoreClientIdl>;

    get connection(): Connection {
        return this.provider.connection;
    }

    get wallet(): WalletLike {
        return this.provider.wallet;
    }

    constructor(
        private readonly provider: AnchorProvider,
        private readonly pgs: SolanaProgramService,
    ) {
        this.program = pgs.getProgram('liqsolCore');
    }

    /**
     * Build a deposit instruction (SOL -> liqSOL).
     */
    async buildDepositTx(
        amount: bigint,
        user = this.wallet.publicKey,
    ): Promise<TransactionInstruction> {
        if (!user) throw new Error('ConvertClient.buildDepositTx: wallet not connected');
        if (!amount || amount <= BigInt(0))
            throw new Error('ConvertClient.buildDepositTx: amount must be greater than zero.');

        const depositAuthority = this.pgs.deriveDepositAuthorityPda();
        const liqsolMint = this.pgs.deriveLiqsolMintPda();
        const liqsolMintAuthority = this.pgs.deriveLiqsolMintAuthorityPda();
        const reservePool = this.pgs.deriveReservePoolPda();
        const vault = this.pgs.deriveVaultPda();
        const controllerState = this.pgs.deriveStakeControllerStatePda();
        const payoutState = this.pgs.derivePayoutStatePda();
        const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
        const payRateHistory = this.pgs.derivePayRateHistoryPda();
        const globalConfig = this.pgs.deriveGlobalConfigPda();

        const userAta = getAssociatedTokenAddressSync(liqsolMint, user, true, TOKEN_2022_PROGRAM_ID);
        const distributionState = this.pgs.deriveDistributionStatePda();
        const userRecord = this.pgs.deriveUserRecordPda(userAta);
        const bucketTokenAccount = getAssociatedTokenAddressSync(
            liqsolMint,
            bucketAuthority,
            true,
            TOKEN_2022_PROGRAM_ID,
        );

        const seed = Math.floor(Math.random() * 2 ** 32);
        const ephemeralStake = await this.pgs.deriveEphemeralStakeAddress(user, seed);

        return await this.program.methods
            .deposit(new BN(amount.toString()), seed)
            .accounts({
                user,
                depositAuthority,
                systemProgram: SystemProgram.programId,
                tokenProgram: TOKEN_2022_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                liqsolProgram: this.pgs.PROGRAM_IDS.LIQSOL_TOKEN,
                stakeProgram: StakeProgram.programId,
                liqsolMint,
                userAta,
                liqsolMintAuthority,
                reservePool,
                vault,
                ephemeralStake,
                controllerState,
                payoutState,
                bucketAuthority,
                bucketTokenAccount,
                userRecord,
                distributionState,
                payRateHistory,
                instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
                clock: SYSVAR_CLOCK_PUBKEY,
                stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
                rent: SYSVAR_RENT_PUBKEY,
                globalConfig,
            })
            .instruction();
    }

    /**
     * Build a withdraw-request instruction (liqSOL burn -> NFT receipt).
     */
    async buildWithdrawTx(
        amount: bigint,
        user = this.wallet.publicKey,
    ): Promise<TransactionInstruction> {
        if (!user) throw new Error('ConvertClient.buildWithdrawTx: wallet not connected');
        if (!amount || amount <= BigInt(0))
            throw new Error('ConvertClient.buildWithdrawTx: amount must be greater than zero.');

        const liqsolMint = this.pgs.deriveLiqsolMintPda();
        const userAta = getAssociatedTokenAddressSync(liqsolMint, user, true, TOKEN_2022_PROGRAM_ID);
        const userRecord = this.pgs.deriveUserRecordPda(userAta);
        const distributionState = this.pgs.deriveDistributionStatePda();

        const global = this.pgs.deriveWithdrawGlobalPda();
        const reservePool = this.pgs.deriveReservePoolPda();
        const stakeAllocationState = this.pgs.deriveStakeAllocationStatePda();
        const stakeMetrics = this.pgs.deriveStakeMetricsPda();
        const maintenanceLedger = this.pgs.deriveMaintenanceLedgerPda();
        const globalConfig = this.pgs.deriveGlobalConfigPda();

        const globalAcct: GlobalAccount = await this.program.account.global.fetch(global);
        const rawId = globalAcct.nextReceiptId;
        const receiptId = normalizeToBigInt(rawId);

        const mintAuthority = this.pgs.deriveWithdrawMintAuthorityPda();
        const metadata = this.pgs.deriveWithdrawMintMetadataPda();
        const nftMint = this.pgs.deriveWithdrawNftMintPda(receiptId);
        const receiptData = this.pgs.deriveLiqReceiptDataPda(nftMint);
        const owner = user;
        const nftAta = getAssociatedTokenAddressSync(nftMint, owner, true, TOKEN_2022_PROGRAM_ID);

        const bucketAuthority = this.pgs.deriveBucketAuthorityPda();
        const bucketTokenAccount = getAssociatedTokenAddressSync(
            liqsolMint,
            bucketAuthority,
            true,
            TOKEN_2022_PROGRAM_ID,
        );

        return await this.program.methods
            .requestWithdraw(new BN(amount.toString()))
            .accounts({
                user,
                owner,
                global,
                liqsolMint,
                userAta,
                userRecord,
                reservePool,
                stakeAllocationState,
                stakeMetrics,
                maintenanceLedger,
                clock: SYSVAR_CLOCK_PUBKEY,
                mintAuthority,
                receiptData,
                metadata,
                nftMint,
                nftAta,
                distributionState,
                bucketTokenAccount,
                tokenProgram: TOKEN_2022_PROGRAM_ID,
                tokenInterface: TOKEN_2022_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                systemProgram: SystemProgram.programId,
                rent: SYSVAR_RENT_PUBKEY,
                globalConfig,
            })
            .instruction();
    }

    /**
     * Enumerate withdrawal receipt NFTs owned by `owner`.
     */
    async fetchWithdrawReceipts(owner: PublicKey): Promise<WithdrawReceipt[]> {
        const globalPda = this.pgs.deriveWithdrawGlobalPda();
        const globalAcct: GlobalAccount = await this.program.account.global.fetch(globalPda);
        const nextId = normalizeToBigInt(globalAcct.nextReceiptId);

        const mintToId = new Map<string, bigint>();
        for (let i = BigInt(0); i < nextId; i++) {
            const mint = this.pgs.deriveWithdrawNftMintPda(i);
            mintToId.set(mint.toBase58(), i);
        }

        const tokenAccounts = await this.connection.getParsedTokenAccountsByOwner(owner, {
            programId: TOKEN_2022_PROGRAM_ID,
        });

        const receipts: WithdrawReceipt[] = [];

        for (const { pubkey, account } of tokenAccounts.value) {
            const info: any = account.data?.['parsed']?.info;
            if (!info) continue;
            const amount = info.tokenAmount;
            const decimals = Number(amount?.decimals ?? 0);
            const uiAmount = Number(amount?.uiAmount ?? 0);
            if (decimals !== 0 || uiAmount !== 1) continue;

            const mintStr: string | undefined = info.mint;
            if (!mintStr) continue;
            const receiptId = mintToId.get(mintStr);
            if (receiptId === undefined) continue;

            const mintKey = new PublicKey(mintStr);
            const receiptDataPda = this.pgs.deriveLiqReceiptDataPda(mintKey);

            let receiptData: ReceiptData;
            try {
                const raw = await this.program.account.liqReceiptData.fetch(receiptDataPda);
                receiptData = {
                    receiptId: normalizeToBigInt(raw.receiptId),
                    liqports: normalizeToBigInt(raw.liqports),
                    epoch: normalizeToBigInt(raw.epoch),
                    fulfilled: Boolean(raw.fulfilled),
                };
            } catch (err) {
                console.warn(`ConvertClient: failed to fetch receipt data for mint ${mintStr}`, err);
                continue;
            }

            const { etaMs, readyAtMs } = await this.estimateEpochEta(receiptData.epoch);
            const status = receiptData.fulfilled ? 'claimed' : etaMs <= 0 ? 'ready' : 'queued';

            const amountView: BalanceView = {
                amount: receiptData.liqports,
                decimals: 9,
                symbol: 'SOL',
            };

            receipts.push({
                tokenId: receiptData.receiptId,
                receipt: {
                    amount: amountView,
                    readyAt: readyAtMs,
                    chain: 'SOL',
                    epoch: receiptData.epoch,
                    status,
                    mint: mintKey.toBase58(),
                    ownerAta: pubkey.toBase58(),
                },
            });
        }

        return receipts;
    }

    /**
     * Build the claim_withdraw instruction for a given receiptId.
     */
    async buildClaimWithdrawTx(
        receiptId: bigint,
        user: PublicKey,
    ): Promise<TransactionInstruction> {
        const mintAccount = this.pgs.deriveWithdrawNftMintPda(receiptId);
        const receiptData = this.pgs.deriveLiqReceiptDataPda(mintAccount);
        const ownerAta = getAssociatedTokenAddressSync(
            mintAccount,
            user,
            true,
            TOKEN_2022_PROGRAM_ID,
        );

        const accounts = {
            user,
            global: this.pgs.deriveWithdrawGlobalPda(),
            mintAuthority: this.pgs.deriveWithdrawMintAuthorityPda(),
            receiptData,
            mintAccount,
            ownerAta,
            reservePool: this.pgs.deriveReservePoolPda(),
            vault: this.pgs.deriveVaultPda(),
            clock: SYSVAR_CLOCK_PUBKEY,
            stakeHistory: SYSVAR_STAKE_HISTORY_PUBKEY,
            globalConfig: this.pgs.deriveGlobalConfigPda(),
            tokenProgram: TOKEN_2022_PROGRAM_ID,
            stakeProgram: StakeProgram.programId,
            systemProgram: SystemProgram.programId,
            associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        };

        return this.program.methods.claimWithdraw().accounts(accounts).instruction();
    }

    /**
     * Estimate ready time for target epoch using recent slot time.
     */
    private async estimateEpochEta(targetEpoch: bigint): Promise<{ etaMs: number; readyAtMs: number }> {
        const conn = this.connection;
        const epochInfo = await conn.getEpochInfo();
        const schedule = await conn.getEpochSchedule();
        const currentEpoch = BigInt(epochInfo.epoch);

        if (targetEpoch <= currentEpoch) {
            const now = Date.now();
            return { etaMs: 0, readyAtMs: now };
        }

        let slotTimeSec = 0.4;
        try {
            const samples = await conn.getRecentPerformanceSamples(1);
            if (samples?.length) {
                const s = samples[0];
                slotTimeSec = s.numSlots > 0 ? s.samplePeriodSecs / s.numSlots : slotTimeSec;
            }
        } catch (_) {
            // ignore
        }

        const slotsPerEpoch = BigInt(schedule.slotsPerEpoch);
        const slotsRemainingInCurrent = slotsPerEpoch - BigInt(epochInfo.slotIndex);
        const epochsRemaining = targetEpoch - currentEpoch - BigInt(1);
        const slotsRemaining =
            (epochsRemaining > 0 ? epochsRemaining * slotsPerEpoch : BigInt(0)) +
            slotsRemainingInCurrent;

        const etaSeconds = Number(slotsRemaining) * slotTimeSec;
        const etaMs = Math.max(0, Math.round(etaSeconds * 1000));
        return { etaMs, readyAtMs: Date.now() + etaMs };
    }
}