import { AnchorProvider, BN, Program } from '@coral-xyz/anchor';
import {
    PublicKey,
    SystemProgram,
    TransactionInstruction,
} from '@solana/web3.js';
import {
    ASSOCIATED_TOKEN_PROGRAM_ID,
    TOKEN_2022_PROGRAM_ID,
} from '@solana/spl-token';

import { LiqsolCore } from '../../../assets/solana/devnet/types/liqsol_core';

import {
    OutpostAccounts,
    buildOutpostAccounts,
} from '../utils';

import {
    OutpostWireStateSnapshot,
    SharesPreview,
    TrancheState,
    WalletLike,
} from '../types';
import { INDEX_SCALE } from '../constants';
import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';

/**
 * OutpostClient
 *
 * Read-only + preview helper for:
 *  - Outpost config + per-user pretokens
 *  - Tranche ladder snapshot
 *  - Pretoken preview math
 *
 * Write helpers:
 *  - buildStakeIx   → liqsol_core::synd
 *  - buildUnstakeIx → liqsol_core::desynd
 *
 * All PDAs / ATAs are sourced via buildOutpostAccounts, which is kept
 * in sync with the on-chain PDAs and capital-staking Wire utils.
 */
export class OutpostClient {
    private readonly program: Program<LiqsolCoreClientIdl>;

    constructor(
        private readonly provider: AnchorProvider,
        private readonly pgs: SolanaProgramService,
    ) {
        this.program = pgs.getProgram('liqsolCore');
    }

    get connection() {
        return this.provider.connection;
    }

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

    /**
     * Build all outpost-related PDAs / ATAs for a given user.
     * Defaults to the connected wallet.
     */
    async buildAccounts(user?: PublicKey): Promise<OutpostAccounts> {
        const userPk = user ?? this.wallet.publicKey;
        if (!userPk) {
            throw new Error('OutpostClient.buildAccounts: wallet not connected');
        }
        return buildOutpostAccounts(this.connection, userPk, this.pgs);
    }

    /**
     * Internal helper: get raw token balance (BN) for a given ATA.
     */
    private async getTokenBalance(ata: PublicKey): Promise<BN> {
        try {
            const bal = await this.connection.getTokenAccountBalance(ata);
            return new BN(bal.value.amount);
        } catch {
            return new BN(0);
        }
    }

    /**
     * Fetch the core "Wire state" for Outpost / pretokens for a given user.
     */
    async fetchWireState(user?: PublicKey): Promise<OutpostWireStateSnapshot> {
        const userPk = user ?? this.wallet.publicKey;
        if (!userPk) {
            throw new Error('OutpostClient.fetchWireState: wallet not connected');
        }

        const pdas = await this.buildAccounts(userPk);

        try {
            const [
                globalState,
                outpostAccount,
                distributionState,
                userPretokenRecord,
                trancheState,
            ] = await Promise.all([
                this.program.account.globalState.fetch(pdas.globalState),
                this.program.account.outpostAccount.fetchNullable(pdas.outpostAccount),
                this.program.account.distributionState.fetchNullable(pdas.distributionState),
                this.program.account.userPretokenRecord.fetchNullable(pdas.userPretokenRecord),
                this.program.account.trancheState.fetchNullable(pdas.trancheState),
            ]);

            const [liqsolPoolBalance, userLiqsolBalance] = await Promise.all([
                this.getTokenBalance(pdas.liqsolPoolAta),
                this.getTokenBalance(pdas.userAta),
            ]);

            return {
                globalState,
                outpostAccount,
                distributionState,
                trancheState,
                userPretokenRecord,
                liqsolPoolBalance,
                userLiqsolBalance,
            };
        }
        catch (err) {
            console.error('Error fetching Outpost wire state:', err);
            throw err;
        }
    }

    // -------------------------------------------------------------------------
    // Outpost stake / unstake builders (synd / desynd)
    // -------------------------------------------------------------------------

    /**
     * Build the `synd` (stake) instruction:
     *   user liqSOL ATA → liqSOL pool ATA
     */
    async buildStakeIx(
        amountLamports: bigint,
        user: PublicKey = this.wallet.publicKey,
    ): Promise<TransactionInstruction> {
        if (!user) {
            throw new Error('OutpostClient.buildStakeIx: wallet not connected');
        }

        const a = await this.buildAccounts(user);

        return this.program.methods
            .synd(new BN(amountLamports.toString()))
            .accounts({
                user,
                liqsolMint: a.liqsolMint,
                globalState: a.globalState,
                distributionState: a.distributionState,
                userAta: a.userAta,
                poolAuthority: a.poolAuthority,
                bucketAuthority: a.bucketAuthority,
                bucketTokenAccount: a.bucketTokenAccount,
                bucketUserRecord: a.bucketUserRecord,
                senderUserRecord: a.userUserRecord,
                receiverUserRecord: a.liqsolPoolUserRecord,
                extraAccountMetaList: a.extraAccountMetaList,
                liqsolCoreProgram: this.pgs.PROGRAM_IDS.LIQSOL_CORE,
                transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                liqsolPoolAta: a.liqsolPoolAta,
                outpostAccount: a.outpostAccount,
                tokenProgram: TOKEN_2022_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                systemProgram: SystemProgram.programId,
                pretokenPurchaseHistory: a.pretokenPurchaseHistory,
            })
            .instruction();
    }

    /**
     * Build the `desynd` (unstake) instruction:
     *   liqSOL pool ATA → user liqSOL ATA
     */
    async buildUnstakeIx(
        amountLamports: bigint,
        user?: PublicKey,
    ): Promise<TransactionInstruction> {
        const userPk = user ?? this.wallet.publicKey;
        if (!userPk) {
            throw new Error('OutpostClient.buildUnstakeIx: wallet not connected');
        }

        const a = await this.buildAccounts(userPk);

        return this.program.methods
            .desynd(new BN(amountLamports.toString()))
            .accounts({
                user: userPk,
                liqsolMint: a.liqsolMint,
                globalState: a.globalState,
                distributionState: a.distributionState,
                userAta: a.userAta,
                poolAuthority: a.poolAuthority,
                bucketAuthority: a.bucketAuthority,
                bucketTokenAccount: a.bucketTokenAccount,
                bucketUserRecord: a.bucketUserRecord,
                senderUserRecord: a.liqsolPoolUserRecord,
                receiverUserRecord: a.userUserRecord,
                extraAccountMetaList: a.extraAccountMetaList,
                liqsolCoreProgram: this.pgs.PROGRAM_IDS.LIQSOL_CORE,
                transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                liqsolPoolAta: a.liqsolPoolAta,
                outpostAccount: a.outpostAccount,
                tokenProgram: TOKEN_2022_PROGRAM_ID,
                systemProgram: SystemProgram.programId,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                pretokenPurchaseHistory: a.pretokenPurchaseHistory,
            })
            .instruction();
    }

    // -------------------------------------------------------------------------
    // Pretoken preview helper (unchanged)
    // -------------------------------------------------------------------------

    /**
     * Local preview for "how many pretokens would I get for this liqSOL
     * amount at the current tranche price and SOL/USD price?"
     *
     *   SCALE = 1e8
     *   usdValue = solAmountLamports * solPriceUsd / 1e9
     *   pretokens = usdValue * SCALE / pretokenPriceUsd
     *
     * All values are BN integers; solPriceUsd and pretokenPriceUsd are 1e8 scaled.
     */
    previewPretokens(params: {
        liqsolAmountLamports: BN;
        solPriceUsd: BN; // 1e8 scale
        trancheState: TrancheState;
    }): SharesPreview {
        const { liqsolAmountLamports, solPriceUsd, trancheState } = params;

        // Current tranche price in USD (1e8 scale)
        const pretokenPriceUsd = new BN(
            trancheState.currentTranchePriceUsd.toString(),
        );

        const SCALE = new BN('100000000'); // 1e8

        // usdValue = (lamports * solPriceUsd) / 1e9
        const usdValue = liqsolAmountLamports
            .mul(solPriceUsd)
            .div(new BN(1_000_000_000));

        const estimatedPretokens = usdValue.mul(SCALE).div(pretokenPriceUsd);

        return {
            liqsolAmountLamports,
            solPriceUsd,
            pretokenPriceUsd,
            estimatedPretokens,
        };
    }

    /**
     * Convert liqSOL tokens (lamports) → Outpost “shares”, rounding UP
     * when there is any remainder.
     *
     * This matches the on-chain logic and the capital-staking helper:
     *
     *   INDEX_SCALE = 1e12
     *   numerator   = amount * INDEX_SCALE
     *   shares      = numerator / currentIndex
     *   remainder   = numerator % currentIndex
     *   if remainder > 0 → shares + 1
     */
    static tokensToShares(amount: BN, currentIndex: BN): BN {
        const numerator = amount.mul(new BN(INDEX_SCALE));
        const shares = numerator.div(currentIndex);
        const remainder = numerator.mod(currentIndex);
        return remainder.eqn(0) ? shares : shares.addn(1);
    }

    /**
     * Format a 1e8-scaled integer (e.g. USD price, pretokens, etc.)
     * into a fixed 8-decimal string.
     *
     * Examples:
     *   0          -> "0.00000000"
     *   1          -> "0.00000001"
     *   100000000  -> "1.00000000"
     *   123456789  -> "1.23456789"
     */
    static formatScale8(value: BN): string {
        const decimals = 8;
        const valueStr = value.toString();

        if (valueStr.length <= decimals) {
            const padding = '0'.repeat(decimals - valueStr.length);
            return `0.${padding}${valueStr}`;
        }

        const intPart = valueStr.slice(0, valueStr.length - decimals);
        const decPart = valueStr.slice(valueStr.length - decimals);
        return `${intPart}.${decPart}`;
    }
}