// src/networks/solana/programs.ts
import { AnchorProvider, Program } from '@coral-xyz/anchor';
import { ChainID, SolChainID } from '@wireio/core';
import { PublicKey, StakeProgram } from '@solana/web3.js';

import {
    PDA_SEEDS,
    getProgramIds,
    SolanaProgramIds,
} from './constants';

// Mainnet IDLs
import liqsolCoreMainnetJson from '../../assets/solana/mainnet/idl/liqsol_core.json';
import liqsolTokenMainnetJson from '../../assets/solana/mainnet/idl/liqsol_token.json';
import validatorLeaderboardMainnetJson from '../../assets/solana/mainnet/idl/validator_leaderboard.json';

// Devnet IDLs
import liqsolCoreDevnetJson from '../../assets/solana/devnet/idl/liqsol_core.json';
import liqsolTokenDevnetJson from '../../assets/solana/devnet/idl/liqsol_token.json';
import validatorLeaderboardDevnetJson from '../../assets/solana/devnet/idl/validator_leaderboard.json';

// Mainnet types
import type { LiqsolCore as LiqSolCoreMainnet } from '../../assets/solana/mainnet/types/liqsol_core';
import type { LiqsolToken as LiqsolTokenMainnet } from '../../assets/solana/mainnet/types/liqsol_token';
import type { ValidatorLeaderboard as ValidatorLeaderboardMainnet } from '../../assets/solana/mainnet/types/validator_leaderboard';

// Devnet types
import type { LiqsolCore as LiqSolCoreDevnet } from '../../assets/solana/devnet/types/liqsol_core';
import type { LiqsolToken as LiqsolTokenDevnet } from '../../assets/solana/devnet/types/liqsol_token';
import type { ValidatorLeaderboard as ValidatorLeaderboardDevnet } from '../../assets/solana/devnet/types/validator_leaderboard';

// “Client” IDLs = mainnet OR devnet, plus `address` field we add at runtime

export type LiqsolCoreClientIdl =
    (LiqSolCoreMainnet | LiqSolCoreDevnet) & { address: string };

export type LiqsolTokenClientIdl =
    (LiqsolTokenMainnet | LiqsolTokenDevnet) & { address: string };

export type ValidatorLeaderboardClientIdl =
    (ValidatorLeaderboardMainnet | ValidatorLeaderboardDevnet) & { address: string };

export type SolanaProgramIdlMap = {
    liqsolCore: LiqsolCoreClientIdl;
    liqsolToken: LiqsolTokenClientIdl;
    validatorLeaderboard: ValidatorLeaderboardClientIdl;
};

export type SolanaProgramName = keyof SolanaProgramIdlMap;

type Entry<IDL> = {
    idl: IDL & { address: string };
    address: string;
};

const PROGRAMS_BY_CHAIN = {
    [SolChainID.Mainnet]: {
        liqsolCore: {
            idl: liqsolCoreMainnetJson,
            address: liqsolCoreMainnetJson.address,
        } as Entry<LiqSolCoreMainnet>,
        liqsolToken: {
            idl: liqsolTokenMainnetJson,
            address: liqsolTokenMainnetJson.address,
        } as Entry<LiqsolTokenMainnet>,
        validatorLeaderboard: {
            idl: validatorLeaderboardMainnetJson,
            address: validatorLeaderboardMainnetJson.address,
        } as Entry<ValidatorLeaderboardMainnet>,
    },
    [SolChainID.Devnet]: {
        liqsolCore: {
            idl: liqsolCoreDevnetJson,
            address: liqsolCoreDevnetJson.address,
        } as Entry<LiqSolCoreDevnet>,
        liqsolToken: {
            idl: liqsolTokenDevnetJson,
            address: liqsolTokenDevnetJson.address,
        } as Entry<LiqsolTokenDevnet>,
        validatorLeaderboard: {
            idl: validatorLeaderboardDevnetJson,
            address: validatorLeaderboardDevnetJson.address,
        } as Entry<ValidatorLeaderboardDevnet>,
    },
} as const;

export class SolanaProgramService {
    private readonly ids: SolanaProgramIds;
    private readonly programs: (typeof PROGRAMS_BY_CHAIN)[SolChainID.Mainnet];

    constructor(
        private provider: AnchorProvider,
        private chainId: ChainID,
    ) {
        const solChain = chainId as SolChainID;
        this.ids = getProgramIds(solChain);
        const programs = PROGRAMS_BY_CHAIN[solChain];
        if (!programs) {
            throw new Error(`Unsupported Solana chainId=${chainId}`);
        }
        this.programs = programs;
    }

    /** Expose per-chain program IDs */
    get programIds(): SolanaProgramIds {
        return this.ids;
    }

    // Generic getProgram, typed by name → union IDL
    getProgram<K extends SolanaProgramName>(
        name: K,
    ): Program<SolanaProgramIdlMap[K]> {
        const { idl, address } = this.programs[name];
        const idlWithAddr = { ...(idl as any), address } as SolanaProgramIdlMap[K];

        return new Program(idlWithAddr, this.provider) as Program<SolanaProgramIdlMap[K]>;
    }

    listProgramNames(): SolanaProgramName[] {
        return Object.keys(this.programs) as SolanaProgramName[];
    }

    get PROGRAM_IDS(): SolanaProgramIds {
        return this.ids;
    }


    // ---------------------------------------------------------------------
    // PDA helpers (instance methods, no ids passed in)
    // ---------------------------------------------------------------------

    // GLOBAL CONFIG
    deriveGlobalConfigPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.GLOBAL_CONFIG)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    // CORE / DISTRIBUTION / DEPOSIT

    deriveLiqsolMintPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.LIQSOL_MINT)],
            this.ids.LIQSOL_TOKEN,
        )[0];
    }

    deriveLiqsolMintAuthorityPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.LIQSOL_MINT_AUTHORITY)],
            this.ids.LIQSOL_TOKEN,
        )[0];
    }

    deriveDepositAuthorityPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.DEPOSIT_AUTHORITY)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveVaultPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.VAULT)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveReservePoolPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.RESERVE_POOL)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveStakeControllerStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.STAKE_CONTROLLER_STATE)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    derivePayoutStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.PAYOUT_STATE)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveBucketAuthorityPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.BUCKET_AUTHORITY)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveDistributionStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.DISTRIBUTION_STATE)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    /**
     * user_record PDA (keyed by USER wallet)
     */
    deriveUserRecordPda(user: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.USER_RECORD), user.toBuffer()],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    derivePayRateHistoryPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.PAY_RATE_HISTORY)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    // --- Withdraw module / receipts ---

    deriveWithdrawGlobalPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.WITHDRAW_GLOBAL)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveStakeAllocationStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.STAKE_ALLOCATION_STATE)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveStakeMetricsPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.STAKE_METRICS)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveMaintenanceLedgerPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.MAINTENANCE_LEDGER)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveWithdrawMintAuthorityPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.WITHDRAW_MINT_AUTHORITY)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveWithdrawMintMetadataPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.MINT_METADATA)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    derivePretokenPurchaseHistoryPda(poolAuthority: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.PRETOKEN_PURCHASE_HISTORY), poolAuthority.toBuffer()],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    /**
     * NFT mint for withdrawal receipt, derived from nextReceiptId.
     */
    deriveWithdrawNftMintPda(receiptId: bigint | number): PublicKey {
        const receiptIdBytes = Buffer.alloc(8);
        const asBigInt =
            typeof receiptId === 'bigint'
                ? receiptId
                : BigInt(receiptId.toString());
        receiptIdBytes.writeBigUInt64LE(asBigInt);
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.WITHDRAW_MINT), receiptIdBytes],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveLiqReceiptDataPda(nftMint: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.LIQ_RECEIPT_DATA), nftMint.toBuffer()],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    // --- Validator leaderboard ---

    deriveLeaderboardStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.LEADERBOARD_STATE)],
            this.ids.VALIDATOR_LEADERBOARD,
        )[0];
    }

    deriveValidatorRecordPda(voteAccount: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.VALIDATOR_RECORD), voteAccount.toBuffer()],
            this.ids.VALIDATOR_LEADERBOARD,
        )[0];
    }

    deriveStakeControllerVaultPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.VAULT)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    // --- Outpost / pretokens ---

    deriveOutpostGlobalStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.OUTPOST_GLOBAL_STATE)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveOutpostPoolAuthorityPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.LIQSOL_POOL)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveOutpostAccountPda(user: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.OUTPOST_ACCOUNT), user.toBuffer()],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveUserPretokenRecordPda(user: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.USER_PRETOKEN_RECORD), user.toBuffer()],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveTrancheStatePda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.TRANCHE_STATE)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    derivePriceHistoryPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.PRICE_HISTORY)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    // --- BAR (bonded actor roles) ---

    deriveBarConfigPda(): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.BAR_STATE_SEED)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveBondLevelPda(bondLevelId: number[]): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.BOND_LEVEL_SEED), Buffer.from(bondLevelId)],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    deriveBondedActorPda(actor: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.BONDED_ACTOR_SEED), actor.toBuffer()],
            this.ids.LIQSOL_CORE,
        )[0];
    }

    // --- Transfer-hook extra metas ---

    deriveExtraAccountMetaListPda(liqsolMint: PublicKey): PublicKey {
        return PublicKey.findProgramAddressSync(
            [Buffer.from(PDA_SEEDS.EXTRA_ACCOUNT_METAS), liqsolMint.toBuffer()],
            this.ids.TRANSFER_HOOK,
        )[0];
    }

    // --- Ephemeral stake address (per-deposit) ---

    /**
     * Ephemeral stake account address used per-deposit.
     * On-chain convention: seed = `ephemeral_<u32>` under StakeProgram.programId.
     */
    async deriveEphemeralStakeAddress(
        user: PublicKey,
        seed: number,
    ): Promise<PublicKey> {
        const seedStr = `ephemeral_${seed}`;
        return PublicKey.createWithSeed(user, seedStr, StakeProgram.programId);
    }
}