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

import {
    TOKEN_2022_PROGRAM_ID,
    ASSOCIATED_TOKEN_PROGRAM_ID,
    getAssociatedTokenAddressSync,
} from '@solana/spl-token';
import * as multisig from "@sqds/multisig";

import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';

import { GlobalAccount, WalletLike } from '../types';

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

    get 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 transaction:
     *   SOL -> liqSOL via liqsol_core::deposit.
     */
    async buildDepositTx(
        amount: bigint,
        user = this.wallet.publicKey,
    ): Promise<TransactionInstruction> {
        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 = 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();

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

        // -------------------------------------------------------------
        // Distribution state + user_record (KEYED BY TOKEN ACCOUNT)
        // -------------------------------------------------------------
        const distributionState = this.pgs.deriveDistributionStatePda();
        const userRecord = this.pgs.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 this.pgs.deriveEphemeralStakeAddress(user, seed);

        // -------------------------------------------------------------
        // BUILD IX (MUST MATCH IDL)
        // -------------------------------------------------------------
        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 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<TransactionInstruction> {
        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 = this.pgs.deriveLiqsolMintPda();

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

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

        // Reserve + stake controller PDAs
        const global = this.pgs.deriveWithdrawGlobalPda();          // withdraw operator state
        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();      // 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 = 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,
        );

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

        // -------------------------------------------------------------
        // BUILD IX (MUST MATCH requestWithdraw IDL)
        // -------------------------------------------------------------
        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();

    }
}

// A “wallet-adapter-like” shape (AnchorProvider.wallet matches this)
type WalletLikeSigner = {
    publicKey: PublicKey;
    signTransaction: (tx: Transaction) => Promise<Transaction>;
};

function isKeypairSigner(x: any): x is { publicKey: PublicKey; secretKey: Uint8Array } {
    return !!x && x.publicKey instanceof PublicKey && x.secretKey instanceof Uint8Array;
}

function isWalletLikeSigner(x: any): x is WalletLikeSigner {
    return !!x && x.publicKey instanceof PublicKey && typeof x.signTransaction === "function";
}
