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

import {
    DEFAULT_AVERAGE_PAY_RATE,
    EPHEMERAL_RENT_EXEMPTION,
    solToLamports,
    DEFAULT_PAY_RATE_LOOKBACK,
    CHAINLINK_FEED,
    CHAINLINK_PROGRAM,
} from './constants';

import { GlobalState, TrancheState } from './types';
import { TrancheLadderItem, TrancheSnapshot } from '../../types';
import { ChainID } from '@wireio/core';
import { SolanaProgramService } from './program';

// -----------------------------------------------------------------------------
// Tranche Support
// -----------------------------------------------------------------------------
const INDEX_SCALE = BigInt(1_000_000_000_000); // 1e12
const USD_SCALE = BigInt(100_000_000);       // 1e8
const BPS = BigInt(10_000);

/**
 * Apply one forward growth step: value * (BPS + growthBps) / BPS.
 * Simple integer round-half-up.
 */
function growSupplyOnce(value: bigint, growthBps: number): bigint {
    const g = BigInt(growthBps);
    return (value * (BPS + g) + BPS / BigInt(2)) / BPS;
}

/**
 * Apply one backward step: value * BPS / (BPS + growthBps).
 * Also integer round-half-up.
 */
function shrinkSupplyOnce(value: bigint, growthBps: number): bigint {
    const g = BigInt(growthBps);
    return (value * BPS + (BPS + g) / BigInt(2)) / (BPS + g);
}

/**
 * Price step is expressed in *cents*, while prices are 1e8 USD.
 * 1 cent = 0.01 * 1e8 = 1e6, so:
 *   step = priceGrowthCents * 1e6
 */
function priceStepUsd1e8(priceGrowthCents: number): bigint {
    if (!priceGrowthCents) return BigInt(0);
    const CENT_SCALE = USD_SCALE / BigInt(100); // 1e6
    return BigInt(priceGrowthCents) * CENT_SCALE;
}

function growPriceOnceUsd1e8(value: bigint, priceGrowthCents: number): bigint {
    const step = priceStepUsd1e8(priceGrowthCents);
    return value + step;
}

function shrinkPriceOnceUsd1e8(value: bigint, priceGrowthCents: number): bigint {
    const step = priceStepUsd1e8(priceGrowthCents);
    if (step === BigInt(0)) return value;
    if (value <= step) return BigInt(0);
    return value - step;
}

/** BN | bigint -> bigint helper (keeps code readable) */
export function toBigint(x: any): bigint {
    if (typeof x === 'bigint') return x;
    if (typeof x === 'number') return BigInt(x);
    // Anchor BN
    return BigInt(x.toString());
}

/**
 * Convert token amount -> shares using the same rule as on-chain tests:
 * shares = ceil(amount * INDEX_SCALE / currentIndex)
 */
export function tokensToShares(amount: bigint, currentIndex: bigint): bigint {
    if (amount === BigInt(0)) return BigInt(0);
    const num = amount * INDEX_SCALE;
    const q = num / currentIndex;
    const r = num % currentIndex;
    return r === BigInt(0) ? q : q + BigInt(1);
}

/**
 * Build a local tranche ladder around the current tranche
 * using only on-chain config + current state.
 *
 * Rules (from liqsol_core tests):
 *  - past tranches are fully sold
 *  - current tranche has (initial - currentSupply) sold
 *  - future tranches start with 0 sold
 *  - supply/price grow by supplyGrowthBps/priceGrowthBps per tranche
 */
export function buildSolanaTrancheLadder(options: {
    currentTranche: number;
    initialTrancheSupply: bigint;
    currentTrancheSupply: bigint;
    currentPriceUsd: bigint;    // 1e8 scale
    supplyGrowthBps: number;
    priceGrowthCents: number;   // e.g. 2 -> $0.02 per tranche
    windowBefore?: number;
    windowAfter?: number;
}): TrancheLadderItem[] {
    const {
        currentTranche,
        initialTrancheSupply,
        currentTrancheSupply,
        currentPriceUsd,
        supplyGrowthBps,
        priceGrowthCents,
        windowBefore = 5,
        windowAfter = 5,
    } = options;

    const startId = Math.max(0, currentTranche - windowBefore);
    const endId = currentTranche + windowAfter;

    const capacity = new Map<number, bigint>();
    const price = new Map<number, bigint>();

    // Seed current tranche
    capacity.set(currentTranche, initialTrancheSupply);
    price.set(currentTranche, currentPriceUsd);

    // Forward (future tranches): grow supply by BPS, price by +cents (linear)
    for (let id = currentTranche + 1; id <= endId; id++) {
        const prevCap = capacity.get(id - 1)!;
        const prevPrice = price.get(id - 1)!;

        capacity.set(id, growSupplyOnce(prevCap, supplyGrowthBps));
        price.set(id, growPriceOnceUsd1e8(prevPrice, priceGrowthCents));
    }

    // Backward (past tranches): shrink supply by inverse BPS, price by -cents
    for (let id = currentTranche - 1; id >= startId; id--) {
        const nextCap = capacity.get(id + 1)!;
        const nextPrice = price.get(id + 1)!;

        capacity.set(id, shrinkSupplyOnce(nextCap, supplyGrowthBps));
        price.set(id, shrinkPriceOnceUsd1e8(nextPrice, priceGrowthCents));
    }

    const ladder: TrancheLadderItem[] = [];
    for (let id = startId; id <= endId; id++) {
        const cap = capacity.get(id)!;

        let sold: bigint;
        if (id < currentTranche) {
            // Past tranches fully sold
            sold = cap;
        } else if (id === currentTranche) {
            // Current tranche: cap - remaining
            sold = cap - currentTrancheSupply;
        } else {
            // Future tranches not yet opened
            sold = BigInt(0);
        }

        ladder.push({
            id,
            capacity: cap,
            sold,
            remaining: cap - sold,
            priceUsd: price.get(id)!, // still 1e8 scale
        });
    }

    return ladder;
}

/**
 * Turn raw liqsol_core accounts into a chain-agnostic TrancheSnapshot for SOL.
 * All math stays here; TokenClient just wires accounts + connection.
 */
export function buildSolanaTrancheSnapshot(options: {
    chainID: ChainID;
    globalState: GlobalState;
    trancheState: TrancheState;
    solPriceUsd?: bigint;
    nativePriceTimestamp?: number;
    ladderWindowBefore?: number;
    ladderWindowAfter?: number;
}): TrancheSnapshot {
    const {
        chainID,
        globalState,
        trancheState,
        solPriceUsd,
        nativePriceTimestamp,
        ladderWindowBefore,
        ladderWindowAfter,
    } = options;

    const currentIndex = toBigint(globalState.currentIndex);
    const totalShares = toBigint(globalState.totalShares);
    const currentTranche = trancheState.currentTrancheNumber.toNumber();
    const currentTrancheSupply = toBigint(trancheState.currentTrancheSupply);
    const initialTrancheSupply = toBigint(trancheState.initialTrancheSupply);
    const totalPretokensSold = toBigint(trancheState.totalPretokensSold);
    const currentPriceUsd = toBigint(trancheState.currentTranchePriceUsd);
    const supplyGrowthBps = trancheState.supplyGrowthBps;
    const priceGrowthCents = trancheState.priceGrowthCents;

    const ladder = buildSolanaTrancheLadder({
        currentTranche,
        initialTrancheSupply,
        currentTrancheSupply,
        currentPriceUsd,
        supplyGrowthBps,
        priceGrowthCents,
        windowBefore: ladderWindowBefore,
        windowAfter: ladderWindowAfter,
    });

    return {
        chainID,
        currentIndex,
        totalShares,
        currentTranche,
        currentPriceUsd,
        supplyGrowthBps,
        priceGrowthCents,
        totalPretokensSold,
        currentTrancheSupply,
        initialTrancheSupply,
        nativePriceUsd: solPriceUsd,
        nativePriceTimestamp,
        ladder,
    };
}

/**
 * ---------------------------------------------------------------------------
 * Outpost-specific helpers
 * ---------------------------------------------------------------------------
 *
 * This is the SDK equivalent of `deriveWirePdas` in capital-staking tests.
 * It aggregates all PDAs / ATAs needed for Outpost pretokens + synd/desynd.
 */
export interface OutpostAccounts {
    user: PublicKey;

    // Outpost / pretoken wiring
    globalState: PublicKey;
    poolAuthority: PublicKey;
    liqsolMint: PublicKey;
    liqsolPoolAta: PublicKey;
    userAta: PublicKey;
    outpostAccount: PublicKey;
    userPretokenRecord: PublicKey;

    // Distribution / yield context (user_record keyed by token accounts)
    distributionState: PublicKey;
    bucketAuthority: PublicKey;
    bucketTokenAccount: PublicKey;
    bucketUserRecord: PublicKey;
    liqsolPoolUserRecord: PublicKey;
    userUserRecord: PublicKey;

    // Tranche / pricing
    trancheState: PublicKey;

    // Chainlink
    chainLinkFeed: PublicKey;
    chainLinkProgram: PublicKey;

    // Transfer hook extra accounts
    extraAccountMetaList: PublicKey;

    // New
    pretokenPurchaseHistory: PublicKey;

    // OPP (outpost protocol pipeline)
    globalConfig: PublicKey;
    oppEpochState: PublicKey;
    pendingAttestations: PublicKey;
    bridgeState: PublicKey;
    bridgeAuthority: PublicKey;
    bridgeVaultAta: PublicKey;
    bridgeUserRecord: PublicKey;
}

export async function buildOutpostAccounts(
    connection: Connection,
    user: PublicKey,
    pgs: SolanaProgramService,
): Promise<OutpostAccounts> {
    const globalState = pgs.deriveOutpostGlobalStatePda();
    const poolAuthority = pgs.deriveOutpostPoolAuthorityPda();
    const liqsolMint = pgs.deriveLiqsolMintPda();
    const distributionState = pgs.deriveDistributionStatePda();
    const bucketAuthority = pgs.deriveBucketAuthorityPda();
    const trancheState = pgs.deriveTrancheStatePda();

    // Outpost / pretoken PDAs
    const outpostAccount = pgs.deriveOutpostAccountPda(user);
    const userPretokenRecord = pgs.deriveUserPretokenRecordPda(user);

    // ATAs
    const liqsolPoolAta = await getAssociatedTokenAddress(
        liqsolMint,
        poolAuthority,
        true,
        TOKEN_2022_PROGRAM_ID,
    );

    const bucketTokenAccount = await getAssociatedTokenAddress(
        liqsolMint,
        bucketAuthority,
        true,
        TOKEN_2022_PROGRAM_ID,
    );

    const userAta = await getAssociatedTokenAddress(
        liqsolMint,
        user,
        true, // allow off curve in case squadsx vault
        TOKEN_2022_PROGRAM_ID,
    );

    // user_record PDAs keyed by TOKEN ACCOUNTS (matches capital-staking)
    const bucketUserRecord = pgs.deriveUserRecordPda(bucketTokenAccount);
    const liqsolPoolUserRecord = pgs.deriveUserRecordPda(liqsolPoolAta);
    const userUserRecord = pgs.deriveUserRecordPda(userAta);

    const extraAccountMetaList = pgs.deriveExtraAccountMetaListPda(liqsolMint);

    const pretokenPurchaseHistory = pgs.derivePretokenPurchaseHistoryPda(poolAuthority);

    // OPP (outpost protocol pipeline)
    const globalConfig = pgs.deriveGlobalConfigPda();
    const oppEpochState = pgs.deriveOppEpochStatePda();
    const epochNumber = await pgs.fetchOppEpochNumber(connection);
    const pendingAttestations = pgs.derivePendingAttestationsPda(epochNumber);
    const bridgeState = pgs.deriveOppBridgeStatePda();
    const bridgeAuthority = PublicKey.findProgramAddressSync(
        [Buffer.from('opp_bridge_vault')],
        pgs.PROGRAM_IDS.LIQSOL_CORE,
    )[0];
    const bridgeVaultAta = await getAssociatedTokenAddress(
        liqsolMint,
        bridgeAuthority,
        true,
        TOKEN_2022_PROGRAM_ID,
    );
    const bridgeUserRecord = pgs.deriveUserRecordPda(bridgeVaultAta);

    // Chainlink program feeds
    let chainLinkFeed = CHAINLINK_FEED;
    let chainLinkProgram = CHAINLINK_PROGRAM;

    try {
        const program: any = pgs.getProgram('liqsolCore');
        const ts: TrancheState =
            await program.account.trancheState.fetch(trancheState);

        // If on-chain config overrides are present, prefer those.
        if (ts.chainlinkFeed && ts.chainlinkProgram) {
            chainLinkFeed = ts.chainlinkFeed as PublicKey;
            chainLinkProgram = ts.chainlinkProgram as PublicKey;
        }
    } catch {
        // If trancheState isn't initialized yet, we fall back to the constants.
        // In that case, purchase instructions will still fail, which is the
        // correct behavior until admin initializes TrancheState on-chain.
    }

    void connection; // reserved for future extra validation

    return {
        user,
        globalState,
        poolAuthority,
        liqsolMint,
        liqsolPoolAta,
        userAta,
        outpostAccount,
        userPretokenRecord,
        distributionState,
        bucketAuthority,
        bucketTokenAccount,
        bucketUserRecord,
        liqsolPoolUserRecord,
        userUserRecord,
        trancheState,
        chainLinkFeed,
        chainLinkProgram,
        extraAccountMetaList,
        pretokenPurchaseHistory,
        globalConfig,
        oppEpochState,
        pendingAttestations,
        bridgeState,
        bridgeAuthority,
        bridgeVaultAta,
        bridgeUserRecord,
    };
}
// -----------------------------------------------------------------------------
// Epoch / scheduling helpers
// -----------------------------------------------------------------------------

export type EpochSnapshot = {
    epoch: number;
    slot: number;
    firstSlot: number;
    slotsInEpoch: number;
    slotMs: number;
};

export async function getEpochSnapshot(
    connection: Connection,
): Promise<EpochSnapshot> {
    const info = await connection.getEpochInfo();

    // Fallback slot time
    let slotTimeMs = Number(process.env.SLOT_TIME_MS_FALLBACK ?? 400);

    try {
        const samples = await connection.getRecentPerformanceSamples(16);
        if (samples.length) {
            const avgMs =
                samples.reduce(
                    (acc, s) => acc + (s.samplePeriodSecs * 1000) / s.numSlots,
                    0,
                ) / samples.length;
            if (isFinite(avgMs) && avgMs > 0) {
                slotTimeMs = avgMs;
            }
        }
    } catch {
        // ignore; keep fallback
    }

    return {
        epoch: info.epoch,
        slot: info.slotIndex,
        firstSlot: info.absoluteSlot - info.slotIndex,
        slotsInEpoch: info.slotsInEpoch,
        slotMs: slotTimeMs,
    };
}

export function msToEpochEnd(snapshot: EpochSnapshot): number {
    const remainingSlots = snapshot.slotsInEpoch - snapshot.slot;
    return remainingSlots * snapshot.slotMs;
}

/**
 * Generic "execute around epoch boundaries" helper:
 *
 * - If current progress is within [early, late], run immediately.
 * - If too early, sleep until `early` percentage into the epoch.
 * - If too late, sleep until `early` percentage into the *next* epoch.
 *
 * This is generic; you can wrap any instruction builder in here (including
 * deposit flows) without baking in program-specific logic.
 */
export async function scheduledInstruction<T>(
    connection: Connection,
    config: ScheduleConfig,
    instruction: () => Promise<T>,
): Promise<T> {
    const early = config.early ?? 0.10;
    const late = config.late ?? 0.90;

    const snapshot = await getEpochSnapshot(connection);
    const progress = snapshot.slot / snapshot.slotsInEpoch;

    // Case 1: Already in safe zone
    if (progress >= early && progress <= late) {
        return instruction();
    }

    // Case 2: Early in epoch → wait until `early`
    if (progress < early) {
        const targetSlot = snapshot.slotsInEpoch * early;
        const slotsRemaining = targetSlot - snapshot.slot;
        const msToWait = slotsRemaining * snapshot.slotMs;

        console.log(
            `Epoch early (${(progress * 100).toFixed(
                2,
            )}%). Sleeping ${(msToWait / 1000).toFixed(1)}s until ${early * 100
            }%...`,
        );

        await sleep(Math.max(1000, msToWait));
        return instruction();
    }

    // Case 3: Late in epoch → wait for next epoch + early window
    const msToNextEpoch = msToEpochEnd(snapshot);
    const earlyWaitTime = snapshot.slotsInEpoch * early * snapshot.slotMs;
    const totalWaitTime = msToNextEpoch + earlyWaitTime + 1000;

    console.log(
        `Epoch late (${(progress * 100).toFixed(
            2,
        )}%). Sleeping ${(totalWaitTime / 1000).toFixed(
            1,
        )}s until next epoch + ${early * 100}%...`,
    );

    await sleep(totalWaitTime);
    return instruction();
}


// -----------------------------------------------------------------------------
// Generic helpers (used in tests + can be useful in apps)
// -----------------------------------------------------------------------------

export function getErrorMessage(error: any): string {
    return (
        error?.error?.errorCode?.code ||
        error?.error?.errorMessage ||
        error?.message ||
        ''
    );
}

export function generateRandomDepositAmount(
    minSol = 2,
    maxSol = 100,
): BN {
    const randomSol = Math.random() * (maxSol - minSol) + minSol;
    return new BN(solToLamports(randomSol));
}

export function generateTestKeypair(): Keypair {
    return Keypair.generate();
}

export async function airdropSol(
    connection: Connection,
    publicKey: PublicKey,
    amountSol: number,
): Promise<void> {
    const lamports = solToLamports(amountSol);
    const sig = await connection.requestAirdrop(publicKey, Number(lamports));
    await connection.confirmTransaction(sig, 'confirmed');
}

export async function waitForConfirmation(
    connection: Connection,
    signature: string,
): Promise<void> {
    await connection.confirmTransaction(signature, 'confirmed');
}

export function sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

/**
 * Simple helper used in tests: wait until safe zone, no-op.
 */
export async function waitUntilSafeToExecuteFunction(
    connection: Connection,
    config: ScheduleConfig = {},
): Promise<void> {
    await scheduledInstruction(connection, config, async () => {
        // no-op
        return;
    });
}

export interface ScheduleConfig {
    early?: number; // fraction of epoch, default 0.10
    late?: number;  // fraction of epoch, default 0.90
}

export function ceilDiv(n: BN, d: BN): BN {
    if (d.isZero()) {
        throw new Error('Division by zero in ceilDiv');
    }
    return n.add(d.subn(1)).div(d);
}

export function normalizeToBigInt(x: any): bigint {
    if (typeof x === 'bigint') return x;
    if (x != null && typeof x === 'object' && 'toString' in x) return BigInt(x.toString());
    if (typeof x === 'number') return BigInt(x);
    throw new Error(`normalizeToBigInt: unsupported type ${typeof x}`);
}

export const safeFetch = async <T>(promise: Promise<T>, label?: string): Promise<T | null> => {
    try { return await promise; }
    catch (err) {
        console.error(`Safe Fetch Failed${label ? ` (${label})` : ''}:`,err);
        return null;
    }
};
