// file: kb_demo_app/frontend/ts/demo3.ts

import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
import type { Demo3LocalDexCorpusSearchRequest } from "./bindings/Demo3LocalDexCorpusSearchRequest.ts";
import type { Demo3LocalDexCorpusSearchPayload } from "./bindings/Demo3LocalDexCorpusSearchPayload.ts";
import type { Demo3OnchainDexDiscoveryRequest } from "./bindings/Demo3OnchainDexDiscoveryRequest.ts";
import type { Demo3OnchainDexDiscoveryResult } from "./bindings/Demo3OnchainDexDiscoveryResult.ts";
import type { Demo3OnchainDexPairCandidate } from "./bindings/Demo3OnchainDexPairCandidate.ts";
import type { Demo3LocalDexCorpusSearchResult } from "./bindings/Demo3LocalDexCorpusSearchResult.ts";
import type { Demo3OnchainDexDiscoveryPayload } from "./bindings/Demo3OnchainDexDiscoveryPayload.ts";

(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;


interface Demo3Preset {
    label: string;
    dexCode: string;
    programId: string;
    description: string;
}

const presets: Demo3Preset[] = [
    { label: "PumpSwap", dexCode: "pump_swap", programId: "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA", description: "DEX effectif PumpSwap." },
    { label: "Raydium CPMM", dexCode: "raydium_cpmm", programId: "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", description: "Raydium CPMM." },
    { label: "Raydium CLMM", dexCode: "raydium_clmm", programId: "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK", description: "Raydium CLMM." },
    { label: "Raydium Launchpad", dexCode: "raydium_launchpad", programId: "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", description: "Raydium Launchpad / LaunchLab. Surface de lancement Raydium 0.7.50." },
    { label: "Raydium AMM v4", dexCode: "raydium_amm_v4", programId: "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", description: "Raydium AMM v4 legacy. À prouver par corpus avant décodage swap." },
    { label: "Raydium Stable Swap", dexCode: "raydium_stable_swap", programId: "5quBtoiQqxF9Jv6KYKctB59NT3gtJD2Y65kdnB1Uev3h", description: "Stable Swap Raydium à vérifier par corpus." },
    { label: "Meteora DLMM", dexCode: "meteora_dlmm", programId: "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo", description: "Meteora DLMM." },
    { label: "Meteora DAMM v1", dexCode: "meteora_damm_v1", programId: "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB", description: "Meteora DAMM v1." },
    { label: "Meteora DAMM v2", dexCode: "meteora_damm_v2", programId: "cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG", description: "Meteora DAMM v2." },
    { label: "Meteora DBC", dexCode: "meteora_dbc", programId: "dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN", description: "Meteora DBC." },
    { label: "Orca Whirlpools", dexCode: "orca_whirlpools", programId: "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", description: "Orca Whirlpools CLMM." },
    { label: "FluxBeam", dexCode: "fluxbeam", programId: "FLUXubRmkEi2q6K3Y9kBPg9248ggaZVsoSFhtJHSrm1X", description: "FluxBeam." },
    { label: "GooseFX V1 (Vybe)", dexCode: "goosefx_v1", programId: "GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Obric V2 (Vybe)", dexCode: "obric_v2", programId: "obriQD1zbpyLz95G5n7nJe6a4DPjpFwa5XYPoNm113y", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Ondo Global Market (Vybe)", dexCode: "ondo_global_market", programId: "XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Scorch (Vybe)", dexCode: "scorch", programId: "SCoRcH8c2dpjvcJD6FiPbCSQyQgu3PcUAWj2Xxx3mqn", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "ZeroFi (Vybe)", dexCode: "zerofi", programId: "ZERor4xhbUycZ6gb9ntrhqscUcZmAbQDjEAtCf4hbZY", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Manifest CLOB (Vybe)", dexCode: "manifest_clob", programId: "MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms", description: "Entrée Vybe orderbook; à vérifier par corpus local." },
    { label: "AlphaQ (Vybe)", dexCode: "alphaq", programId: "ALPHAQmeA7bjrVuccPsYPiCvsi428SNwte66Srvs4pHA", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Goonfi (Vybe)", dexCode: "goonfi", programId: "goonERTdGsjnkZqWuVjs73BZ3Pb9qoCUdBUL17BnS5j", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Goonfi V2 (Vybe)", dexCode: "goonfi_v2", programId: "goonuddtQRrWqqn5nFyczVKaie28f3kDkHWkHtURSLE", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Byreal (Vybe)", dexCode: "byreal", programId: "REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "BisonFi (Vybe)", dexCode: "bisonfi", programId: "BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "FusionAMM (Vybe)", dexCode: "fusionamm", programId: "fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Woofi (Vybe)", dexCode: "woofi", programId: "WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Aquifer (Vybe)", dexCode: "aquifer", programId: "AQU1FRd7papthgdrwPTTq5JacJh8YtwEXaBfKU3bTz45", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "Humidifi (Vybe)", dexCode: "humidifi", programId: "9H6tua7jkLhdm3w8BvgpTn5LZNU7g4ZynDmCiNN3q6Rp", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "SolFi V2 (Vybe)", dexCode: "solfi_v2", programId: "SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF", description: "Entrée Vybe; à vérifier par corpus local." },
    { label: "DexLab", dexCode: "dexlab", programId: "DSwpgjMvXhtGn6BsbqmacdBZyfLj6jSWf3HJpdJtmg6N", description: "DexLab Swap/Pool." },
    { label: "Aldrin (historical)", dexCode: "aldrin", programId: "AMM55ShdkoGRB5jVYPjWziwk8m5MpwyDgsMWHaMSQWH6", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Aldrin V2 (historical)", dexCode: "aldrin_v2", programId: "CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Crema (historical)", dexCode: "crema", programId: "CLMM9tUoggJu2wagPkkqs9eFG4BWhVBZWkP1qv3Sp7tR", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Cropper (historical)", dexCode: "cropper", programId: "H8W3ctz92svYg6mkn1UtGfu2aQr2fnUFHM1RhScEtQDt", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Lifinity V1 (historical)", dexCode: "lifinity_v1", programId: "EewxydAPCCVuNEyrVN68PuSYdQ7wKn27V9Gjeoi8dy3S", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Lifinity V2 (historical)", dexCode: "lifinity_v2", programId: "2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Mercurial (historical)", dexCode: "mercurial", programId: "MERLuDFBMmsHnsBPZw2sDQZHvXFMwp8EdjudcU2HKky", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Orca V1 (historical)", dexCode: "orca_v1", programId: "DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Orca V2 (historical)", dexCode: "orca_v2", programId: "9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Phoenix (historical)", dexCode: "phoenix", programId: "PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Saber (historical)", dexCode: "saber", programId: "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "OpenBook V2 (historical)", dexCode: "openbook_v2", programId: "opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Fox (historical)", dexCode: "fox", programId: "HyhpEq587ANShDdbx1mP4dTmDZC44CXWft29oYQXDb53", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Sanctum Infinity (historical)", dexCode: "sanctum_infinity", programId: "5ocnV1qiCgaQR8Jb8xWnVbApfaygJ8tNoZfgPwsgx9kx", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Saros (historical)", dexCode: "saros", programId: "SSwapUtytfBdBn1b9NUGG6foMVPtcWgpRU32HToDUZr", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Perps (historical)", dexCode: "perps", programId: "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "StepN (historical)", dexCode: "stepn", programId: "Dooar9JkhdZ7J3LHN3A7YCuoGRUggXhQaG4kijfLGU2j", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Solayer (historical)", dexCode: "solayer", programId: "endoLNCKTqDn8gSVnN2hDdpgACUPWHZTwoYnnMybpAT", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Penguin (historical)", dexCode: "penguin", programId: "PSwapMdSai8tjrEXcxFeQth87xC4rRsa4VA5mhGhXkP", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Sanctum (historical)", dexCode: "sanctum", programId: "stkitrT1Uoy18Dk1fTrgPw8W6MVzoCfYoAFT4MLsmhq", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Guacswap (historical)", dexCode: "guacswap", programId: "Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Oasis (historical)", dexCode: "oasis", programId: "9tKE7Mbmj4mxDjWatikzGAtkoWosiiZX9y6J4Hfm2R8H", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Saber Decimals (historical)", dexCode: "saber_decimals", programId: "DecZY86MU5Gj7kppfUCEmd4LbXXuyZH1yHaP2NTqdiZB", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Stabble Stable Swap (historical)", dexCode: "stabble_stable_swap", programId: "swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Stabble Weighted Swap (historical)", dexCode: "stabble_weighted_swap", programId: "swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "1DEX (historical)", dexCode: "one_dex", programId: "DEXYosS6oEGvk8uCDayvwEZz4qEyDJRf9nFgYCaqPMTm", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "SolFi (historical)", dexCode: "solfi", programId: "SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Moonshot (historical)", dexCode: "moonshot", programId: "MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Bonkswap (historical)", dexCode: "bonkswap", programId: "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Invariant (historical)", dexCode: "invariant", programId: "HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Token Swap (historical)", dexCode: "token_swap", programId: "SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Helium Network (historical)", dexCode: "helium_network", programId: "treaf4wWBBty3fHdyBpo35Mz84M8k3heKXmjmi9vFt5", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Marinade Liquid Staking (historical)", dexCode: "marinade_liquid_staking", programId: "MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Francium Yield Pools (historical)", dexCode: "francium_yield_pools", programId: "FC81tbGt6JWRXidaWYFXxGnTk4VgobhJHATvTRVMqgWj", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Marinade Governance (historical)", dexCode: "marinade_governance", programId: "GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Serum DAO (historical)", dexCode: "serum_dao", programId: "SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Port Finance (historical)", dexCode: "port_finance", programId: "Port7uDYB3wk6GJAw4KT1WpTeMtSu9bTcChBHkX2LfR", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Solend Classic (historical)", dexCode: "solend_classic", programId: "So1endDq2YkqhipRh3WViPa8hdiSpxWy6z3Z6tMCpAo", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Hyperspace NFT AMM (historical)", dexCode: "hyperspace_nft_amm", programId: "HYPERfwdTjyJ2SCaKHmpF2MtrXqWxrsotYDsTrshHWq8", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Magic Eden NFT AMM (historical)", dexCode: "magic_eden_nft_amm", programId: "MEisE1HzehtrDpAAT8PnLHjpSSkRYakotTuJRPjTpo8", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Raydium Staking Early (historical)", dexCode: "raydium_staking_early", programId: "EhhTKczWMGQt46ynNeRX1WfeagwwJd7ufHvCDjRxjo5Q", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Orca Aquafarm V1 (historical)", dexCode: "orca_aquafarm_v1", programId: "82yxjeMsvaURa4MbZZ7WZZHfobirZYkH1zF8fmeGtyaQ", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Quarry Merge Mining (historical)", dexCode: "quarry_merge_mining", programId: "QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", description: "Program id historique importé depuis entities.py; à vérifier par corpus." },
    { label: "Printr", dexCode: "printr", programId: "T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint", description: "Candidat Printr; à vérifier par corpus local." },
    { label: "metaDAO — umbrella (manual)", dexCode: "metadao", programId: "", description: "Umbrella MetaDAO. Choisir de préférence une surface spécifique ci-dessous." },
    { label: "metaDAO Launchpad v0.7.0", dexCode: "metadao_launchpad_v0_7_0", programId: "moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM", description: "Programme MetaDAO officiel candidat; à vérifier par corpus local." },
    { label: "metaDAO Bid Wall v0.7.0", dexCode: "metadao_bid_wall_v0_7_0", programId: "WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx", description: "Programme MetaDAO officiel candidat; à vérifier par corpus local." },
    { label: "metaDAO Futarchy v0.6.0", dexCode: "metadao_futarchy_v0_6_0", programId: "FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq", description: "Programme MetaDAO officiel candidat; à vérifier par corpus local." },
    { label: "metaDAO AMM v0.5.0", dexCode: "metadao_amm_v0_5_0", programId: "AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ", description: "Programme MetaDAO officiel candidat; à vérifier par corpus local." },
];

let lastResultJson = "";

function byId<T extends HTMLElement>(id: string): T {
    const element = document.getElementById(id);
    if (element === null) {
        throw new Error(`missing element #${id}`);
    }
    return element as T;
}

function valueOrNull(value: string): string | null {
    const trimmed = value.trim();
    return trimmed === "" ? null : trimmed;
}

function invalidBase58Characters(value: string): string[] {
    const invalid = new Set<string>();
    for (const character of value.trim()) {
        if (!/^[1-9A-HJ-NP-Za-km-z]$/.test(character)) {
            invalid.add(character);
        }
    }
    return Array.from(invalid);
}

function isSolanaAddressLike(value: string): boolean {
    const trimmed = value.trim();
    if (trimmed.length < 32 || trimmed.length > 44) {
        return false;
    }
    return /^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed);
}

function splitSourceAddresses(value: string): string[] {
    const seen = new Set<string>();
    const addresses: string[] = [];
    for (const token of value.split(/[\s,;]+/g)) {
        const trimmed = token.trim();
        if (trimmed === "" || seen.has(trimmed)) {
            continue;
        }
        seen.add(trimmed);
        addresses.push(trimmed);
    }
    return addresses;
}

function isSolanaSignatureLike(value: string): boolean {
    const trimmed = value.trim();
    if (trimmed.length < 64 || trimmed.length > 128) {
        return false;
    }
    return /^[1-9A-HJ-NP-Za-km-z]+$/.test(trimmed);
}

function validateOptionalSignature(value: string | null, label: string): void {
    if (value === null || value.trim() === "") {
        return;
    }
    if (!isSolanaSignatureLike(value)) {
        throw new Error(`${label} must be a valid Solana transaction signature.`);
    }
}

function validateOnchainRequest(request: Demo3OnchainDexDiscoveryRequest): void {
    const addresses = request.sourceAddresses ?? [];
    if (request.signatureSource === "address" && addresses.length === 0) {
        throw new Error("Signature source is 'address': provide at least one Solana account, pool, vault, position, config or mint address.");
    }
    for (const address of addresses) {
        if (!isSolanaAddressLike(address)) {
            throw new Error(`Invalid source address '${address}'. Provide Solana account addresses separated by commas, spaces or new lines.`);
        }
    }
    validateOptionalSignature(request.beforeSignature, "Before signature");
    validateOptionalSignature(request.untilSignature, "Until signature");
    if (request.programId !== null && !isSolanaAddressLike(request.programId)) {
        const invalidCharacters = invalidBase58Characters(request.programId);
        if (invalidCharacters.length > 0) {
            throw new Error(`Program id filter must be a valid Solana base58 program id. Invalid character(s): ${invalidCharacters.join(", ")}.`);
        }
        throw new Error("Program id filter must be a valid Solana program id, or empty when using a preset that resolves it.");
    }
    validateDiscriminatorFilter(request.targetDiscriminatorHex);
}

function validateDiscriminatorFilter(value: string | null): void {
    if (value === null || value.trim() === "") {
        return;
    }
    const tokens = value.split(/[\s,]+/).map((token) => token.trim()).filter((token) => token !== "");
    for (const token of tokens) {
        const normalized = token.startsWith("0x") || token.startsWith("0X") ? token.slice(2) : token;
        if (!/^[0-9a-fA-F]{16}$/.test(normalized)) {
            throw new Error(`Target discriminator '${token}' must be exactly 8 bytes / 16 hex characters.`);
        }
    }
}

function numberValueOrNull(value: string): number | null {
    const trimmed = value.trim();
    if (trimmed === "") {
        return null;
    }
    const parsed = Number.parseInt(trimmed, 10);
    return Number.isFinite(parsed) ? parsed : null;
}

function intValue(id: string, fallback: number): number {
    const parsed = Number.parseInt(byId<HTMLInputElement>(id).value, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function selectedTargetEvents(): string[] {
    return Array.from(document.querySelectorAll<HTMLInputElement>('input[name="demo3TargetEventInput"]:checked'))
        .map((input) => input.value.trim())
        .filter((value) => value !== "");
}

function readTargetEventFilter(): string | null {
    const selected = selectedTargetEvents();
    return selected.length === 0 ? null : selected.join(",");
}

function targetEventLabel(targetEvent: string | null): string {
    return targetEvent === null || targetEvent.trim() === "" ? "any" : targetEvent;
}

function clearTargetEventFilters(): void {
    document.querySelectorAll<HTMLInputElement>('input[name="demo3TargetEventInput"]').forEach((input) => {
        input.checked = false;
    });
}

function escapeHtml(value: string): string {
    return value
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

function shortText(value: string | null, maxLength: number): string {
    if (value === null) {
        return "-";
    }
    if (value.length <= maxLength) {
        return value;
    }
    return `${value.slice(0, maxLength)}…`;
}

function shortList(values: string[], maxItems: number, itemLength: number): string {
    if (values.length === 0) {
        return "-";
    }
    return values.slice(0, maxItems).map((value) => shortText(value, itemLength)).join(", ");
}

function candidateAccountList(accounts: Demo3OnchainDexPairCandidate["candidatePoolAccounts"], maxItems: number): string {
    if (accounts.length === 0) {
        return "-";
    }
    return accounts.slice(0, maxItems).map((account) => shortText(account.address, 14)).join(", ");
}

function tokenDeltaList(candidate: Demo3OnchainDexPairCandidate): string {
    if (candidate.tokenBalanceDeltas.length === 0) {
        return "-";
    }
    return candidate.tokenBalanceDeltas.slice(0, 4).map((delta) => {
        const amount = delta.deltaRaw ?? "?";
        return `${shortText(delta.mint, 10)}:${amount}`;
    }).join(", ");
}

function appendLogLine(line: string): void {
    const textarea = byId<HTMLTextAreaElement>("demo3LogTextarea");
    const timestamp = new Date().toLocaleTimeString("fr-CH", { hour12: false });
    const lines = textarea.value === "" ? [] : textarea.value.split("\n");
    lines.push(`[${timestamp}] ${line}`);
    textarea.value = lines.slice(-400).join("\n");
    textarea.scrollTop = textarea.scrollHeight;
}

function setStatus(label: string, cssClass: string): void {
    const badge = byId<HTMLElement>("demo3StatusBadge");
    badge.className = `badge ${cssClass}`;
    badge.textContent = label;
}

function populatePresetSelect(): void {
    const select = byId<HTMLSelectElement>("demo3PresetSelect");
    select.innerHTML = '<option value="">Custom / empty</option>';
    presets.forEach((preset, index) => {
        const option = document.createElement("option");
        option.value = String(index);
        option.textContent = preset.label;
        select.appendChild(option);
    });
}

function applyPreset(indexText: string): void {
    if (indexText === "") {
        return;
    }
    const index = Number.parseInt(indexText, 10);
    if (!Number.isFinite(index) || index < 0 || index >= presets.length) {
        return;
    }
    const preset = presets[index];
    byId<HTMLInputElement>("demo3DexCodeInput").value = preset.dexCode;
    byId<HTMLInputElement>("demo3ProgramIdInput").value = preset.programId;
    byId<HTMLElement>("demo3PresetHelp").textContent = preset.description;
}

function readOnchainRequest(): Demo3OnchainDexDiscoveryRequest {
    const sourceAddresses = splitSourceAddresses(byId<HTMLTextAreaElement>("demo3SourceAddressInput").value);
    return {
        dexCode: valueOrNull(byId<HTMLInputElement>("demo3DexCodeInput").value),
        programId: valueOrNull(byId<HTMLInputElement>("demo3ProgramIdInput").value),
        signatureSource: valueOrNull(byId<HTMLSelectElement>("demo3SignatureSourceSelect").value),
        sourceAddress: sourceAddresses.length === 1 ? sourceAddresses[0] : null,
        sourceAddresses,
        beforeSignature: valueOrNull(byId<HTMLInputElement>("demo3BeforeSignatureInput").value),
        untilSignature: valueOrNull(byId<HTMLInputElement>("demo3UntilSignatureInput").value),
        maxPages: intValue("demo3MaxPagesInput", 1),
        scanOrder: valueOrNull(byId<HTMLSelectElement>("demo3ScanOrderSelect").value),
        targetEvent: readTargetEventFilter(),
        targetInstructionName: valueOrNull(byId<HTMLInputElement>("demo3TargetInstructionNameInput").value),
        targetDiscriminatorHex: valueOrNull(byId<HTMLInputElement>("demo3TargetDiscriminatorHexInput").value),
        excludeSwaps: byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked,
        includeFailed: byId<HTMLInputElement>("demo3IncludeFailedInput").checked,
        httpRole: byId<HTMLInputElement>("demo3HttpRoleInput").value.trim() || "history_backfill",
        signatureLimit: intValue("demo3SignatureLimitInput", 50),
        transactionLimit: intValue("demo3TransactionLimitInput", 25),
        candidateLimit: intValue("demo3CandidateLimitInput", 25),
    };
}

function readLocalRequest(): Demo3LocalDexCorpusSearchRequest {
    return {
        dexCode: valueOrNull(byId<HTMLInputElement>("demo3DexCodeInput").value),
        programId: valueOrNull(byId<HTMLInputElement>("demo3ProgramIdInput").value),
        pairId: numberValueOrNull(byId<HTMLInputElement>("demo3PairIdInput").value),
        poolAddress: valueOrNull(byId<HTMLInputElement>("demo3PoolAddressInput").value),
        tokenMint: valueOrNull(byId<HTMLInputElement>("demo3TokenMintInput").value),
        signature: valueOrNull(byId<HTMLInputElement>("demo3SignatureInput").value),
        limit: intValue("demo3CandidateLimitInput", 25),
    };
}

function clearFilters(): void {
    byId<HTMLInputElement>("demo3DexCodeInput").value = "";
    byId<HTMLInputElement>("demo3ProgramIdInput").value = "";
    byId<HTMLSelectElement>("demo3SignatureSourceSelect").value = "program_id";
    byId<HTMLTextAreaElement>("demo3SourceAddressInput").value = "";
    byId<HTMLInputElement>("demo3BeforeSignatureInput").value = "";
    byId<HTMLInputElement>("demo3UntilSignatureInput").value = "";
    byId<HTMLInputElement>("demo3MaxPagesInput").value = "1";
    byId<HTMLSelectElement>("demo3ScanOrderSelect").value = "newest_first";
    byId<HTMLInputElement>("demo3PairIdInput").value = "";
    byId<HTMLInputElement>("demo3PoolAddressInput").value = "";
    byId<HTMLInputElement>("demo3TokenMintInput").value = "";
    byId<HTMLInputElement>("demo3SignatureInput").value = "";
    clearTargetEventFilters();
    byId<HTMLInputElement>("demo3ExcludeSwapsInput").checked = false;
    byId<HTMLInputElement>("demo3IncludeFailedInput").checked = true;
    byId<HTMLSelectElement>("demo3PresetSelect").value = "";
    byId<HTMLElement>("demo3PresetHelp").textContent = "Choisis un DEX ou saisis un program id manuellement.";
}

function renderOnchainResult(result: Demo3OnchainDexDiscoveryResult): void {
    byId<HTMLElement>("demo3SummarySignatureCount").textContent = String(result.fetchedSignatureCount);
    byId<HTMLElement>("demo3SummaryUniqueFetchedSignatureCount").textContent = String(result.uniqueFetchedSignatureCount);
    byId<HTMLElement>("demo3SummaryFetchedPageCount").textContent = String(result.fetchedSignaturePageCount);
    byId<HTMLElement>("demo3SummaryUniqueSignatureCount").textContent = String(result.uniqueSignatureCount);
    byId<HTMLElement>("demo3SummaryFetchedTxCount").textContent = String(result.fetchedTransactionCount);
    byId<HTMLElement>("demo3SummaryMissingTxCount").textContent = String(result.missingTransactionCount);
    byId<HTMLElement>("demo3SummaryFailedTxCount").textContent = String(result.failedTransactionCount);
    byId<HTMLElement>("demo3SummarySkippedFailedTxCount").textContent = String(result.skippedFailedTransactionCount);
    byId<HTMLElement>("demo3SummarySkippedSwapTxCount").textContent = String(result.skippedSwapLogTransactionCount);
    byId<HTMLElement>("demo3SummaryExtractedCandidateCount").textContent = String(result.extractedCandidateCount);
    byId<HTMLElement>("demo3SummaryRejectedCandidateCount").textContent = String(result.targetRejectedCandidateCount);
    byId<HTMLElement>("demo3SummaryCandidateCount").textContent = String(result.candidateCount);
    const targetEvent = targetEventLabel(result.request.targetEvent);
    const targetInstruction = result.request.targetInstructionName ?? "any";
    const targetDiscriminator = result.request.targetDiscriminatorHex ?? "any";
    const sourceText = result.resolvedSignatureAddresses.length === 0 ? result.resolvedSignatureAddress : result.resolvedSignatureAddresses.join(",");
    byId<HTMLElement>("demo3TargetText").textContent = `${result.resolvedDexCode ?? "custom"} / program=${result.resolvedProgramId} / source=${result.resolvedSignatureSource}:${sourceText} / target=${targetEvent} / instr=${targetInstruction} / disc=${targetDiscriminator} / order=${result.request.scanOrder ?? "newest_first"}`;
    byId<HTMLElement>("demo3UniqueSignatureText").textContent = result.uniqueBackfillSignatures.length === 0 ? "-" : result.uniqueBackfillSignatures.join(", ");
    byId<HTMLElement>("demo3NextBeforeText").textContent = result.nextBeforeByAddress.length === 0 ? "-" : result.nextBeforeByAddress.map((cursor) => `${cursor.address}:${cursor.nextBeforeSignature ?? "-"}`).join(" | ");
    renderRejectedSummary(result);
    renderOnchainCandidates(result.candidates);
}

function renderRejectedSummary(result: Demo3OnchainDexDiscoveryResult): void {
    const body = byId<HTMLTableSectionElement>("demo3RejectedSummaryTableBody");
    if (result.rejectedCandidateSummary.length === 0) {
        body.innerHTML = '<tr><td colspan="5" class="text-body-secondary">No rejected candidate summary.</td></tr>';
        return;
    }
    body.innerHTML = result.rejectedCandidateSummary.map((summary) => `
        <tr>
            <td>${escapeHtml(summary.candidateKind)}</td>
            <td class="font-monospace" title="${escapeHtml(summary.instructionDataPrefix ?? "")}">${escapeHtml(shortText(summary.instructionDataPrefix, 16))}</td>
            <td>${escapeHtml(summary.instructionName ?? "-")}</td>
            <td>${escapeHtml(summary.rejectionReason)}</td>
            <td>${summary.count}</td>
        </tr>`).join("");
}

function renderOnchainCandidates(candidates: Demo3OnchainDexPairCandidate[]): void {
    const body = byId<HTMLTableSectionElement>("demo3OnchainCandidateTableBody");
    if (candidates.length === 0) {
        body.innerHTML = '<tr><td colspan="13" class="text-body-secondary">No on-chain candidate.</td></tr>';
        return;
    }
    body.innerHTML = candidates.map((candidate) => {
        const verifiedPool = candidate.verifiedPoolAddress;
        const firstCandidatePool = candidate.candidatePoolAccounts.length > 0 ? candidate.candidatePoolAccounts[0].address : null;
        const poolForFilter = verifiedPool ?? candidate.poolAddress ?? firstCandidatePool;
        if (poolForFilter !== null) {
            byId<HTMLInputElement>("demo3PoolAddressInput").value = poolForFilter;
        }
        if (candidate.tokenAMint !== null && byId<HTMLInputElement>("demo3TokenMintInput").value.trim() === "") {
            byId<HTMLInputElement>("demo3TokenMintInput").value = candidate.tokenAMint;
        }
        if (candidate.tokenAMint === null && candidate.observedTokenMints.length > 0 && byId<HTMLInputElement>("demo3TokenMintInput").value.trim() === "") {
            byId<HTMLInputElement>("demo3TokenMintInput").value = candidate.observedTokenMints[0];
        }
        byId<HTMLInputElement>("demo3SignatureInput").value = candidate.signature;
        const accountTitle = candidate.candidatePoolAccounts.map((account) => `${account.address} (${account.reason})`).join("\n");
        const vaultTitle = candidate.candidateTokenVaultAccounts.map((account) => `${account.address} (${account.reason})`).join("\n");
        return `
            <tr>
                <td class="font-monospace" title="${escapeHtml(candidate.signature)}">${escapeHtml(shortText(candidate.signature, 18))}</td>
                <td>${candidate.slot ?? "-"}</td>
                <td><span class="badge text-bg-info">${escapeHtml(candidate.candidateKind)}</span></td>
                <td><span class="badge text-bg-${candidate.confidence === "high" ? "success" : candidate.confidence === "medium" ? "warning" : "secondary"}">${escapeHtml(candidate.confidence)}</span></td>
                <td class="font-monospace" title="${escapeHtml(candidate.instructionDataPrefix ?? "")}">${escapeHtml(shortText(candidate.instructionDataPrefix, 14))}</td>
                <td class="font-monospace" title="${escapeHtml(candidate.instructionDiscriminatorHex ?? "")}">${escapeHtml(shortText(candidate.instructionDiscriminatorHex, 16))}</td>
                <td class="font-monospace" title="${escapeHtml(verifiedPool ?? "")}">${escapeHtml(shortText(verifiedPool, 14))}</td>
                <td class="font-monospace" title="${escapeHtml(candidate.tokenAMint ?? "")}">${escapeHtml(shortText(candidate.tokenAMint, 14))}</td>
                <td class="font-monospace" title="${escapeHtml(candidate.tokenBMint ?? "")}">${escapeHtml(shortText(candidate.tokenBMint, 14))}</td>
                <td class="font-monospace" title="${escapeHtml(candidate.observedTokenMints.join("\n"))}">${escapeHtml(shortList(candidate.observedTokenMints, 3, 10))}</td>
                <td class="font-monospace" title="${escapeHtml(tokenDeltaList(candidate))}">${escapeHtml(tokenDeltaList(candidate))}</td>
                <td class="font-monospace" title="${escapeHtml(accountTitle)}">${escapeHtml(candidateAccountList(candidate.candidatePoolAccounts, 3))}<br /><span class="text-body-secondary">vaults: ${escapeHtml(candidateAccountList(candidate.candidateTokenVaultAccounts, 2))}</span></td>
                <td class="small" title="${escapeHtml(vaultTitle)}">${escapeHtml(candidate.backfillHint)}</td>
            </tr>`;
    }).join("");
}

function renderLocalResult(result: Demo3LocalDexCorpusSearchResult): void {
    byId<HTMLElement>("demo3SummaryLocalPairCount").textContent = String(result.summary.pairCount);
    const body = byId<HTMLTableSectionElement>("demo3LocalPoolPairTableBody");
    if (result.poolPairSamples.length === 0) {
        body.innerHTML = '<tr><td colspan="8" class="text-body-secondary">No local pool/pair sample.</td></tr>';
        return;
    }
    body.innerHTML = result.poolPairSamples.map((sample) => `
        <tr>
            <td>${escapeHtml(sample.dexCode ?? "-")}</td>
            <td class="font-monospace" title="${escapeHtml(sample.poolAddress ?? "")}">${escapeHtml(shortText(sample.poolAddress, 16))}</td>
            <td>${sample.pairId ?? "-"}</td>
            <td>${escapeHtml(sample.pairSymbol ?? "-")}</td>
            <td class="font-monospace" title="${escapeHtml(sample.baseMint ?? "")}">${escapeHtml(sample.baseSymbol ?? shortText(sample.baseMint, 12))}</td>
            <td class="font-monospace" title="${escapeHtml(sample.quoteMint ?? "")}">${escapeHtml(sample.quoteSymbol ?? shortText(sample.quoteMint, 12))}</td>
            <td>${sample.tradeEventCount}</td>
            <td>${sample.pairCandleCount}</td>
        </tr>`).join("");
}

async function discoverOnchain(): Promise<void> {
    const request = readOnchainRequest();
    try {
        validateOnchainRequest(request);
    } catch (error) {
        setStatus("error", "text-bg-danger");
        appendLogLine(`on-chain discovery rejected locally: ${String(error)}`);
        return;
    }
    setStatus("running", "text-bg-warning");
    appendLogLine(`on-chain discovery dex='${request.dexCode ?? ""}' program='${request.programId ?? ""}' source='${request.signatureSource ?? "program_id"}:${request.sourceAddresses.join(",")}' target='${targetEventLabel(request.targetEvent)}' instruction='${request.targetInstructionName ?? ""}' discriminator='${request.targetDiscriminatorHex ?? ""}' pages='${request.maxPages}' order='${request.scanOrder ?? "newest_first"}' before='${request.beforeSignature ?? ""}' until='${request.untilSignature ?? ""}' excludeSwaps='${request.excludeSwaps}' role='${request.httpRole}'`);
    try {
        const payload = await invoke<Demo3OnchainDexDiscoveryPayload>("demo3_discover_onchain_dex_pairs", { request });
        lastResultJson = payload.resultJson;
        byId<HTMLTextAreaElement>("demo3JsonTextarea").value = payload.resultJson;
        renderOnchainResult(payload.result);
        setStatus("ok", "text-bg-success");
        appendLogLine(`on-chain discovery completed: candidates='${payload.result.candidateCount}' unique='${payload.result.uniqueSignatureCount}' signatures='${payload.result.fetchedSignatureCount}' uniqueFetched='${payload.result.uniqueFetchedSignatureCount}' pages='${payload.result.fetchedSignaturePageCount}' extracted='${payload.result.extractedCandidateCount}' rejected='${payload.result.targetRejectedCandidateCount}' skippedSwapTx='${payload.result.skippedSwapLogTransactionCount}'`);
    } catch (error) {
        setStatus("error", "text-bg-danger");
        appendLogLine(`on-chain discovery failed: ${String(error)}`);
    }
}

async function searchLocalDb(): Promise<void> {
    const request = readLocalRequest();
    setStatus("running", "text-bg-warning");
    appendLogLine("local DB search started");
    try {
        const payload = await invoke<Demo3LocalDexCorpusSearchPayload>("demo3_search_local_dex_corpus", { request });
        lastResultJson = payload.resultJson;
        byId<HTMLTextAreaElement>("demo3JsonTextarea").value = payload.resultJson;
        renderLocalResult(payload.result);
        setStatus("ok", "text-bg-success");
        appendLogLine(`local DB search completed: pairs='${payload.result.summary.pairCount}' tx='${payload.result.summary.transactionCount}'`);
    } catch (error) {
        setStatus("error", "text-bg-danger");
        appendLogLine(`local DB search failed: ${String(error)}`);
    }
}

async function copyJson(): Promise<void> {
    if (lastResultJson === "") {
        appendLogLine("nothing to copy");
        return;
    }
    try {
        await navigator.clipboard.writeText(lastResultJson);
        appendLogLine("JSON copied to clipboard");
    } catch (error) {
        appendLogLine(`copy failed: ${String(error)}`);
    }
}

function init(): void {
    takeoverConsole();
    void debug("demo3 on-chain discovery initialized");
    debug("demo3 window loaded");

    const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
    if (sidebarToggle) {
        // restaurer l’état depuis localStorage
        if (localStorage.getItem('sidebar-toggle') === 'true') {
            document.body.classList.add('sidenav-toggled');
        }

        sidebarToggle.addEventListener('click', (event) => {
            event.preventDefault();
            document.body.classList.toggle('sidenav-toggled');
            localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
        });
    }

    const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
    Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
    const toastElList = document.querySelectorAll('.toast');
    Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
    const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
    Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));

    const gobackto = location.pathname + location.search;

    document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
        const href = a.getAttribute("href");
        if (!href) return; // pas de href => on ignore

        const url = new URL(href, location.origin);
        url.searchParams.set("gobackto", gobackto);

        // conserve une URL relative (path + query)
        a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
    });

    populatePresetSelect();
    byId<HTMLSelectElement>("demo3PresetSelect").addEventListener("change", (event) => {
        applyPreset((event.target as HTMLSelectElement).value);
    });
    byId<HTMLButtonElement>("demo3DiscoverButton").addEventListener("click", () => {
        void discoverOnchain();
    });
    byId<HTMLButtonElement>("demo3LocalSearchButton").addEventListener("click", () => {
        void searchLocalDb();
    });
    byId<HTMLButtonElement>("demo3ClearFiltersButton").addEventListener("click", clearFilters);
    byId<HTMLButtonElement>("demo3CopyJsonButton").addEventListener("click", () => {
        void copyJson();
    });
    byId<HTMLButtonElement>("demo3ClearLogButton").addEventListener("click", () => {
        byId<HTMLTextAreaElement>("demo3LogTextarea").value = "";
    });
    appendLogLine("ready");
}

document.addEventListener("DOMContentLoaded", init);
