import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
import {
    SystemProgram,
    Transaction,
    TransactionInstruction,
    StakeProgram,
    SYSVAR_INSTRUCTIONS_PUBKEY,
    SYSVAR_CLOCK_PUBKEY,
    SYSVAR_RENT_PUBKEY,
    SYSVAR_STAKE_HISTORY_PUBKEY,
} from '@solana/web3.js';
import {
    TOKEN_2022_PROGRAM_ID,
    ASSOCIATED_TOKEN_PROGRAM_ID,
    getAssociatedTokenAddressSync,
} from '@solana/spl-token';

import { SolanaProgramService } from '../program';
import type { LiqsolCore } from '../../../assets/solana/types/liqsol_core';

import {
    PROGRAM_IDS,
    deriveDepositAuthorityPda,
    deriveLiqsolMintPda,
    deriveLiqsolMintAuthorityPda,
    deriveReservePoolPda,
    deriveVaultPda,
    deriveStakeControllerStatePda,
    derivePayoutStatePda,
    deriveBucketAuthorityPda,
    derivePayRateHistoryPda,
    deriveDistributionStatePda,
    deriveUserRecordPda,
    deriveEphemeralStakeAddress,
    deriveWithdrawGlobalPda,
    deriveStakeAllocationStatePda,
    deriveStakeMetricsPda,
    deriveMaintenanceLedgerPda,
    deriveWithdrawMintAuthorityPda,
    deriveWithdrawMintMetadataPda,
    deriveWithdrawNftMintPda,
    deriveLiqReceiptDataPda,
    deriveGlobalConfigPda,
} from '../constants';
import { GlobalAccount, WalletLike } from '../types';

export class DepositClient {
    private program: Program<LiqsolCore>;

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

    constructor(private provider: AnchorProvider) {
        const svc = new SolanaProgramService(provider);
        this.program = svc.getProgram('liqsolCore');
    }

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

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

        // -------------------------------------------------------------
        // Token-2022 ATAs
        // -------------------------------------------------------------
        const userAta = getAssociatedTokenAddressSync(
            liqsolMint,
            user,
            false,
            TOKEN_2022_PROGRAM_ID,
        );

        // -------------------------------------------------------------
        // Distribution state + user_record (KEYED BY TOKEN ACCOUNT)
        // -------------------------------------------------------------
        const distributionState = deriveDistributionStatePda();
        const userRecord = deriveUserRecordPda(userAta);

        const bucketTokenAccount = getAssociatedTokenAddressSync(
            liqsolMint,
            bucketAuthority,
            true,
            TOKEN_2022_PROGRAM_ID,
        );

        // -------------------------------------------------------------
        // Ephemeral stake
        // -------------------------------------------------------------
        const seed = Math.floor(Math.random() * 2 ** 32);
        const ephemeralStake = await deriveEphemeralStakeAddress(user, seed);

        // -------------------------------------------------------------
        // BUILD IX (MUST MATCH IDL)
        // -------------------------------------------------------------
        const ix: TransactionInstruction = 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: 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();

        return new Transaction().add(ix);
    }

    /**
     * Build a withdraw-request transaction:
     *   liqSOL -> SOL via liqsol_core::requestWithdraw.
     *
     * This:
     *  - burns liqSOL from the user
     *  - increments totalEncumberedFunds in global state
     *  - mints an NFT receipt (liqReceiptData + NFT ATA for owner)
     */
    async buildWithdrawTx(
        amount: bigint,
        user = this.wallet.publicKey,
    ): Promise<Transaction> {
        if (!user) {
            throw new Error(
                'DepositClient.buildWithdrawTx: wallet not connected',
            );
        }
        if (!amount || amount <= BigInt(0)) {
            throw new Error(
                'DepositClient.buildWithdrawTx: amount must be greater than zero.',
            );
        }

        // -------------------------------------------------------------
        // Core program + liqSOL mint / user ATA
        // -------------------------------------------------------------
        const liqsolMint = deriveLiqsolMintPda();

        const userAta = getAssociatedTokenAddressSync(
            liqsolMint,
            user,
            false,
            TOKEN_2022_PROGRAM_ID,
        );

        // Distribution / balance-tracking
        // user_record is keyed by the user’s liqSOL ATA (same convention
        // as deposit/purchase).
        const userRecord = deriveUserRecordPda(userAta);
        const distributionState = deriveDistributionStatePda();

        // Reserve + stake controller PDAs
        const global = deriveWithdrawGlobalPda();          // withdraw operator state
        const reservePool = deriveReservePoolPda();
        const stakeAllocationState = deriveStakeAllocationStatePda();
        const stakeMetrics = deriveStakeMetricsPda();
        const maintenanceLedger = deriveMaintenanceLedgerPda();
        const globalConfig = deriveGlobalConfigPda();      // liqSOL config / roles

        // -------------------------------------------------------------
        // Need nextReceiptId from withdraw global state
        // -------------------------------------------------------------
        const globalAcct : GlobalAccount = await this.program.account.global.fetch(global);

        const rawId = globalAcct.nextReceiptId;
        let receiptId: bigint;

        if (typeof rawId === 'bigint') {
            // New-style IDL / accounts returning bigint directly
            receiptId = rawId;
        } else if (rawId != null && typeof rawId === 'object' && 'toString' in rawId) {
            // Anchor BN / bn.js or similar – normalize through string
            receiptId = BigInt(rawId.toString());
        } else if (typeof rawId === 'number') {
            // Just in case someone typed it as a JS number in tests
            receiptId = BigInt(rawId);
        } else {
            throw new Error(
                `DepositClient.buildWithdrawTx: unexpected nextReceiptId type (${typeof rawId})`,
            );
        }

        // -------------------------------------------------------------
        // NFT receipt PDAs (mint, metadata, data, ATA)
        // -------------------------------------------------------------
        const mintAuthority = deriveWithdrawMintAuthorityPda();
        const metadata = deriveWithdrawMintMetadataPda();

        const nftMint = deriveWithdrawNftMintPda(receiptId);
        const receiptData = deriveLiqReceiptDataPda(nftMint);

        const owner = user;
        const nftAta = getAssociatedTokenAddressSync(
            nftMint,
            owner,
            false,
            TOKEN_2022_PROGRAM_ID,
        );

        // Bucket token account (same bucket used by deposit/distribution)
        const bucketAuthority = deriveBucketAuthorityPda();
        const bucketTokenAccount = getAssociatedTokenAddressSync(
            liqsolMint,
            bucketAuthority,
            true,
            TOKEN_2022_PROGRAM_ID,
        );

        // -------------------------------------------------------------
        // BUILD IX (MUST MATCH requestWithdraw IDL)
        // -------------------------------------------------------------
        const ix: TransactionInstruction = 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();

        return new Transaction().add(ix);
    }
}