import {
    Commitment,
    ComputeBudgetProgram,
    Connection,
    ConnectionConfig,
    PerfSample,
    PublicKey as SolPubKey,
    SystemProgram,
    Transaction,
    TransactionSignature,
} from '@solana/web3.js';
import { AnchorProvider, BN } from '@coral-xyz/anchor';
import { BaseSignerWalletAdapter } from '@solana/wallet-adapter-base';
import {
    ASSOCIATED_TOKEN_PROGRAM_ID,
    getAssociatedTokenAddressSync,
    TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';

import {
    ChainID,
    ExternalNetwork,
    KeyType,
    PublicKey,
    SolChainID,
} from '@wireio/core';

import {
    IStakingClient,
    Portfolio,
    StakerConfig,
    TrancheSnapshot,
} from '../../types';

import { DepositClient } from './clients/deposit.client';
import { DistributionClient } from './clients/distribution.client';
import { LeaderboardClient } from './clients/leaderboard.client';
import { OutpostClient } from './clients/outpost.client';
import { TokenClient } from './clients/token.client';

import {
    deriveLiqsolMintPda,
    deriveReservePoolPda,
    deriveStakeMetricsPda,
    deriveVaultPda,
    INDEX_SCALE,
    PAY_RATE_SCALE_FACTOR,
} from './constants';

import { buildSolanaTrancheSnapshot, ceilDiv } from './utils';
import { GlobalConfig, PayRateEntry, SolanaTransaction } from './types';
import { SolanaProgramService } from './program';

const commitment: Commitment = 'confirmed';

export const SCALE = new BN('1000000000000');

/**
 * Solana implementation of IStakingClient.
 *
 * Responsibilities:
 *  - liqSOL deposit (SOL -> liqSOL)
 *  - withdraw requests (liqSOL -> NFT receipt / encumbered SOL)
 *  - Pretoken (WIRE) buys via liqSOL
 *  - Unified portfolio + tranche snapshot + balance correction (later)
 *
 * This composes lower-level clients; it does not know about UI.
 */
export class SolanaStakingClient implements IStakingClient {
    public pubKey?: PublicKey; // Wire ED key (optional → read-only)
    public connection: Connection;
    public anchor: AnchorProvider;

    public depositClient: DepositClient;
    public distributionClient: DistributionClient;
    public leaderboardClient: LeaderboardClient;
    public outpostClient: OutpostClient;
    public tokenClient: TokenClient;
    public program: SolanaProgramService

    get solPubKey(): SolPubKey {
        if (!this.pubKey) throw new Error('pubKey is undefined');
        return new SolPubKey(this.pubKey.data.array);
    }

    get network(): ExternalNetwork {
        return this.config.network;
    }

    constructor(private config: StakerConfig) {
        const adapter = config.provider as BaseSignerWalletAdapter | undefined;

        if (!config.network.rpcUrls.length) {
            throw new Error('No RPC URLs provided');
        }

        // -------------------------------------------------------------
        // Resolve Solana wallet pubkey (or dummy in read-only mode)
        // -------------------------------------------------------------
        let solWalletPubkey: SolPubKey;
        if (adapter?.publicKey) {
            solWalletPubkey = adapter.publicKey;
        } else {
            // Zeroed pubkey when no wallet (read-only usage)
            solWalletPubkey = new SolPubKey(new Uint8Array(32));
        }

        // -------------------------------------------------------------
        // Resolve Wire pubKey
        // -------------------------------------------------------------
        if (config.pubKey) {
            const wirePub = config.pubKey;

            if (adapter?.publicKey) {
                const derived = new PublicKey(
                    KeyType.ED,
                    adapter.publicKey.toBytes(),
                );
                if (!derived.equals(wirePub)) {
                    throw new Error(
                        "Passed-in pubKey doesn't match adapter.publicKey",
                    );
                }
            }

            this.pubKey = wirePub;
        } else if (adapter?.publicKey) {
            // Derive Wire pubKey from adapter when not explicitly passed
            this.pubKey = new PublicKey(
                KeyType.ED,
                adapter.publicKey.toBytes(),
            );
        }

        // -------------------------------------------------------------
        // Connection + AnchorProvider
        // -------------------------------------------------------------
        const opts: ConnectionConfig = { commitment };
        if (
            config.network.rpcUrls.length > 1 &&
            config.network.rpcUrls[1].startsWith('ws')
        ) {
            opts.wsEndpoint = config.network.rpcUrls[1];
        }

        const anchorWallet = adapter
            ? {
                publicKey: solWalletPubkey,
                async signTransaction<T extends SolanaTransaction>(
                    tx: T,
                ): Promise<T> {
                    return adapter.signTransaction(tx);
                },
                async signAllTransactions<T extends SolanaTransaction>(
                    txs: T[],
                ): Promise<T[]> {
                    if (adapter.signAllTransactions) {
                        return adapter.signAllTransactions(txs);
                    }
                    return Promise.all(
                        txs.map((tx) => adapter.signTransaction(tx)),
                    );
                },
            }
            : {
                publicKey: solWalletPubkey,
                async signTransaction<T extends SolanaTransaction>(
                    _tx: T,
                ): Promise<T> {
                    throw new Error(
                        'Wallet not connected: signTransaction not available',
                    );
                },
                async signAllTransactions<T extends SolanaTransaction>(
                    _txs: T[],
                ): Promise<T[]> {
                    throw new Error(
                        'Wallet not connected: signAllTransactions not available',
                    );
                },
            };

        this.connection = new Connection(config.network.rpcUrls[0], opts);
        this.anchor = new AnchorProvider(this.connection, anchorWallet, {
            commitment,
        });

        this.depositClient = new DepositClient(this.anchor);
        this.distributionClient = new DistributionClient(this.anchor);
        this.leaderboardClient = new LeaderboardClient(this.anchor);
        this.outpostClient = new OutpostClient(this.anchor);
        this.tokenClient = new TokenClient(this.anchor);
        this.program = new SolanaProgramService(this.anchor);
    }

    // ---------------------------------------------------------------------
    // IStakingClient core methods
    // ---------------------------------------------------------------------

    /**
     * Deposit native SOL into liqSOL (liqsol_core::deposit).
     * Handles tx build, sign, send, and confirmation.
     */
    async deposit(amountLamports: bigint): Promise<string> {
        this.ensureUser();
        if (amountLamports <= BigInt(0)) {
            throw new Error('Deposit amount must be greater than zero.');
        }

        try {
            // Build compute budget increase instruction
            const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });

            // Build the Outpost synd instruction
            const ix = await this.depositClient.buildDepositTx(amountLamports);

            // Wrap in a transaction and send
            const tx = new Transaction().add(cuIx, ix);
            const prepared = await this.prepareTx(tx);
            const signed = await this.signTransaction(prepared.tx);

            return this.sendAndConfirmHttp(signed, prepared);
        } catch (err) {
            throw new Error(`Failed to deposit Solana: ${err}`);
        }
    }

    /**
     * Request a withdraw from liqSOL to SOL (liqsol_core::requestWithdraw).
     *
     * This:
     *  - burns liqSOL from the user ATA
     *  - mints an NFT withdrawal receipt
     *  - increases totalEncumberedFunds in global state
     *
     * Actual SOL payout happens later via the operator-side flow.
     */
    async withdraw(amountLamports: bigint): Promise<string> {
        this.ensureUser();
        if (amountLamports <= BigInt(0)) {
            throw new Error('Withdraw amount must be greater than zero.');
        }

        try {
            // Build compute budget increase instruction
            const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });

            // Build the Outpost synd instruction
            const ix = await this.depositClient.buildWithdrawTx(amountLamports);

            // Wrap in a transaction and send
            const tx = new Transaction().add(cuIx, ix);
            const prepared = await this.prepareTx(tx);
            const signed = await this.signTransaction(prepared.tx);

            return this.sendAndConfirmHttp(signed, prepared);
        } catch (err) {
            throw new Error(`Failed to withdraw Solana: ${err}`);
        }
    }

    /**
     * Stake liqSOL into Outpost (liqSOL → pool) via liqsol_core::synd.
     */
    async stake(amountLamports: bigint): Promise<string> {
        this.ensureUser();

        if (!amountLamports || amountLamports <= BigInt(0)) {
            throw new Error('Stake amount must be greater than zero.');
        }

        try {
            const user = this.solPubKey;

            // Build compute budget increase instruction
            const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });

            // Build the Outpost synd instruction
            const ix = await this.outpostClient.buildStakeIx(amountLamports, user);

            // Wrap in a transaction and send
            const tx = new Transaction().add(cuIx, ix);
            const prepared = await this.prepareTx(tx);
            const signed = await this.signTransaction(prepared.tx);

            return this.sendAndConfirmHttp(signed, prepared);
        } catch (err) {
            throw new Error(`Failed to stake Solana: ${err}`);
        }
    }

    /**
     * Unstake liqSOL from Outpost (pool → liqSOL) via liqsol_core::desynd.
     */
    async unstake(amountLamports: bigint): Promise<string> {
        this.ensureUser();

        if (!amountLamports || amountLamports <= BigInt(0)) {
            throw new Error('Unstake amount must be greater than zero.');
        }

        try {
            const user = this.solPubKey;

            // Build compute budget increase instruction
            const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });

            // Build the Outpost desynd instruction
            const ix = await this.outpostClient.buildUnstakeIx(amountLamports, user);

            // Wrap in a transaction and send
            const tx = new Transaction().add(cuIx, ix);
            const prepared = await this.prepareTx(tx);
            const signed = await this.signTransaction(prepared.tx);

            return this.sendAndConfirmHttp(signed, prepared);
        }
        catch (err) {
            throw new Error(`Failed to unstake Solana: ${err}`);
        }
    }

    /**
     * Buy prelaunch WIRE “pretokens” using liqSOL.
     *
     * This delegates to TokenClient, which uses the `purchase`-style
     * instruction under the new IDL (no more native-SOL purchase).
     */
    async buy(amountLamports: bigint): Promise<string> {
        this.ensureUser();
        if (!amountLamports || amountLamports <= BigInt(0)) {
            throw new Error('liqSOL pretoken purchase requires a positive amount.');
        }

        try {
            const user = this.solPubKey;
            const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
            const ix = await this.tokenClient.buildPurchaseIx(amountLamports, user);

            const tx = new Transaction().add(cuIx, ix);
            const prepared = await this.prepareTx(tx);
            const signed = await this.signTransaction(prepared.tx);

            return this.sendAndConfirmHttp(signed, prepared);
        }
        catch (err) {
            throw new Error(`Failed to buy liqSOL pretokens: ${err}`);
        }
    }

    /**
     * Aggregate view of the user’s balances on Solana:
     *  - native: SOL wallet balance
     *  - liq: liqSOL token balance (Token-2022 ATA)
     *  - staked: liqSOL staked in Outpost (synd/desynd) → outpostAccount.stakedLiqsol
     *  - wire: total WIRE pretokens purchased (1e8 scale)
     *  - yield: on-chain index/shares plus an estimated accrued liqSOL yield
     *  - extras: useful internal addresses and raw state for debugging/UX
     */
    async getPortfolio(): Promise<Portfolio> {
        if (!this.pubKey) throw new Error('User pubKey is undefined');

        try {
            const user = this.solPubKey;

            const reservePoolPDA = deriveReservePoolPda();
            const vaultPDA = deriveVaultPda();
            const liqsolMint = deriveLiqsolMintPda();

            const userLiqsolAta = getAssociatedTokenAddressSync(
                liqsolMint,
                user,
                false,
                TOKEN_2022_PROGRAM_ID,
                ASSOCIATED_TOKEN_PROGRAM_ID,
            );

            // NOTE:
            //  - nativeLamports: wallet SOL
            //  - actualBalResp: liqSOL balance in user ATA
            //  - snapshot: Outpost + pretokens + global index/shares
            const [nativeLamports, actualBalResp, snapshot] = await Promise.all([
                this.connection.getBalance(user, 'confirmed'),
                this.connection
                    .getTokenAccountBalance(userLiqsolAta, 'confirmed')
                    .catch(() => null),
                this.outpostClient.fetchWireState(user).catch(() => null),
            ]);

            const LIQSOL_DECIMALS = 9;

            const actualAmountStr = actualBalResp?.value?.amount ?? '0';

            const globalState = snapshot?.globalState ?? null;
            const outpostAccount = snapshot?.outpostAccount ?? null;
            const trancheState = snapshot?.trancheState ?? null;
            const userPretokenRecord = snapshot?.userPretokenRecord ?? null;

            // -----------------------------
            //  Staked liqSOL (Outpost)
            // -----------------------------
            // This is the liqSOL that has been syndicated into Outpost via `synd`.
            // It lives on the outpost account as `stakedLiqsol`.
            const stakedLiqsolStr =
                outpostAccount?.stakedLiqsol?.toString?.() ?? '0';

            // -----------------------------
            //  WIRE pretokens (1e8 scale)
            // -----------------------------
            // This is NOT stake — it’s the prelaunch WIRE position.
            const wirePretokensStr =
                userPretokenRecord?.totalPretokensPurchased?.toString?.() ??
                '0';

            // -----------------------------
            //  Yield view (index + shares)
            // -----------------------------
            // We expose:
            //  - currentIndex: globalState.currentIndex (1e12 scale)
            //  - totalShares:  globalState.totalShares
            //  - userShares:   outpostAccount.stakedShares
            //  - estimatedClaimLiqsol: floor(userShares * index / INDEX_SCALE)
            //  - estimatedYield: max(0, estimatedClaim - stakedLiqsol)
            //
            // This matches the capital-staking math:
            //   sharesToTokens(shares, index) = shares * index / INDEX_SCALE
            const currentIndexStr =
                globalState?.currentIndex?.toString?.() ?? '0';
            const totalSharesStr =
                globalState?.totalShares?.toString?.() ?? '0';
            const userSharesStr =
                outpostAccount?.stakedShares?.toString?.() ?? '0';

            const stakedLiqsol = BigInt(stakedLiqsolStr);
            const currentIndex = BigInt(currentIndexStr);
            const totalShares = BigInt(totalSharesStr);
            const userShares = BigInt(userSharesStr);

            let estimatedClaim = BigInt(0);
            let estimatedYield = BigInt(0);

            if (userShares > BigInt(0) && currentIndex > BigInt(0)) {
                // sharesToTokens(userShares, currentIndex)
                estimatedClaim = (userShares * currentIndex) / INDEX_SCALE;

                if (estimatedClaim > stakedLiqsol) {
                    estimatedYield = estimatedClaim - stakedLiqsol;
                }
            }

            return {
                native: {
                    amount: BigInt(nativeLamports),
                    symbol: 'SOL',
                    decimals: 9,
                },
                liq: {
                    amount: BigInt(actualAmountStr),
                    symbol: 'LiqSOL',
                    decimals: LIQSOL_DECIMALS,
                    ata: userLiqsolAta,
                },
                staked: {
                    // liqSOL staked in Outpost via `synd`
                    amount: stakedLiqsol,
                    symbol: 'LiqSOL',
                    decimals: LIQSOL_DECIMALS,
                },
                wire: {
                    // Prelaunch WIRE pretokens (1e8 scale)
                    amount: BigInt(wirePretokensStr),
                    symbol: '$WIRE',
                    decimals: 8,
                },
                yield: {
                    // Raw primitives so the frontend can display curves, charts, etc.
                    currentIndex,
                    indexScale: INDEX_SCALE,
                    totalShares,
                    userShares,
                    // liqSOL amounts (lamports) implied by index/shares
                    estimatedClaim,
                    estimatedYield,
                },
                extras: {
                    userLiqsolAta: userLiqsolAta.toBase58(),
                    reservePoolPDA: reservePoolPDA.toBase58(),
                    vaultPDA: vaultPDA.toBase58(),
                    globalIndex: globalState?.currentIndex?.toString(),
                    totalShares: globalState?.totalShares?.toString(),
                    currentTrancheNumber:
                        trancheState?.currentTrancheNumber?.toString(),
                    currentTranchePriceUsd:
                        trancheState?.currentTranchePriceUsd?.toString(), // 1e8 USD
                },
                chainID: this.network.chainId,
            };
        }
        catch (err) {
            throw new Error(`Failed to get Solana portfolio: ${err}`);
        }
    }

    /**
     * Convenience helper to fetch the distribution userRecord for the current user.
     * Used by balance-correction flows and debugging.
     */
    async getUserRecord() {
        if (!this.pubKey) throw new Error('User pubKey is undefined');
        return this.distributionClient.getUserRecord(this.solPubKey);
    }


    // ---------------------------------------------------------------------
    // READ-ONLY Public Methods
    // ---------------------------------------------------------------------

    /**
     * Unified, chain-agnostic tranche snapshot for Solana.
     *
     * Uses:
     *  - liqsol_core.globalState (currentIndex, totalShares, etc.)
     *  - liqsol_core.trancheState (price, supply, total sold, etc.)
     *  - PriceHistory/Chainlink SOL/USD via TokenClient.getSolPriceUsdSafe()
     *
     * This is READ-ONLY and works even with no connected wallet.
     */
    async getTrancheSnapshot(options?: {
        chainID?: ChainID;
        windowBefore?: number;
        windowAfter?: number;
    }): Promise<TrancheSnapshot> {
        try {
            const {
                chainID = SolChainID.WireTestnet,
                windowBefore,
                windowAfter,
            } = options ?? {};

            const [globalState, trancheState] = await Promise.all([
                this.tokenClient.fetchGlobalState(),
                this.tokenClient.fetchTrancheState(),
            ]);

            const { price: solPriceUsd, timestamp } =
                await this.tokenClient.getSolPriceUsdSafe();

            return buildSolanaTrancheSnapshot({
                chainID,
                globalState,
                trancheState,
                solPriceUsd,
                nativePriceTimestamp: timestamp,
                ladderWindowBefore: windowBefore,
                ladderWindowAfter: windowAfter,
            });
        }
        catch (err) {
            throw new Error(`Failed to build Solana tranche snapshot: ${err}`);
        }
    }

    /**
     * Returns the system APY (percent) for Solana,
     * using compound interest per epoch and a
     * cluster-derived epochs-per-year.
     */
    async getSystemAPY(): Promise<number> {
        try {
            // 1) Per-epoch rate (decimal) from on-chain stakeMetrics
            const ratePerEpoch = await this.getEpochRateDecimalFromProgram();
            // 2) Live epochs-per-year estimate from cluster
            const epochsPerYear = await this.getEpochsPerYearFromCluster();
            // 3) Compound: (1 + r)^N - 1
            const apyDecimal = Math.pow(1 + ratePerEpoch, epochsPerYear) - 1;
            // 4) Convert to percent
            const apyPercent = apyDecimal * 100;

            return apyPercent;
        } catch (err) {
            throw new Error(`Failed to compute Solana system APY: ${err}`);
        }
    }

    /**
     * Reads the liqsol_core stakeMetrics account and returns the
     * Solana per-epoch system rate as a **decimal** (not BPS),
     * de-scaled using PAY_RATE_SCALE_FACTOR (1e12).
     */
    private async getEpochRateDecimalFromProgram(): Promise<number> {
        try {

            const liqSolCoreProgram = this.program.getProgram('liqsolCore');
            const stakeMetricsPda = deriveStakeMetricsPda();
            const stakeMetrics =
                await liqSolCoreProgram.account.stakeMetrics.fetch(stakeMetricsPda);

            // solSystemPayRate is stored on-chain with PAY_RATE_SCALE_FACTOR (1e12)
            const raw = BigInt(stakeMetrics.solSystemPayRate.toString());

            // Convert to JS number in **decimal per epoch** units
            const rateDecimal = Number(raw) / Number(PAY_RATE_SCALE_FACTOR);

            return rateDecimal;
        }
        catch (err) {
            throw new Error(`Failed to read stakeMetrics from program: ${err}`);
        }
    }

    // Simple cache so we don’t hammer RPC
    private epochsPerYearCache?: { value: number; fetchedAt: number };
    private static readonly EPOCHS_PER_YEAR_TTL_MS = 10 * 60 * 1000; // 10 minutes

    /**
     * Derive "epochs per year" from the live Solana cluster.
     *
     * Uses:
     *  - getRecentPerformanceSamples() to estimate slots/second
     *  - getEpochInfo() to read slotsInEpoch
     */
    private async getEpochsPerYearFromCluster(): Promise<number> {
        const now = Date.now();

        if (
            this.epochsPerYearCache &&
            now - this.epochsPerYearCache.fetchedAt <
            SolanaStakingClient.EPOCHS_PER_YEAR_TTL_MS
        ) {
            return this.epochsPerYearCache.value;
        }

        const connection = this.anchor.connection;

        // 1) Estimate slots/second from recent performance samples
        const samples: PerfSample[] = await connection.getRecentPerformanceSamples(
            60,
        );
        if (!samples.length) {
            throw new Error('No performance samples available from cluster');
        }

        const totalSlots = samples.reduce((acc, s) => acc + s.numSlots, 0);
        const totalSecs = samples.reduce((acc, s) => acc + s.samplePeriodSecs, 0);

        if (totalSecs === 0) {
            throw new Error(
                'Cluster returned zero samplePeriodSecs in performance samples',
            );
        }

        const slotsPerSecond = totalSlots / totalSecs;

        // 2) Slots per epoch from cluster
        const epochInfo = await connection.getEpochInfo(); // finalized commitment by default
        const slotsPerEpoch = epochInfo.slotsInEpoch;

        const secondsPerEpoch = slotsPerEpoch / slotsPerSecond;
        const secondsPerYear = 365 * 24 * 60 * 60;

        const epochsPerYear = secondsPerYear / secondsPerEpoch;

        this.epochsPerYearCache = {
            value: epochsPerYear,
            fetchedAt: now,
        };

        return epochsPerYear;
    }

    // ---------------------------------------------
    // Deposit fee calculation (SOL -> liqSOL)
    // ---------------------------------------------

    /**
     * Estimate the protocol deposit fee in lamports for a given SOL amount,
     * based on recent pay rates and globalConfig.depositFeeMultiplier.
     *
     * - amountLamports: deposit notional in lamports
     * - windowSize: how many recent payRate entries to average (default 5)
     *
     * Returns 0n if payRateHistory or globalConfig is missing, or if
     * there are no valid pay-rate entries yet.
     */
    async getDepositFee(
        amountLamports: bigint,
        windowSize = 5,
    ): Promise<bigint> {
        if (amountLamports <= BigInt(0)) {
            return BigInt(0);
        }

        const [avgPayRate, globalConfig]: [BN, GlobalConfig] = await Promise.all([
            this.distributionClient.getAverageScaledPayRate(windowSize),
            this.distributionClient.getGlobalConfig(),
        ]);

        if (!globalConfig || avgPayRate.isZero()) {
            return BigInt(0);
        }

        // depositFeeMultiplier may be BN or number depending on your type
        const rawMultiplier = globalConfig.depositFeeMultiplier;
        const multiplier = new BN(
            rawMultiplier?.toString?.() ?? rawMultiplier ?? 0,
        );
        if (multiplier.isZero()) {
            return BigInt(0);
        }

        const amountBn = new BN(amountLamports.toString());

        // 10^12 scale (matches scaledRate / index scale)

        const feeBn = ceilDiv(
            avgPayRate.mul(multiplier).mul(amountBn),
            SCALE,
        );

        return BigInt(feeBn.toString());
    }

    /**
     * Compute a conservative "deposit buffer" in lamports that should be left
     * in the wallet so that a MAX deposit (balance - buffer) will succeed.
     *
     * It accounts for:
     *   - Runtime tx fees (via cached dummy self-transfer fee)
     *   - Protocol deposit fee (via getDepositFee(amount))
     *
     * Intended UI usage:
     *   const buffer = await client.getDepositBuffer();
     *   const balance = portfolio.native.amount;
     *   const maxDeposit = balance > buffer ? balance - buffer : BigInt(0);
     */
    async getDepositBuffer(options?: {
        txCount?: number;                 // how many txs to cover in gas buffer (default 2)
        safetyMultiplier?: number;        // safety multiplier on per-tx fee (default 3x)
        minBufferLamports?: bigint;       // minimum gas buffer (default ~0.01 SOL)
        balanceOverrideLamports?: bigint; // for tests/custom callers
    }): Promise<bigint> {
        this.ensureUser();

        const payer = this.solPubKey;

        // -------------------------------------------------------------
        // 1) Current wallet balance (prefer caller override)
        // -------------------------------------------------------------
        const balanceLamports: bigint =
            options?.balanceOverrideLamports ??
            BigInt(await this.connection.getBalance(payer, commitment));

        if (balanceLamports <= BigInt(0)) {
            return BigInt(0);
        }

        // -------------------------------------------------------------
        // 2) Estimate gas buffer (using cached single-tx fee)
        // -------------------------------------------------------------
        let gasBuffer = BigInt(0);

        try {
            const singleTxFeeLamports = await this.getSingleTxFeeLamports();

            const txCount = BigInt(options?.txCount ?? 2);
            const safetyMultiplier = options?.safetyMultiplier ?? 3;
            const safetyScaled = BigInt(Math.round(safetyMultiplier * 100)); // e.g. 300

            let buf =
                (singleTxFeeLamports * txCount * safetyScaled) /
                BigInt(100);

            const defaultMinBufferLamports = BigInt(10_000_000); // ~0.01 SOL
            const minBufferLamports =
                options?.minBufferLamports ?? defaultMinBufferLamports;

            if (buf < minBufferLamports) {
                buf = minBufferLamports;
            }

            gasBuffer = buf;
        } catch {
            // If fee estimation fails, just fall back to "no gas buffer".
            gasBuffer = BigInt(0);
        }

        // If gas buffer alone eats the whole balance, we just keep everything.
        if (balanceLamports <= gasBuffer) {
            return balanceLamports;
        }

        const spendable = balanceLamports - gasBuffer;

        // -------------------------------------------------------------
        // 3) If we can't compute deposit fee, buffer == gasBuffer
        // -------------------------------------------------------------
        if (typeof this.getDepositFee !== 'function') {
            return gasBuffer;
        }

        // -------------------------------------------------------------
        // 4) Approximate principal using linear fee model from ONE RPC call
        //
        //   fee(a) ≈ k * a
        //   want: a + k*a <= spendable → a <= spendable / (1 + k)
        //
        //   We estimate k from fee(spendable) / spendable.
        // -------------------------------------------------------------
        let feeAtSpendable: bigint;

        try {
            feeAtSpendable = await this.getDepositFee(spendable);
        } catch {
            // If protocol fee lookup fails, fall back to gas-only buffer
            return gasBuffer;
        }

        // If there is effectively no protocol fee, keep only gas buffer.
        if (feeAtSpendable <= BigInt(0)) {
            return gasBuffer;
        }

        const s = spendable;
        const f = feeAtSpendable;

        // denom = s + f = s * (1 + k)  (since f ≈ k*s)
        const denom = s + f;
        if (denom === BigInt(0)) {
            return gasBuffer;
        }

        // a ≈ floor( s^2 / (s + f) )
        let a = (s * s) / denom;

        // Tiny safety haircut to avoid edge off-by-one lamports
        const fudge = BigInt(10_000); // ≈ 0.00001 SOL
        let effectivePrincipal =
            a > fudge ? a - fudge : a;

        if (effectivePrincipal < BigInt(0)) {
            effectivePrincipal = BigInt(0);
        }

        const buffer = balanceLamports > effectivePrincipal
            ? balanceLamports - effectivePrincipal
            : balanceLamports;

        // Ensure we never under-reserve gas.
        return buffer < gasBuffer ? gasBuffer : buffer;
    }

    private cachedTxFee?: { value: bigint; fetchedAt: number };
    private static readonly FEE_CACHE_TTL_MS = 60_000; // 60s

    private async getSingleTxFeeLamports(): Promise<bigint> {
        const now = Date.now();

        if (this.cachedTxFee && now - this.cachedTxFee.fetchedAt < SolanaStakingClient.FEE_CACHE_TTL_MS) {
            return this.cachedTxFee.value;
        }

        const payer = this.solPubKey;

        const dummyIx = SystemProgram.transfer({
            fromPubkey: payer,
            toPubkey: payer,
            lamports: 0,
        });

        const tx = new Transaction().add(dummyIx);
        const { blockhash } = await this.connection.getLatestBlockhash(commitment);
        tx.recentBlockhash = blockhash;
        tx.feePayer = payer;

        const message = tx.compileMessage();
        const feeInfo = await this.connection.getFeeForMessage(message, commitment);

        const singleTxFeeLamports = BigInt(feeInfo.value ?? 5000);

        this.cachedTxFee = {
            value: singleTxFeeLamports,
            fetchedAt: now,
        };

        return singleTxFeeLamports;
    }

    // ---------------------------------------------------------------------
    // Tx helpers
    // ---------------------------------------------------------------------

    /**
     * Send a signed transaction over HTTP RPC and wait for confirmation.
     * Throws if the transaction fails.
     */
    private async sendAndConfirmHttp(
        signed: SolanaTransaction,
        ctx: { blockhash: string; lastValidBlockHeight: number },
    ): Promise<string> {
        this.ensureUser();

        const signature = await this.connection.sendRawTransaction(
            signed.serialize(),
            {
                skipPreflight: false,
                preflightCommitment: commitment,
                maxRetries: 3,
            },
        );

        const conf = await this.connection.confirmTransaction(
            {
                signature,
                blockhash: ctx.blockhash,
                lastValidBlockHeight: ctx.lastValidBlockHeight,
            },
            commitment,
        );

        if (conf.value.err) {
            throw new Error(
                `Transaction failed: ${JSON.stringify(conf.value.err)}`,
            );
        }

        return signature;
    }

    /**
     * Sign a single Solana transaction using the connected wallet adapter.
     */
    async signTransaction(
        tx: SolanaTransaction,
    ): Promise<SolanaTransaction> {
        this.ensureUser();
        return this.anchor.wallet.signTransaction(tx);
    }

    /**
     * Generic "fire and forget" send helper if the caller already
     * prepared and signed the transaction.
     */
    async sendTransaction(
        signed: SolanaTransaction,
    ): Promise<TransactionSignature> {
        this.ensureUser();
        return this.anchor.sendAndConfirm(signed);
    }

    /**
     * Attach recent blockhash + fee payer to a transaction.
     * Required before signing and sending.
     */
    async prepareTx(
        tx: Transaction,
    ): Promise<{
        tx: Transaction;
        blockhash: string;
        lastValidBlockHeight: number;
    }> {
        const { blockhash, lastValidBlockHeight } =
            await this.connection.getLatestBlockhash('confirmed');
        tx.recentBlockhash = blockhash;
        tx.feePayer = this.solPubKey;
        return { tx, blockhash, lastValidBlockHeight };
    }

    /**
     * Guard for all write operations (deposit/withdraw/stake/unstake/buy).
     * Ensures we have a Wire pubKey and an Anchor wallet pubKey, and that they match.
     */
    ensureUser() {
        if (!this.pubKey || !this.anchor.wallet.publicKey) {
            throw new Error('User Authorization required: pubKey is undefined');
        }
        if (
            this.solPubKey.toBase58() !==
            this.anchor.wallet.publicKey.toBase58()
        ) {
            throw new Error(
                'Write access requires connected wallet to match pubKey',
            );
        }
    }

}