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

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

import { LiqsolCoreClientIdl, SolanaProgramService } from '../program';

import {
    GlobalState,
    PriceHistory,
    TrancheState,
    WalletLike,
    OutpostAccount,
    UserPretokenRecord,
} from '../types';

import {
    CHAINLINK_FEED,
    CHAINLINK_PROGRAM,
} from '../constants';

export class TokenClient {
    private readonly program: Program<LiqsolCoreClientIdl>;

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

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

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

    // ---------------------------------------------------------------------------
    // Account helpers
    // ---------------------------------------------------------------------------

    async getAccounts(user: PublicKey): Promise<OutpostAccounts> {
        return buildOutpostAccounts(this.provider.connection, user, this.pgs);
    }

    async fetchGlobalState(): Promise<GlobalState> {
        const program = this.program;
        const { globalState } = await this.getAccounts(this.wallet.publicKey);
        return program.account.globalState.fetch(globalState);
    }

    async fetchTrancheState(): Promise<TrancheState> {
        const program = this.program;
        const { trancheState } = await this.getAccounts(this.wallet.publicKey);
        return program.account.trancheState.fetch(trancheState);
    }

    /**
     * Fetch the per-user Outpost account (old "wire receipt").
     * Returns null if the user has never interacted with Outpost.
     */
    async fetchOutpostAccount(user: PublicKey): Promise<OutpostAccount | null> {
        const program = this.program;
        const { outpostAccount } = await this.getAccounts(user);
        return program.account.outpostAccount.fetchNullable(outpostAccount);
    }

    /**
     * Fetch the per-user pretoken purchase record.
     * Returns null if the user has never purchased pretokens.
     */
    async fetchUserPretokenRecord(
        user: PublicKey,
    ): Promise<UserPretokenRecord | null> {
        const program = this.program;
        const { userPretokenRecord } = await this.getAccounts(user);
        return program.account.userPretokenRecord.fetchNullable(
            userPretokenRecord,
        );
    }

    // ---------------------------------------------------------------------------
    // Instruction builders (no send / confirm)
    // ---------------------------------------------------------------------------

    /**
     * Build a pretoken `purchase` instruction using liqSOL from the user’s ATA.
     *
     * This mirrors capital-staking `executePurchase` wiring:
     * - amountLamports: liqSOL amount (raw token-2022 units, not SOL)
     */
    async buildPurchaseIx(
        amountLamports: bigint,
        user = this.wallet.publicKey,
    ): Promise<TransactionInstruction> {
        const program = this.program;
        console.log('build purchase for', user.toBase58());
        
        const a = await this.getAccounts(user);

        const extraAccountMetaList = this.pgs.deriveExtraAccountMetaListPda(a.liqsolMint);
        const liqsolCoreProgram = this.pgs.PROGRAM_IDS.LIQSOL_CORE;
        const transferHookProgram = this.pgs.PROGRAM_IDS.TRANSFER_HOOK;

        return program.methods
            .purchase(new BN(amountLamports.toString()))
            .accounts({
                user: a.user,
                liqsolMint: a.liqsolMint,
                globalState: a.globalState,
                distributionState: a.distributionState,

                // buyer’s liqSOL
                buyerAta: a.userAta,

                // pool / bucket context
                poolAuthority: a.poolAuthority,
                bucketAuthority: a.bucketAuthority,
                bucketTokenAccount: a.bucketTokenAccount,
                bucketUserRecord: a.bucketUserRecord,

                // distribution userRecords (token-account keyed)
                senderUserRecord: a.userUserRecord,
                receiverUserRecord: a.liqsolPoolUserRecord,

                // transfer-hook wiring
                extraAccountMetaList,
                liqsolCoreProgram,
                transferHookProgram,

                // pool + outpost + tranche
                liqsolPoolAta: a.liqsolPoolAta,
                outpostAccount: a.outpostAccount,
                trancheState: a.trancheState,
                userPretokenRecord: a.userPretokenRecord,

                // Chainlink pricing
                chainlinkFeed: a.chainLinkFeed ?? CHAINLINK_FEED,
                chainlinkProgram: a.chainLinkProgram ?? CHAINLINK_PROGRAM,

                tokenProgram: TOKEN_2022_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                systemProgram: SystemProgram.programId,

                pretokenPurchaseHistory: a.pretokenPurchaseHistory,

                globalConfig: a.globalConfig,
                oppEpochState: a.oppEpochState, 
                pendingAttestations: a.pendingAttestations
            })
            .instruction();
    }

    // NOTE:
    // - There is no longer `purchaseWithSol` in the IDL - SOL → liqSOL happens
    //   through the deposit / liqSOL minting path, not directly in pretokens.
    // - `purchaseFromYield` is an operator-facing / protocol flow and you
    //   indicated you’ll trigger it from Wire infra, so we don’t expose a
    //   client helper here for now.

    // ---------------------------------------------------------------------------
    // Price helpers (SOL/USD via priceHistory account)
    // ---------------------------------------------------------------------------

    async getSolPriceUsdSafe(): Promise<{ price?: bigint; timestamp?: number }> {
        try {
            const price = await this.getSolPriceUsd();
            // current priceHistory account has no timestamp; keep optional
            return { price, timestamp: undefined };
        } catch {
            return { price: undefined, timestamp: undefined };
        }
    }

    /**
     * Fetch latest SOL/USD price (1e8 scale) from liqsol_core.priceHistory.
     * Uses the ring-buffer semantics from capital-staking.
     */
    async getSolPriceUsd(): Promise<bigint> {
        const program = this.program;
        const priceHistoryPda = this.pgs.derivePriceHistoryPda();
        const history = (await program.account.priceHistory.fetch(
            priceHistoryPda,
        )) as PriceHistory;

        const { prices, nextIndex, count, windowSize } = history;

        if (!prices || prices.length === 0 || !count) {
            throw new Error('Price history is empty - no SOL price available');
        }

        const capacity = prices.length || windowSize;
        if (!capacity) {
            throw new Error(
                'Price history capacity is zero - check account layout',
            );
        }

        const lastIndex = nextIndex === 0 ? capacity - 1 : nextIndex - 1;
        const latest = prices[lastIndex];

        if (!BN.isBN(latest)) {
            throw new Error(
                'Latest price entry is not a BN - check IDL/decoder',
            );
        }

        return BigInt(latest.toString());
    }
}
