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

import * as multisig from "@sqds/multisig";

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

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

import { ConvertClient } from './clients/convert.client';
import { DistributionClient } from './clients/distribution.client';
import { SolanaInstaswapClient } from './clients/instaswap.client';
import { LeaderboardClient } from './clients/leaderboard.client';
import { OutpostClient } from './clients/outpost.client';
import { TokenClient } from './clients/token.client';

import {
    DEFAULT_AVERAGE_PAY_RATE,
    INDEX_SCALE,
    PAY_RATE_SCALE_FACTOR,
} from './constants';

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

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 convertClient: ConvertClient;
    public distributionClient: DistributionClient;
    public instaswapClient: SolanaInstaswapClient;
    public leaderboardClient: LeaderboardClient;
    public outpostClient: OutpostClient;
    public tokenClient: TokenClient;
    public program: SolanaProgramService

    private smartAccount?: SolPubKey; // PDA (off-curve)
    private signer?: SolPubKey;       // on-curve signer

    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;
    }

    get feePayer(): SolPubKey {
        if (this.signer) return this.signer;
        // fallback for normal wallets
        if (this.anchor.wallet.publicKey) return this.anchor.wallet.publicKey;
        throw new Error('No signing authority available');
    }

    get squadsX(): SquadsXConfig | null {
        const config = this.config.extras?.squadsX;
        return config ?? null;
    }

    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.program = new SolanaProgramService(this.anchor, config.network.chainId as SolChainID);

        this.convertClient = new ConvertClient(this.anchor, this.program);
        this.distributionClient = new DistributionClient(this.anchor, this.program);
        this.instaswapClient = new SolanaInstaswapClient(
            this.anchor,
            this.program,
            this.buildAndSendIx.bind(this),
        );
        this.leaderboardClient = new LeaderboardClient(this.anchor, this.program);
        this.outpostClient = new OutpostClient(this.anchor, this.program);
        this.tokenClient = new TokenClient(this.anchor, this.program);
    }

    // ---------------------------------------------------------------------
    // 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 {
            const user = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
            const submit = async (ix: TransactionInstruction | TransactionInstruction[]) => (
                !!this.squadsX
                    ? await this.sendSquadsIxs(ix)
                    : await this.buildAndSendIx(ix)
            );

            try {
                const ix = await this.convertClient.buildDepositTx(amountLamports, user);
                return await submit(ix);
            } catch (err) {
                if (!this.isLegacyUserRecordMigrationRequired(err)) {
                    throw err;
                }

                const registerIx = await this.convertClient.buildRegisterUserIx(user);
                await submit(registerIx);

                const retryIx = await this.convertClient.buildDepositTx(amountLamports, user);
                return await submit(retryIx);
            }

        } catch (err) {
            console.log(`Failed to deposit Solana: ${err}`);
            throw 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 {
            const ix = await this.convertClient.buildWithdrawTx(amountLamports, this.squadsVaultPDA)
            return !!this.squadsX
                ? await this.sendSquadsIxs(ix)
                : await this.buildAndSendIx(ix)

        } catch (err) {
            console.log(`Failed to withdraw Solana: ${err}`);
            throw err;
        }
    }

    /**
     * Enumerate withdrawal receipt NFTs held by the user (queued/ready/claimed).
     * Mirrors the ETH getPendingWithdraws helper for UI parity.
     */
    async getPendingWithdraws(address?: string): Promise<WithdrawReceipt[]> {
        this.ensureUser();
        const owner = address ? new SolPubKey(address) :
            this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
        return await this.convertClient.fetchWithdrawReceipts(owner);
    }

    /**
     * Claim a withdrawal receipt (burn NFT + receive SOL) via claim_withdraw.
     */
    async claimWithdraw(tokenId: bigint): Promise<string> {
        this.ensureUser();
        const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;

        try {
            const ix = await this.convertClient.buildClaimWithdrawTx(tokenId, owner);
            return !!this.squadsX
                ? await this.sendSquadsIxs(ix)
                : await this.buildAndSendIx(ix);
        } catch (err) {
            console.log(`Failed to claim withdraw on Solana: ${err}`);
            throw err;
        }
    }

    getLiqsolDestinationOwner(owner?: SolPubKey): SolPubKey {
        return owner ?? this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
    }

    getLiqsolDestinationAta(owner?: SolPubKey): SolPubKey {
        const destinationOwner = this.getLiqsolDestinationOwner(owner);
        const mint = this.program.deriveLiqsolMintPda();
        return getAssociatedTokenAddressSync(
            mint,
            destinationOwner,
            true,
            TOKEN_2022_PROGRAM_ID,
            ASSOCIATED_TOKEN_PROGRAM_ID,
        );
    }

    async getLiqsolDestinationAtaState(owner?: SolPubKey): Promise<{
        owner: SolPubKey;
        mint: SolPubKey;
        ata: SolPubKey;
        exists: boolean;
    }> {
        const destinationOwner = this.getLiqsolDestinationOwner(owner);
        const mint = this.program.deriveLiqsolMintPda();
        const ata = this.getLiqsolDestinationAta(destinationOwner);
        const accountInfo = await this.connection.getAccountInfo(ata, 'confirmed');

        return {
            owner: destinationOwner,
            mint,
            ata,
            exists: !!accountInfo,
        };
    }

    async maybeBuildCreateLiqsolDestinationAtaIx(
        owner?: SolPubKey,
    ): Promise<TransactionInstruction | null> {
        const state = await this.getLiqsolDestinationAtaState(owner);
        if (state.exists) {
            return null;
        }

        return createAssociatedTokenAccountInstruction(
            this.feePayer,
            state.ata,
            state.owner,
            state.mint,
            TOKEN_2022_PROGRAM_ID,
            ASSOCIATED_TOKEN_PROGRAM_ID,
        );
    }

    async ensureLiqsolDestinationAta(owner?: SolPubKey): Promise<string | null> {
        this.ensureUser();
        const createAtaIx = await this.maybeBuildCreateLiqsolDestinationAtaIx(owner);
        if (!createAtaIx) {
            return null;
        }

        try {
            return await this.buildAndSendIx(createAtaIx);
        } catch (error) {
            if (!this.isAtaAlreadyCreatedError(error)) {
                throw error;
            }
            return null;
        }
    }

    private async prependLiqsolDestinationAtaIx(
        ix: TransactionInstruction | TransactionInstruction[],
        owner?: SolPubKey,
    ): Promise<TransactionInstruction[]> {
        const ixs = Array.isArray(ix) ? [...ix] : [ix];
        const createAtaIx = await this.maybeBuildCreateLiqsolDestinationAtaIx(owner);
        return createAtaIx ? [createAtaIx, ...ixs] : ixs;
    }

    /**
     * Claim accrued liqSOL distribution rewards (liqsol_core::claim_rewards).
     */
    async claimLiqsolRewards(): Promise<string> {
        this.ensureUser();
        const owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;

        try {
            const ix = await this.distributionClient.buildClaimRewardsIx(owner);
            const ixs = await this.prependLiqsolDestinationAtaIx(ix, owner);
            return !!this.squadsX
                ? await this.sendSquadsIxs(ixs)
                : await this.buildAndSendIx(ixs);
        } catch (err) {
            console.log(`Failed to claim liqSOL rewards on Solana: ${err}`);
            throw err;
        }
    }

    /**
     * Stake liqSOL into Outpost (liqSOL → pool) via liqsol_core::synd.
     */
    async stake(amountLamports: bigint, wireAccount?: string): Promise<string> {
        this.ensureUser();
        if (!amountLamports || amountLamports <= BigInt(0))
            throw new Error('Stake amount must be greater than zero.');

        try {
            const ix = await this.outpostClient.buildStakeIx(
                amountLamports,
                this.squadsVaultPDA,
                wireAccount,
            )
            return !!this.squadsX
                ? await this.sendSquadsIxs(ix)
                : await this.buildAndSendIx(ix)

        } catch (err) {
            console.log(`Failed to stake Solana: ${err}`);
            throw 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 owner = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
            const ix = await this.outpostClient.buildUnstakeIx(amountLamports, this.squadsVaultPDA)
            const ixs = await this.prependLiqsolDestinationAtaIx(ix, owner);
            return !!this.squadsX
                ? await this.sendSquadsIxs(ixs)
                : await this.buildAndSendIx(ixs)
        }
        catch (err) {
            console.log(`Failed to unstake Solana: ${err}`);
            throw 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 ix = await this.tokenClient.buildPurchaseIx(amountLamports, this.squadsVaultPDA)
            return !!this.squadsX
                ? await this.sendSquadsIxs(ix)
                : await this.buildAndSendIx(ix)
        }
        catch (err) {
            console.log(`Failed to buy liqSOL pretokens: ${err}`);
            throw err;
        }
    }

    /** Deposit SOL to liqSOL, then syndicate that liqSOL in one transaction. Requires enough SOL for the deposit fee. */
    async depositAndStake(amountLamports: bigint, wireAccount?: string): Promise<string> {
        this.ensureUser();
        if (!amountLamports || amountLamports <= BigInt(0))
            throw new Error('Amount must be greater than zero.');

        try {
            const user = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
            const depositIx = await this.convertClient.buildDepositTx(amountLamports, user);
            const stakeIx = await this.outpostClient.buildStakeIx(
                amountLamports,
                user,
                wireAccount,
            );

            return !!this.squadsX
                ? await this.sendSquadsIxs([depositIx, stakeIx])
                : await this.buildAndSendIx([depositIx, stakeIx]);
        } catch (err) {
            console.log(`Failed to deposit and stake: ${err}`);
            throw err;
        }
    }

    /** Deposit SOL to liqSOL, then buy pretokens with that liqSOL in one transaction. Requires enough SOL for the deposit fee. */
    async depositAndBuy(amountLamports: bigint): Promise<string> {
        this.ensureUser();
        if (!amountLamports || amountLamports <= BigInt(0))
            throw new Error('Amount must be greater than zero.');

        try {
            const user = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
            const depositIx = await this.convertClient.buildDepositTx(amountLamports, user);
            const purchaseIx = await this.tokenClient.buildPurchaseIx(amountLamports, user);

            return !!this.squadsX
                ? await this.sendSquadsIxs([depositIx, purchaseIx])
                : await this.buildAndSendIx([depositIx, purchaseIx]);
        } catch (err) {
            console.log(`Failed to deposit and buy: ${err}`);
            throw err;
        }
    }

    /** Desyndicate liqSOL, then request withdraw in one transaction. */
    async unstakeAndWithdraw(amountLamports: bigint): Promise<string> {
        this.ensureUser();
        if (!amountLamports || amountLamports <= BigInt(0))
            throw new Error('Amount must be greater than zero.');

        try {
            const user = this.squadsVaultPDA ?? this.anchor.wallet.publicKey;
            const unstakeIx = await this.outpostClient.buildUnstakeIx(amountLamports, user);
            const withdrawIx = await this.convertClient.buildWithdrawTx(amountLamports, user);
            const ixs = await this.prependLiqsolDestinationAtaIx([unstakeIx, withdrawIx], user);

            return !!this.squadsX
                ? await this.sendSquadsIxs(ixs)
                : await this.buildAndSendIx(ixs);
        } catch (err) {
            console.log(`Failed to unstake and withdraw: ${err}`);
            throw 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(address?: string): Promise<Portfolio> {
        // if (!this.pubKey) throw new Error('User pubKey is undefined');
        try {
            const user = address ? new SolPubKey(address) : 
                !!this.squadsX ? this.squadsVaultPDA! : this.solPubKey;

            const reservePoolPDA = this.program.deriveReservePoolPda();
            const vaultPDA = this.program.deriveVaultPda();
            const liqsolMint = this.program.deriveLiqsolMintPda();

            const userLiqsolAta = getAssociatedTokenAddressSync(
                liqsolMint,
                user,
                true,
                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, claimableLamports] = await Promise.all([
                this.connection.getBalance(user, 'confirmed'),
                this.connection
                    .getTokenAccountBalance(userLiqsolAta, 'confirmed')
                    .catch(() => null),
                this.outpostClient.fetchWireState(user).catch(() => null),
                this.distributionClient.getClaimableLiqsol(user).catch(() => new BN(0)),
            ]);

            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,
                },
                claimable: {
                    amount: BigInt(claimableLamports.toString()),
                    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) {
            console.log(`Failed to get Solana portfolio: ${err}`);
            throw 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);
    }

    // ---------------------------------------------------------------------
    // SquadsX Helpers
    // ---------------------------------------------------------------------

    get squadsMultisigPDA(): SolPubKey | undefined {
        if (!this.squadsX) return undefined;
        return new SolPubKey(this.squadsX.multisigPDA);
    }

    get squadsVaultPDA(): SolPubKey | undefined {
        if (!this.squadsX || !this.squadsMultisigPDA) return undefined;
        const multisigPda = this.squadsMultisigPDA;
        const index = this.squadsX.vaultIndex ?? 0;
        const pda = multisig.getVaultPda({ multisigPda, index });
        return pda[0];
    }

    /** Create a Squads vault transaction/proposal for one or more instructions. */
    private async sendSquadsIxs(ix: TransactionInstruction | TransactionInstruction[]): Promise<string> {
        if (!this.squadsX) throw new Error('Attempting to wrap Squads instruction without SquadsX config');

        const ixs = Array.isArray(ix) ? ix : [ix];
        const multisigPda = this.squadsMultisigPDA!;
        const vaultPda = this.squadsVaultPDA!;
        const vaultIndex = this.squadsX?.vaultIndex ?? 0;
        const creator = this.solPubKey;

        const ms = await multisig.accounts.Multisig.fromAccountAddress(this.connection, multisigPda);
        const current = BigInt(ms.transactionIndex?.toString() ?? 0);
        const transactionIndex = current + BigInt(1);

        const altAddress = this.program.PROGRAM_IDS.ALT;
        const altAccount = await this.connection.getAddressLookupTable(altAddress);
        if (!altAccount.value) throw new Error("ALT not found on-chain or not yet active.");

        const lookupTable: AddressLookupTableAccount = altAccount.value;
        const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 });
        const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 2000 });

        const { blockhash } = await this.connection.getLatestBlockhash("confirmed");
        const transactionMessage = new TransactionMessage({
            payerKey: vaultPda,
            recentBlockhash: blockhash,
            instructions: [computeLimitIx, computePriceIx, ...ixs],
        });

        const createVaultTxIx = await multisig.instructions.vaultTransactionCreate({
            multisigPda,
            transactionIndex,
            creator,
            vaultIndex,
            transactionMessage,
            ephemeralSigners: 0,
            addressLookupTableAccounts: [lookupTable],
        });

        const vaultTransactionCreate = await this.buildAndSendIx(createVaultTxIx)
        console.log('SQUADSX: vaultTransactionCreate', vaultTransactionCreate);

        const createProposalIx = await multisig.instructions.proposalCreate({
            multisigPda,
            transactionIndex,
            creator,
        });

        const proposalCreate = await this.buildAndSendIx(createProposalIx)
        console.log('SQUADSX: proposalCreate', proposalCreate);

        return proposalCreate;
    }

    // async finish(multisigPda: string, transactionIndex: number): Promise<void> {
    //     const vaultExecute = await multisig.instructions.vaultTransactionExecute({
    //         connection: this.connection,
    //         multisigPda: new SolPubKey(multisigPda),
    //         transactionIndex: BigInt(transactionIndex),
    //         member: this.solPubKey
    //     });

    //     const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
    //     const tx1 = new Transaction().add(cuIx, vaultExecute.instruction);
    //     const prepared1 = await this.prepareTx(tx1);
    //     const signed1 = await this.signTransaction(prepared1.tx);
    //     const sent1 = await this.sendAndConfirmHttp(signed1, prepared1);
    //     console.log('SENT FINISH', sent1);
    // }

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

    async buildAndSendIx(ix: TransactionInstruction | TransactionInstruction[]): Promise<string> {
        const ixs = Array.isArray(ix) ? ix : [ix];
        const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 });
        const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 2000 });
        const { blockhash, lastValidBlockHeight } =
            await this.connection.getLatestBlockhash('confirmed');

        const altAddress = this.program.PROGRAM_IDS.ALT;
        const altAccount = await this.connection.getAddressLookupTable(altAddress);
        const lookupTable = altAccount.value;

        const instructions = [computeLimitIx, computePriceIx, ...ixs];

        const tx = lookupTable
            ? new VersionedTransaction(
                new TransactionMessage({
                    payerKey: this.feePayer,
                    recentBlockhash: blockhash,
                    instructions,
                }).compileToV0Message([lookupTable]),
            )
            : new Transaction().add(...instructions);

        const prepared =
            tx instanceof Transaction
                ? await this.prepareTx(tx)
                : { tx, blockhash, lastValidBlockHeight };

        const signed = await this.signTransaction(prepared.tx);

        return this.sendAndConfirmHttp(signed, prepared);
    }


    /**
     * Send a signed transaction over HTTP RPC.
     *
     * - Returns immediately on successful sendRawTransaction.
     * - If sendRawTransaction throws a SendTransactionError with
     *   "already been processed", we treat it as success and
     *   just return a derived signature.
     * - No confirmTransaction / polling / blockheight handling.
     */
    private async sendAndConfirmHttp(
        signed: SolanaTransaction,
        _ctx: { blockhash: string; lastValidBlockHeight: number },
    ): Promise<string> {
        this.ensureUser();

        const rawTx = signed.serialize();

        try {
            // Normal happy path: RPC accepts the tx and returns a signature
            const signature = await this.connection.sendRawTransaction(rawTx, {
                skipPreflight: false,
                preflightCommitment: commitment,
                maxRetries: 3,
            });
            return signature;
        } catch (e: any) {
            const msg = e?.message ?? '';
            const isSendTxError =
                e instanceof SendTransactionError || e?.name === 'SendTransactionError';

            // Benign duplicate case: tx is already in the ledger / cache
            if (isSendTxError && msg.includes('already been processed')) {
                console.warn(
                    'sendRawTransaction reports "already been processed"; ' +
                    'treating as success without further confirmation.',
                );

                // Try to derive a signature from the signed Transaction.
                // If SolanaTransaction is a legacy Transaction, this works.
                const legacy = signed as unknown as Transaction;
                const first = legacy.signatures?.[0]?.signature;

                if (first) {
                    return bs58.encode(first);
                }

                // Fallback: return a dummy string
                return 'already-processed';
            }

            // Any other send error is a real failure
            throw e;
        }
    }

    /**
     * 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.feePayer;
        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) throw new Error('User pubKey is undefined');

        const wallet = this.anchor?.wallet as any;
        const pk = wallet?.publicKey as SolPubKey | undefined;

        if (!pk) throw new Error('Wallet not connected');
        if (typeof wallet.signTransaction !== 'function') {
            throw new Error('Wallet does not support signTransaction');
        }

        // 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',
        //     );
        // }
    }

    private isLegacyUserRecordMigrationRequired(error: unknown): boolean {
        const message = error instanceof Error ? error.message : String(error ?? '');
        const normalized = message.toLowerCase();
        return normalized.includes('legacyuserrecordmigrationrequired')
            || normalized.includes('legacy user record must be migrated')
            || normalized.includes('custom program error: 0x1784');
    }

    private isAtaAlreadyCreatedError(error: unknown): boolean {
        const message = error instanceof Error ? error.message : String(error ?? '');
        const normalized = message.toLowerCase();
        return normalized.includes('already in use')
            || normalized.includes('already exists');
    }

    // ---------------------------------------------------------------------
    // 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(chainID: ChainID): Promise<TrancheSnapshot> {
        try {
            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,
            });
        }
        catch (err) {
            console.log(`Failed to build Solana tranche snapshot: ${err}`);
            throw 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) {
            console.log(`Failed to compute Solana system APY: ${err}`);
            throw 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> {
        const liqSolCoreProgram: any = this.program.getProgram('liqsolCore');
        const payRateHistoryPda = this.program.derivePayRateHistoryPda();

        const payRateHistory =
            await liqSolCoreProgram.account.payRateHistory.fetch(payRateHistoryPda);

        const totalEntriesAdded = Number(payRateHistory.totalEntriesAdded);
        const maxEntries = Number(payRateHistory.maxEntries);

        if (totalEntriesAdded === 0) {
            // No real entries yet, fall back to default
            return Number(DEFAULT_AVERAGE_PAY_RATE) / Number(PAY_RATE_SCALE_FACTOR);
        }

        const currentIndex = Number(payRateHistory.currentIndex);

        // Only average entries that have actually been processed (written by add_entry),
        // not the default-initialized slots. After 10+ entries, processedCount === maxEntries.
        const processedCount = Math.min(totalEntriesAdded, maxEntries);

        let sum = BigInt(0);
        let validCount = 0;
        // Walk backward from most recent entry
        let idx = (currentIndex - 1 + maxEntries) % maxEntries;

        for (let i = 0; i < processedCount; i++) {
            const entry = payRateHistory.entries[idx];
            const scaledRate = BigInt(entry.scaledRate.toString());
            if (scaledRate > BigInt(0)) {
                sum += scaledRate;
                validCount++;
            }
            idx = (idx - 1 + maxEntries) % maxEntries;
        }

        if (validCount === 0) {
            return Number(DEFAULT_AVERAGE_PAY_RATE) / Number(PAY_RATE_SCALE_FACTOR);
        }

        // Average the processed entries
        const average = Number(sum / BigInt(validCount));
        return average / Number(PAY_RATE_SCALE_FACTOR);
    }

    // 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 | null] = 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();

        // -------------------------------------------------------------
        // 1) Current wallet balance (prefer caller override)
        // -------------------------------------------------------------
        const balanceLamports: bigint =
            options?.balanceOverrideLamports ??
            BigInt(await this.connection.getBalance(this.feePayer, 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.feePayer;

        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;
    }


}
