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 {
    OutpostAccounts,
    buildOutpostAccounts,
    safeFetch,
} from '../utils';

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

export function buildSolanaSyndicateAccounts(params: {
    user: PublicKey;
    accounts: OutpostAccounts;
    liqsolCoreProgram: PublicKey;
    transferHookProgram: PublicKey;
}) {
    const { user, accounts, liqsolCoreProgram, transferHookProgram } = params;

    return {
        user,
        liqsolMint: accounts.liqsolMint,
        distributionState: accounts.distributionState,
        globalState: accounts.globalState,
        userAta: accounts.userAta,
        senderUserRecord: accounts.userUserRecord,
        bridgeAuthority: accounts.bridgeAuthority,
        bridgeVaultAta: accounts.bridgeVaultAta,
        bridgeUserRecord: accounts.bridgeUserRecord,
        bridgeState: accounts.bridgeState,
        outpostAccount: accounts.outpostAccount,
        extraAccountMetaList: accounts.extraAccountMetaList,
        liqsolCoreProgram,
        transferHookProgram,
        bucketAuthority: accounts.bucketAuthority,
        bucketTokenAccount: accounts.bucketTokenAccount,
        bucketUserRecord: accounts.bucketUserRecord,
        tokenProgram: TOKEN_2022_PROGRAM_ID,
        associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
        globalConfig: accounts.globalConfig,
        oppEpochState: accounts.oppEpochState,
        pendingAttestations: accounts.pendingAttestations,
    };
}

/**
 * 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 (error) {
            if (this.isMissingTokenAccountError(error)) {
                return new BN(0);
            }
            throw error;
        }
    }

    private isMissingTokenAccountError(error: unknown): boolean {
        const message = String(
            (error as { message?: string } | null)?.message ?? '',
        ).toLowerCase();

        return (
            message.includes('failed to get token account balance') &&
            message.includes('could not find account')
        );
    }

    /**
     * Fetch an OutpostAccount, handling legacy accounts that pre-date the
     * three trailing Option<u64> fields.  If the normal decode throws a
     * RangeError we re-fetch the raw data, pad it to the expected size
     * (zeros → Option::None for each missing field), and let Anchor's
     * coder decode the padded buffer.
     */
    private async fetchOutpostAccount(address: PublicKey) {
        const program = this.program;
        try {
            return await program.account.outpostAccount.fetchNullable(address);
        } catch (e) {
            if (!(e instanceof RangeError)) throw e;

            const info = await this.connection.getAccountInfo(address);
            if (!info) return null;

            // 3 × Option<u64> encoded as 1-byte tag (0x00 = None) = 3 bytes
            // when present (Some) each is 9 bytes, but None is just 0x00
            const EXPECTED_SIZE = info.data.length + 3;
            const padded = Buffer.alloc(EXPECTED_SIZE);
            info.data.copy(padded);
            // trailing bytes are already 0x00 → Option::None for each field

            return program.coder.accounts.decode(
                'outpostAccount',
                padded,
            );
        }
    }

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

        const pdas = await this.buildAccounts(userPk);
        const [
            globalState,
            outpostAccount,
            distributionState,
            userPretokenRecord,
            trancheState,
            liqsolPoolBalance,
            userLiqsolBalance,
        ] = await Promise.all([
            safeFetch(program.account.globalState.fetch(pdas.globalState), 'globalState'),
            safeFetch(this.fetchOutpostAccount(pdas.outpostAccount), 'outpostAccount'),
            safeFetch(program.account.distributionState.fetchNullable(pdas.distributionState), 'distributionState'),
            safeFetch(program.account.userPretokenRecord.fetchNullable(pdas.userPretokenRecord), 'userPretokenRecord'),
            safeFetch(program.account.trancheState.fetchNullable(pdas.trancheState), 'trancheState'),
            safeFetch(this.getTokenBalance(pdas.liqsolPoolAta), 'liqsolPoolAta'),
            safeFetch(this.getTokenBalance(pdas.userAta), 'userAta'),
        ]);

        return {
            globalState: globalState as GlobalState | null,
            outpostAccount: outpostAccount as OutpostAccount | null,
            distributionState: distributionState as DistributionState | null,
            trancheState: trancheState as TrancheState | null,
            userPretokenRecord: userPretokenRecord as UserPretokenRecord | null,
            liqsolPoolBalance,
            userLiqsolBalance,
        };
    }



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

    /**
     * Build the `syndicate_liqsol_to_wire` (stake) instruction:
     *   user liqSOL ATA → liqSOL pool ATA, bridged to the Wire chain.
     *
     * @param amountLamports  Amount of liqSOL to stake (in lamports / base units).
     * @param user            Solana signer. Defaults to the connected wallet.
     * @param wireAccount     Wire chain recipient account name. Required.
     */
    async buildStakeIx(
        amountLamports: bigint,
        user: PublicKey = this.wallet.publicKey,
        wireAccount?: string,
    ): Promise<TransactionInstruction> {
        const program = this.program;
        if (!user) {
            throw new Error('OutpostClient.buildStakeIx: wallet not connected');
        }
        if (!wireAccount?.trim()) {
            throw new Error('OutpostClient.buildStakeIx: wireAccount is required');
        }

        const a = await this.buildAccounts(user);

        return program.methods
            .syndicateLiqsolToWire(wireAccount.trim(), new BN(amountLamports.toString()))
            .accounts(
                buildSolanaSyndicateAccounts({
                    user,
                    accounts: a,
                    liqsolCoreProgram: this.pgs.PROGRAM_IDS.LIQSOL_CORE,
                    transferHookProgram: this.pgs.PROGRAM_IDS.TRANSFER_HOOK,
                }),
            )
            .instruction();
    }

    /**
     * Build the `desynd` (unstake / desync) instruction:
     *   liqSOL pool ATA → user liqSOL ATA
     *
     * When Wire is in PostLaunch mode this instruction emits a `DesyncRequest`
     * OPP attestation and does NOT immediately return tokens.  The Wire chain
     * relay picks up the attestation, and an admin subsequently executes
     * `complete_withdraw` to deliver the tokens to the user.
     *
     * In PreLaunch mode the tokens are returned directly.
     */
    async buildUnstakeIx(
        amountLamports: bigint,
        user?: PublicKey,
    ): Promise<TransactionInstruction> {
        const program = this.program;
        const userPk = user ?? this.wallet.publicKey;
        if (!userPk) {
            throw new Error('OutpostClient.buildUnstakeIx: wallet not connected');
        }

        const a = await this.buildAccounts(userPk);

        return 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,
                globalConfig: a.globalConfig,
                oppEpochState: a.oppEpochState,
                pendingAttestations: a.pendingAttestations,
                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}`;
    }
}
