import {
    Commitment,
    ComputeBudgetProgram,
    Connection,
    ConnectionConfig,
    PublicKey as SolPubKey,
    Transaction,
    TransactionSignature,
} from '@solana/web3.js';
import { AnchorProvider } 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,
    deriveVaultPda,
    INDEX_SCALE,
} from './constants';

import { buildSolanaTrancheSnapshot } from './utils';
import { SolanaTransaction } from './types';

const commitment: Commitment = 'confirmed';

/**
 * 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;

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

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

        const tx = await this.depositClient.buildDepositTx(amountLamports);
        const { tx: prepared, blockhash, lastValidBlockHeight } =
            await this.prepareTx(tx);
        const signed = await this.signTransaction(prepared);
        return await this.sendAndConfirmHttp(signed, {
            blockhash,
            lastValidBlockHeight,
        });
    }

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

        const tx = await this.depositClient.buildWithdrawTx(amountLamports);
        const { tx: prepared, blockhash, lastValidBlockHeight } =
            await this.prepareTx(tx);
        const signed = await this.signTransaction(prepared);
        return await this.sendAndConfirmHttp(signed, {
            blockhash,
            lastValidBlockHeight,
        });
    }

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

        const user = this.solPubKey;

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

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

        return this.sendAndConfirmHttp(signed, prepared);
    }

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

        const user = this.solPubKey;

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

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

        return this.sendAndConfirmHttp(signed, prepared);
    }

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

        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 { tx: prepared, blockhash, lastValidBlockHeight } =
            await this.prepareTx(tx);
        const signed = await this.signTransaction(prepared);
        return await this.sendAndConfirmHttp(signed, {
            blockhash,
            lastValidBlockHeight,
        });
    }

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

        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)
        //  - estimatedYieldLiqsol: 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 estimatedClaimLiqsol = BigInt(0);
        let estimatedYieldLiqsol = BigInt(0);

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

            if (estimatedClaimLiqsol > stakedLiqsol) {
                estimatedYieldLiqsol = estimatedClaimLiqsol - 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
                estimatedClaimLiqsol,
                estimatedYieldLiqsol,
            },
            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,
        };
    }

    /**
     * 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
    // ---------------------------------------------------------------------

    // Estimated total APY for staking yeild
    getSystemAPY(): Promise<number> {
        // TODO
        return Promise.resolve(0);
    }

    // Protocol fee charged for deposit from Native to LIQ
    getDepositFee(amount: bigint): Promise<bigint> {
        // TODO
        return Promise.resolve(BigInt(0));
    }

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

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