// file: kb_demo_app/frontend/ts/demo_pipeline2.ts

import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import * as echarts from "echarts";
import { invoke } from "@tauri-apps/api/core";
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";

import type { DemoPipeline2CatalogPayload } from "./bindings/DemoPipeline2CatalogPayload.ts";
import type { DemoPipeline2BackfillTokenRequest } from "./bindings/DemoPipeline2BackfillTokenRequest.ts";
import type { DemoPipeline2BackfillPoolRequest } from "./bindings/DemoPipeline2BackfillPoolRequest.ts";
import type { DemoPipeline2BackfillSignatureRequest } from "./bindings/DemoPipeline2BackfillSignatureRequest.ts";
import type { DemoPipeline2BackfillSignaturesBatchRequest } from "./bindings/DemoPipeline2BackfillSignaturesBatchRequest.ts";
import type { DemoPipeline2BackfillPayload } from "./bindings/DemoPipeline2BackfillPayload.ts";
import type { DemoPipeline2PairCandlesRequest } from "./bindings/DemoPipeline2PairCandlesRequest.ts";
import type { DemoPipeline2PairCandlesPayload } from "./bindings/DemoPipeline2PairCandlesPayload.ts";
import type { DemoPipeline2LocalDiagnosticsPayload } from "./bindings/DemoPipeline2LocalDiagnosticsPayload.ts";
import type { DemoPipeline2LocalValidationPayload } from "./bindings/DemoPipeline2LocalValidationPayload.ts";
import type { DemoPipeline2LocalValidationRequest } from "./bindings/DemoPipeline2LocalValidationRequest.ts";
import type { DemoPipeline2ProgramInstructionDiscriminatorSummaryRequest } from "./bindings/DemoPipeline2ProgramInstructionDiscriminatorSummaryRequest.ts";
import type { DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload } from "./bindings/DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload.ts";
import { DemoPipeline2ProtocolCandidateSummaryRequest } from './bindings/DemoPipeline2ProtocolCandidateSummaryRequest.ts';
import { DemoPipeline2ProtocolCandidateSummaryPayload } from './bindings/DemoPipeline2ProtocolCandidateSummaryPayload.ts';

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

interface PairCandle {
    id: number | null;
    pair_id: number;
    timeframe_seconds: number;
    bucket_start_unix: number;
    bucket_end_unix: number;
    open_price_quote_per_base: number;
    high_price_quote_per_base: number;
    low_price_quote_per_base: number;
    close_price_quote_per_base: number;
    trade_count: number;
    buy_count: number;
    sell_count: number;
    base_volume_raw: string | null;
    quote_volume_raw: string | null;
    first_trade_signature: string | null;
    last_trade_signature: string | null;
    created_at: string;
    updated_at: string;
}


interface LocalPipelineReplayResult {
    selectedTransactionCount: number;
    replayedTransactionCount: number;
    decodeErrorCount: number;
    detectErrorCount: number;
    tradeAggregationErrorCount: number;
    nonTradeMaterializationErrorCount: number;
    pairCandleErrorCount: number;
    analyticSignalErrorCount: number;
    decodedEventCount: number;
    decodeSkippedCount: number;
    decodeSkippedEventCount: number;
    decodeLedgerUpsertCount: number;
    decodeLedgerUnsafeCount: number;
    detectionCount: number;
    tradeEventCount: number;
    liquidityEventCount: number;
    poolLifecycleEventCount: number;
    feeEventCount: number;
    rewardEventCount: number;
    poolAdminEventCount: number;
    tokenAccountEventCount: number;
    pairCandleUpsertCount: number;
    analyticSignalUpsertCount: number;
    transactionClassificationCount: number;
    transactionClassificationErrorCount: number;
    tokenMetadataUpdatedCount: number;
    pairSymbolUpdatedCount: number;
    resetMarketMaterializationDeletedCount: number;
    instructionObservationScannedCount: number;
    instructionObservationUpsertedCount: number;
    globalErrorCount: number;
}
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
    const now = new Date();
    const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });

    const lines = textarea.value === "" ? [] : textarea.value.split("\n");
    lines.push(`[${timestamp}] ${line}`);

    textarea.value = lines.slice(-300).join("\n");
    textarea.scrollTop = textarea.scrollHeight;
}

function setEmptyChart(
    chart: echarts.ECharts,
    chartMeta: HTMLElement,
    message: string,
): void {
    chartMeta.textContent = message;

    chart.setOption({
        animation: false,
        title: {
            text: message,
            left: "center",
            top: "middle",
            textStyle: {
                fontSize: 14,
                fontWeight: "normal",
            },
        },
        tooltip: {},
        xAxis: { show: false, type: "category", data: [] },
        yAxis: { show: false, type: "value" },
        series: [],
    }, true);
}

function readPositiveIntegerInput(
    input: HTMLInputElement,
    logTextarea: HTMLTextAreaElement,
    label: string,
): number | undefined {
    const text = input.value.trim();
    if (text === "") {
        appendLogLine(logTextarea, `[ui] ${label} is required`);
        return undefined;
    }

    const parsed = Number.parseInt(text, 10);
    if (Number.isNaN(parsed) || parsed <= 0) {
        appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
        return undefined;
    }

    return parsed;
}


function readOptionalPositiveIntegerInput(
    input: HTMLInputElement,
    logTextarea: HTMLTextAreaElement,
    label: string,
): number | null | undefined {
    const text = input.value.trim();
    if (text === "") {
        return null;
    }

    const parsed = Number.parseInt(text, 10);
    if (Number.isNaN(parsed) || parsed <= 0) {
        appendLogLine(logTextarea, `[ui] invalid ${label} '${text}'`);
        return undefined;
    }

    return parsed;
}
function parseSignatureBatchText(raw: string): string[] {
    const signatures: string[] = [];
    const seen = new Set<string>();

    for (const line of raw.split(/\r?\n/)) {
        const signature = line.trim();
        if (signature === "") {
            continue;
        }
        if (seen.has(signature)) {
            continue;
        }
        seen.add(signature);
        signatures.push(signature);
    }

    return signatures;
}

function refreshPairSelect(
    catalog: DemoPipeline2CatalogPayload,
    select: HTMLSelectElement,
): void {
    const previousValue = select.value;

    select.innerHTML = "";
    const emptyOption = document.createElement("option");
    emptyOption.value = "";
    emptyOption.textContent = "Aucune";
    select.appendChild(emptyOption);

    for (const pair of catalog.pairs) {
        const option = document.createElement("option");
        option.value = pair.pairId.toString();
        option.textContent = `#${pair.pairId.toString()} ${pair.symbol ?? ""} ${pair.poolAddress}`.trim();
        if (option.value === previousValue) {
            option.selected = true;
        }
        select.appendChild(option);
    }
}

function renderCatalogTextareas(
    catalog: DemoPipeline2CatalogPayload,
    tokensTextarea: HTMLTextAreaElement,
    poolsTextarea: HTMLTextAreaElement,
    pairsTextarea: HTMLTextAreaElement,
): void {
    tokensTextarea.value = JSON.stringify(catalog.tokens, null, 2);
    poolsTextarea.value = JSON.stringify(catalog.pools, null, 2);
    pairsTextarea.value = JSON.stringify(catalog.pairs, null, 2);
}


function toChartNumber(value: number | string | null | undefined): number {
    if (value === null || value === undefined) {
        return 0;
    }

    if (typeof value === "number") {
        return Number.isFinite(value) ? value : 0;
    }

    const parsed = Number.parseFloat(value);
    if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
        return 0;
    }

    return parsed;
}

function calculateVisibleWindowStart(totalCandles: number): number {
    if (totalCandles <= 90) {
        return 0;
    }

    return Math.max(0, ((totalCandles - 90) / totalCandles) * 100);
}

function parseCandlesJson(raw: string): PairCandle[] {
    if (raw.trim() === "") {
        return [];
    }

    try {
        return JSON.parse(raw) as PairCandle[];
    } catch {
        return [];
    }
}

function parseVolume(text: string | null, fallback: number): number {
    if (text === null || text.trim() === "") {
        return Number(fallback);
    }

    const parsed = Number.parseFloat(text);
    if (Number.isNaN(parsed)) {
        return Number(fallback);
    }

    return parsed;
}

function renderCandlesChart(
    chart: echarts.ECharts,
    chartMeta: HTMLElement,
    pairId: number,
    timeframeSeconds: number,
    candles: PairCandle[],
): void {
    if (candles.length === 0) {
        setEmptyChart(chart, chartMeta, "Aucune candle disponible.");
        return;
    }

    const sorted = [...candles].sort(
        (left, right) => left.bucket_start_unix - right.bucket_start_unix,
    );

    const categoryData = sorted.map((candle) =>
        new Date(Number(candle.bucket_start_unix) * 1000).toLocaleString("fr-CH", {
            hour12: false,
            year: "2-digit",
            month: "2-digit",
            day: "2-digit",
            hour: "2-digit",
            minute: "2-digit",
        }),
    );

    const ohlcData = sorted.map((candle) => [
        toChartNumber(candle.open_price_quote_per_base),
        toChartNumber(candle.close_price_quote_per_base),
        toChartNumber(candle.low_price_quote_per_base),
        toChartNumber(candle.high_price_quote_per_base),
    ]);

    const volumeData = sorted.map((candle) =>
        parseVolume(candle.quote_volume_raw, candle.trade_count),
    );

    const zoomStart = calculateVisibleWindowStart(sorted.length);

    chartMeta.textContent =
        `Pair #${pairId.toString()} • timeframe ${timeframeSeconds.toString()}s • ${sorted.length} candles`;

    chart.setOption({
        animation: false,
        legend: {
            data: ["OHLC", "Volume"],
            top: 4,
            left: 16,
        },
        tooltip: {
            trigger: "axis",
            axisPointer: {
                type: "cross",
            },
        },
        axisPointer: {
            link: [{ xAxisIndex: "all" }],
        },
        grid: [
            { left: 76, right: 32, top: 52, height: "58%" },
            { left: 76, right: 32, top: "77%", height: "12%" },
        ],
        xAxis: [
            {
                type: "category",
                data: categoryData,
                boundaryGap: true,
                axisLine: { onZero: false },
                axisTick: { alignWithLabel: true },
                splitLine: { show: true },
                axisLabel: { hideOverlap: true, margin: 14 },
                min: "dataMin",
                max: "dataMax",
            },
            {
                type: "category",
                gridIndex: 1,
                data: categoryData,
                boundaryGap: true,
                axisLine: { onZero: false },
                axisTick: { alignWithLabel: true },
                splitLine: { show: false },
                axisLabel: { hideOverlap: true, margin: 10 },
                min: "dataMin",
                max: "dataMax",
            },
        ],
        yAxis: [
            {
                scale: true,
                splitNumber: 5,
                splitArea: { show: false },
                axisLabel: { margin: 12 },
            },
            {
                gridIndex: 1,
                scale: true,
                splitNumber: 2,
                axisLabel: { margin: 12 },
            },
        ],
        dataZoom: [
            {
                type: "inside",
                xAxisIndex: [0, 1],
                filterMode: "none",
                start: zoomStart,
                end: 100,
            },
            {
                show: true,
                type: "slider",
                xAxisIndex: [0, 1],
                filterMode: "none",
                bottom: 8,
                height: 22,
                start: zoomStart,
                end: 100,
            },
        ],
        series: [
            {
                name: "OHLC",
                type: "candlestick",
                data: ohlcData,
                xAxisIndex: 0,
                yAxisIndex: 0,
                barMinWidth: 4,
                barMaxWidth: 16,
                itemStyle: {
                    borderWidth: 1.4,
                },
            },
            {
                name: "Volume",
                type: "bar",
                xAxisIndex: 1,
                yAxisIndex: 1,
                data: volumeData,
                barMinWidth: 2,
                barMaxWidth: 10,
            },
        ],
    }, true);

    window.setTimeout(() => chart.resize(), 0);
}

document.addEventListener("DOMContentLoaded", async () => {
    void takeoverConsole();
    debug("demo_pipeline2 window loaded");

    const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
    Array.from(tooltipTriggerList).map((tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl));

    const refreshCatalogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2RefreshCatalogButton");
    const tokensTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2TokensTextarea");
    const poolsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2PoolsTextarea");
    const pairsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2PairsTextarea");

    const httpRoleInput = document.querySelector<HTMLInputElement>("#demoPipeline2HttpRoleInput");
    const mintInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintInput");
    const mintSignatureLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintSignatureLimitInput");
    const mintPoolLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2MintPoolLimitInput");
    const backfillMintButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillMintButton");

    const poolInput = document.querySelector<HTMLInputElement>("#demoPipeline2PoolInput");
    const poolSignatureLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2PoolSignatureLimitInput");
    const backfillPoolButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillPoolButton");

    const signatureInput = document.querySelector<HTMLInputElement>("#demoPipeline2SignatureInput");
    const backfillSignatureButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillSignatureButton");
    const signatureBatchTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2SignatureBatchTextarea");
    const signatureBatchContinueOnErrorCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2SignatureBatchContinueOnErrorCheckbox");
    const backfillSignatureBatchButton = document.querySelector<HTMLButtonElement>("#demoPipeline2BackfillSignatureBatchButton");

    const replayLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayLimitInput");
    const replayMetadataCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayMetadataCheckbox");
    const replayMetadataLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayMetadataLimitInput");
    const replaySkipCertifiedDexDecodeCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2ReplaySkipCertifiedDexDecodeCheckbox");
    const replayForceDexDecodeCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayForceDexDecodeCheckbox");
    const replayDeferInstructionObservationCheckbox = document.querySelector<HTMLInputElement>("#demoPipeline2ReplayDeferInstructionObservationCheckbox");
    const replayLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ReplayLocalPipelineButton");
    const diagnoseLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2DiagnoseLocalPipelineButton");
    const validateLocalPipelineButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ValidateLocalPipelineButton");
    const validationProfileSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2ValidationProfileSelect");

    const discriminatorProgramIdInput = document.querySelector<HTMLInputElement>("#demoPipeline2DiscriminatorProgramIdInput");
    const discriminatorLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2DiscriminatorLimitInput");
    const loadDiscriminatorSummariesButton = document.querySelector<HTMLButtonElement>("#demoPipeline2LoadDiscriminatorSummariesButton");

    const protocolCandidateLimitInput = document.querySelector<HTMLInputElement>("#demoPipeline2ProtocolCandidateLimitInput");
    const refreshProtocolCandidatesButton = document.querySelector<HTMLButtonElement>("#demoPipeline2RefreshProtocolCandidatesButton");

    const pairSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2PairSelect");
    const timeframeSelect = document.querySelector<HTMLSelectElement>("#demoPipeline2TimeframeSelect");
    const customTimeframeInput = document.querySelector<HTMLInputElement>("#demoPipeline2CustomTimeframeInput");
    const preferMaterializedInput = document.querySelector<HTMLInputElement>("#demoPipeline2PreferMaterializedInput");
    const loadCandlesButton = document.querySelector<HTMLButtonElement>("#demoPipeline2LoadCandlesButton");

    const backfillSummaryTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2BackfillSummaryTextarea");
    const chartElement = document.querySelector<HTMLDivElement>("#demoPipeline2Chart");
    const chartMeta = document.querySelector<HTMLDivElement>("#demoPipeline2ChartMeta");
    const localDiagnosticsTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LocalDiagnosticsTextarea");
    const localValidationTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LocalValidationTextarea");

    const discriminatorSummariesTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2DiscriminatorSummariesTextarea");

    const protocolCandidateSummariesTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2ProtocolCandidateSummariesTextarea");

    const clearLogButton = document.querySelector<HTMLButtonElement>("#demoPipeline2ClearLogButton");
    const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoPipeline2LogTextarea");

    if (
        !refreshCatalogButton ||
        !tokensTextarea ||
        !poolsTextarea ||
        !pairsTextarea ||
        !httpRoleInput ||
        !mintInput ||
        !mintSignatureLimitInput ||
        !mintPoolLimitInput ||
        !backfillMintButton ||
        !poolInput ||
        !poolSignatureLimitInput ||
        !backfillPoolButton ||
        !signatureInput ||
        !backfillSignatureButton ||
        !signatureBatchTextarea ||
        !signatureBatchContinueOnErrorCheckbox ||
        !backfillSignatureBatchButton ||
        !replayLimitInput ||
        !replayMetadataCheckbox ||
        !replayMetadataLimitInput ||
        !replaySkipCertifiedDexDecodeCheckbox ||
        !replayForceDexDecodeCheckbox ||
        !replayDeferInstructionObservationCheckbox ||
        !replayLocalPipelineButton ||
        !diagnoseLocalPipelineButton ||
        !validateLocalPipelineButton ||
        !validationProfileSelect ||
        !discriminatorProgramIdInput ||
        !discriminatorLimitInput ||
        !loadDiscriminatorSummariesButton ||
        !protocolCandidateLimitInput ||
        !refreshProtocolCandidatesButton ||
        !pairSelect ||
        !timeframeSelect ||
        !customTimeframeInput ||
        !preferMaterializedInput ||
        !loadCandlesButton ||
        !backfillSummaryTextarea ||
        !localDiagnosticsTextarea ||
        !localValidationTextarea ||
        !discriminatorSummariesTextarea ||
        !protocolCandidateSummariesTextarea ||
        !chartElement ||
        !chartMeta ||
        !clearLogButton ||
        !logTextarea
    ) {
        console.error("demo_pipeline2 DOM is incomplete");
        return;
    }

    const safeTokensTextarea = tokensTextarea;
    const safePoolsTextarea = poolsTextarea;
    const safePairsTextarea = pairsTextarea;


    const safePairSelect = pairSelect;
    const safeChartElement = chartElement;
    const safeChartMeta = chartMeta;

    const safeLogTextarea = logTextarea;
    const safeLocalDiagnosticsTextarea = localDiagnosticsTextarea;

    const chart = echarts.init(safeChartElement);
    setEmptyChart(chart, safeChartMeta, "Aucune candle disponible.");
    window.addEventListener("resize", () => chart.resize());
    const chartCollapse = document.querySelector<HTMLDivElement>("#demoPipeline2ChartCollapse");
    chartCollapse?.addEventListener("shown.bs.collapse", () => {
        window.setTimeout(() => chart.resize(), 0);
    });

    clearLogButton.addEventListener("click", () => {
        logTextarea.value = "";
    });

    let currentCatalog: DemoPipeline2CatalogPayload | null = null;

    async function refreshCatalog(): Promise<void> {
        appendLogLine(safeLogTextarea, "[ui] refreshing local catalog");

        try {
            const catalog = await invoke<DemoPipeline2CatalogPayload>("demo_pipeline2_get_catalog");
            currentCatalog = catalog;

            renderCatalogTextareas(catalog, safeTokensTextarea, safePoolsTextarea, safePairsTextarea);
            refreshPairSelect(catalog, safePairSelect);

            appendLogLine(
                safeLogTextarea,
                `[ui] catalog refreshed: ${catalog.tokens.length} tokens, ${catalog.pools.length} pools, ${catalog.pairs.length} pairs`,
            );
        } catch (error) {
            appendLogLine(safeLogTextarea, `[ui] catalog refresh error: ${String(error)}`);
        }
    }

    refreshCatalogButton.addEventListener("click", () => {
        void refreshCatalog();
    });

    backfillMintButton.addEventListener("click", async () => {
        const tokenMint = mintInput.value.trim();
        if (tokenMint === "") {
            appendLogLine(logTextarea, "[ui] token mint is required");
            return;
        }

        const mintSignatureLimit = readPositiveIntegerInput(
            mintSignatureLimitInput,
            logTextarea,
            "mintSignatureLimit",
        );
        if (mintSignatureLimit === undefined) {
            return;
        }

        const poolSignatureLimit = readPositiveIntegerInput(
            mintPoolLimitInput,
            logTextarea,
            "poolSignatureLimit",
        );
        if (poolSignatureLimit === undefined) {
            return;
        }

        const httpRoleText = httpRoleInput.value.trim();
        const httpRole = httpRoleText === "" ? null : httpRoleText;

        appendLogLine(
            logTextarea,
            `[ui] launching token backfill for '${tokenMint}' with role '${httpRole ?? "history_backfill"}'`,
        );

        const request: DemoPipeline2BackfillTokenRequest = {
            tokenMint,
            httpRole,
            mintSignatureLimit,
            poolSignatureLimit,
        };

        try {
            const payload = await invoke<DemoPipeline2BackfillPayload>(
                "demo_pipeline2_backfill_token_mint",
                { request },
            );

            backfillSummaryTextarea.value = payload.summaryJson;
            currentCatalog = payload.catalog;
            renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
            refreshPairSelect(payload.catalog, pairSelect);

            appendLogLine(logTextarea, `[ui] token backfill completed for '${payload.objectKey}'`);
        } catch (error) {
            appendLogLine(logTextarea, `[ui] token backfill error: ${String(error)}`);
        }
    });

    backfillPoolButton.addEventListener("click", async () => {
        const poolAddress = poolInput.value.trim();
        if (poolAddress === "") {
            appendLogLine(logTextarea, "[ui] pool address is required");
            return;
        }

        const poolSignatureLimit = readPositiveIntegerInput(
            poolSignatureLimitInput,
            logTextarea,
            "poolSignatureLimit",
        );
        if (poolSignatureLimit === undefined) {
            return;
        }

        const httpRoleText = httpRoleInput.value.trim();
        const httpRole = httpRoleText === "" ? null : httpRoleText;

        appendLogLine(
            logTextarea,
            `[ui] launching pool backfill for '${poolAddress}' with role '${httpRole ?? "history_backfill"}'`,
        );

        const request: DemoPipeline2BackfillPoolRequest = {
            poolAddress,
            httpRole,
            poolSignatureLimit,
        };

        try {
            const payload = await invoke<DemoPipeline2BackfillPayload>(
                "demo_pipeline2_backfill_pool_address",
                { request },
            );

            backfillSummaryTextarea.value = payload.summaryJson;
            currentCatalog = payload.catalog;
            renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
            refreshPairSelect(payload.catalog, pairSelect);

            appendLogLine(logTextarea, `[ui] pool backfill completed for '${payload.objectKey}'`);
        } catch (error) {
            appendLogLine(logTextarea, `[ui] pool backfill error: ${String(error)}`);
        }
    });

    backfillSignatureButton.addEventListener("click", async () => {
        const signature = signatureInput.value.trim();
        if (signature === "") {
            appendLogLine(logTextarea, "[ui] signature is required");
            return;
        }

        const httpRoleText = httpRoleInput.value.trim();
        const httpRole = httpRoleText === "" ? null : httpRoleText;

        appendLogLine(
            logTextarea,
            `[ui] launching signature backfill for '${signature}' with role '${httpRole ?? "history_backfill"}'`,
        );

        const request: DemoPipeline2BackfillSignatureRequest = {
            signature,
            httpRole,
        };

        try {
            const payload = await invoke<DemoPipeline2BackfillPayload>(
                "demo_pipeline2_backfill_signature",
                { request },
            );

            backfillSummaryTextarea.value = payload.summaryJson;
            currentCatalog = payload.catalog;
            renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
            refreshPairSelect(payload.catalog, pairSelect);

            appendLogLine(logTextarea, `[ui] signature backfill completed for '${payload.objectKey}'`);
        } catch (error) {
            appendLogLine(logTextarea, `[ui] signature backfill error: ${String(error)}`);
        }
    });

    backfillSignatureBatchButton.addEventListener("click", async () => {
        const signatures = parseSignatureBatchText(signatureBatchTextarea.value);
        if (signatures.length === 0) {
            appendLogLine(logTextarea, "[ui] signature batch is empty");
            return;
        }

        const httpRoleText = httpRoleInput.value.trim();
        const httpRole = httpRoleText === "" ? null : httpRoleText;
        const continueOnError = signatureBatchContinueOnErrorCheckbox.checked;

        appendLogLine(
            logTextarea,
            `[ui] launching signature batch backfill count='${signatures.length.toString()}' role='${httpRole ?? "history_backfill"}' continueOnError='${continueOnError ? "yes" : "no"}'`,
        );

        const request: DemoPipeline2BackfillSignaturesBatchRequest = {
            signatures,
            httpRole,
            continueOnError,
        };

        backfillSignatureBatchButton.disabled = true;
        backfillSignatureButton.disabled = true;
        backfillMintButton.disabled = true;
        backfillPoolButton.disabled = true;

        try {
            const payload = await invoke<DemoPipeline2BackfillPayload>(
                "demo_pipeline2_backfill_signatures_batch",
                { request },
            );

            backfillSummaryTextarea.value = payload.summaryJson;
            currentCatalog = payload.catalog;
            renderCatalogTextareas(payload.catalog, tokensTextarea, poolsTextarea, pairsTextarea);
            refreshPairSelect(payload.catalog, pairSelect);

            appendLogLine(logTextarea, `[ui] signature batch backfill completed for '${payload.objectKey}'`);
        } catch (error) {
            appendLogLine(logTextarea, `[ui] signature batch backfill error: ${String(error)}`);
        } finally {
            backfillSignatureBatchButton.disabled = false;
            backfillSignatureButton.disabled = false;
            backfillMintButton.disabled = false;
            backfillPoolButton.disabled = false;
        }
    });

    replayLocalPipelineButton.addEventListener("click", async () => {
        const replayLimit = readOptionalPositiveIntegerInput(
            replayLimitInput,
            logTextarea,
            "replayLimit",
        );
        if (replayLimit === undefined) {
            return;
        }

        const tokenMetadataLimit = readOptionalPositiveIntegerInput(
            replayMetadataLimitInput,
            logTextarea,
            "tokenMetadataLimit",
        );
        if (tokenMetadataLimit === undefined) {
            return;
        }

        appendLogLine(
            logTextarea,
            `[ui] launching local pipeline replay limit='${replayLimit ?? "none"}' metadata='${replayMetadataCheckbox.checked ? "yes" : "no"}' skipDexDecode='${replaySkipCertifiedDexDecodeCheckbox.checked ? "yes" : "no"}' forceDexDecode='${replayForceDexDecodeCheckbox.checked ? "yes" : "no"}' deferInstructionObservations='${replayDeferInstructionObservationCheckbox.checked ? "yes" : "no"}'`,
        );

        try {
            const result = await invoke<LocalPipelineReplayResult>(
                "demo_pipeline2_replay_local_pipeline",
                {
                    limit: replayLimit,
                    refreshMissingTokenMetadata: replayMetadataCheckbox.checked,
                    tokenMetadataLimit,
                    skipCertifiedDexDecode: replaySkipCertifiedDexDecodeCheckbox.checked,
                    forceDecodeReplay: replayForceDexDecodeCheckbox.checked,
                    deferInstructionObservationIndexRefresh: replayDeferInstructionObservationCheckbox.checked,
                },
            );

            backfillSummaryTextarea.value = JSON.stringify(result, null, 2);

            appendLogLine(
                logTextarea,
                `[ui] local pipeline replay completed: ${result.replayedTransactionCount.toString()} replayed, ${result.decodeSkippedCount.toString()} decode skipped, ${result.decodeLedgerUpsertCount.toString()} ledger upserts, ${result.decodeLedgerUnsafeCount.toString()} unsafe ledger rows, ${result.tradeEventCount.toString()} trades, ${result.liquidityEventCount.toString()} liquidity, ${result.poolLifecycleEventCount.toString()} lifecycle, ${result.tokenAccountEventCount.toString()} tokenAccount, ${result.pairCandleUpsertCount.toString()} candle upserts, instructionObservations='${result.instructionObservationUpsertedCount.toString()}', resetDeleted='${result.resetMarketMaterializationDeletedCount.toString()}'`,
            );

            await refreshCatalog();
        } catch (error) {
            appendLogLine(logTextarea, `[ui] local pipeline replay error: ${String(error)}`);
        }
    });

    diagnoseLocalPipelineButton.addEventListener("click", async () => {
        appendLogLine(logTextarea, "[ui] diagnosing local pipeline");

        diagnoseLocalPipelineButton.disabled = true;

        try {
            const payload = await invoke<DemoPipeline2LocalDiagnosticsPayload>(
                "demo_pipeline2_diagnose_local_pipeline",
            );

            safeLocalDiagnosticsTextarea.value = payload.summaryJson;

            appendLogLine(
                logTextarea,
                `[ui] local pipeline diagnostics completed: ${payload.summary.decodedEventCount.toString()} decoded, ${payload.summary.tradeEventCount.toString()} trades, ${payload.summary.pairCandleCount.toString()} candles, actionableMissing='${payload.summary.actionableMissingTradeEventCount.toString()}', nonActionablePairs='${payload.summary.nonActionablePairCount.toString()}', blocking='${payload.summary.blockingIssueCount.toString()}'`,
            );
        } catch (error) {
            appendLogLine(logTextarea, `[ui] local pipeline diagnostics error: ${String(error)}`);
        } finally {
            diagnoseLocalPipelineButton.disabled = false;
        }
    });

    validateLocalPipelineButton.addEventListener("click", async () => {
        const request: DemoPipeline2LocalValidationRequest = {
            profileCode: validationProfileSelect.value,
        };

        appendLogLine(
            logTextarea,
            `[ui] validating local pipeline with '${request.profileCode}' profile`,
        );

        try {
            const payload = await invoke<DemoPipeline2LocalValidationPayload>(
                "demo_pipeline2_validate_local_pipeline",
                { request },
            );

            localValidationTextarea.value = payload.validationJson;
            localDiagnosticsTextarea.value = payload.summaryJson;

            appendLogLine(
                logTextarea,
                `[ui] local pipeline validation completed: profile='${payload.run.validationProfileCode}' passed='${payload.run.validationPassed ? "yes" : "no"}' blocking='${payload.run.blockingIssueCount.toString()}' warnings='${payload.run.warningCount.toString()}'`,
            );
        } catch (error) {
            appendLogLine(logTextarea, `[ui] local pipeline validation error: ${String(error)}`);
        }
    });

    refreshProtocolCandidatesButton.addEventListener("click", async () => {
        const limit = readPositiveIntegerInput(
            protocolCandidateLimitInput,
            logTextarea,
            "protocolCandidateSummaryLimit",
        );
        if (limit === undefined) {
            return;
        }

        appendLogLine(
            logTextarea,
            `[ui] loading protocol candidate summaries with limit '${limit.toString()}'`,
        );

        const request: DemoPipeline2ProtocolCandidateSummaryRequest = {
            limit,
        };

        try {
            const payload = await invoke<DemoPipeline2ProtocolCandidateSummaryPayload>(
                "demo_pipeline2_get_protocol_candidate_summaries",
                { request },
            );

            protocolCandidateSummariesTextarea.value = payload.summariesJson;

            appendLogLine(
                logTextarea,
                "[ui] protocol candidate summaries loaded",
            );
        } catch (error) {
            appendLogLine(
                logTextarea,
                `[ui] protocol candidate summary error: ${String(error)}`,
            );
        }
    });

    loadDiscriminatorSummariesButton.addEventListener("click", async () => {
        const programId = discriminatorProgramIdInput.value.trim();
        if (programId === "") {
            appendLogLine(logTextarea, "[ui] discriminator program id is required");
            return;
        }

        const limit = readPositiveIntegerInput(
            discriminatorLimitInput,
            logTextarea,
            "discriminatorSummaryLimit",
        );
        if (limit === undefined) {
            return;
        }

        appendLogLine(
            logTextarea,
            `[ui] loading instruction discriminator summaries for program '${programId}' with limit '${limit.toString()}'`,
        );

        const request: DemoPipeline2ProgramInstructionDiscriminatorSummaryRequest = {
            programId,
            limit,
        };

        try {
            const payload = await invoke<DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload>(
                "demo_pipeline2_get_program_instruction_discriminator_summaries",
                { request },
            );

            discriminatorSummariesTextarea.value = payload.summariesJson;

            appendLogLine(
                logTextarea,
                "[ui] instruction discriminator summaries loaded",
            );
        } catch (error) {
            appendLogLine(
                logTextarea,
                `[ui] instruction discriminator summary error: ${String(error)}`,
            );
        }
    });

    loadCandlesButton.addEventListener("click", async () => {
        const pairIdText = pairSelect.value.trim();
        if (pairIdText === "") {
            appendLogLine(logTextarea, "[ui] pair selection is required");
            return;
        }

        const parsedPairId = Number.parseInt(pairIdText, 10);
        if (Number.isNaN(parsedPairId) || parsedPairId <= 0) {
            appendLogLine(logTextarea, `[ui] invalid pair id '${pairIdText}'`);
            return;
        }

        let timeframeSeconds = Number.parseInt(timeframeSelect.value.trim(), 10);
        const customTimeframeText = customTimeframeInput.value.trim();
        if (customTimeframeText !== "") {
            const parsedCustom = Number.parseInt(customTimeframeText, 10);
            if (Number.isNaN(parsedCustom) || parsedCustom <= 0) {
                appendLogLine(logTextarea, `[ui] invalid custom timeframe '${customTimeframeText}'`);
                return;
            }
            timeframeSeconds = parsedCustom;
        }

        appendLogLine(
            logTextarea,
            `[ui] loading candles for pair '${parsedPairId}' timeframe '${timeframeSeconds}s'`,
        );

        const request: DemoPipeline2PairCandlesRequest = {
            pairId: parsedPairId,
            timeframeSeconds,
            preferMaterialized: preferMaterializedInput.checked,
        };

        try {
            const payload = await invoke<DemoPipeline2PairCandlesPayload>(
                "demo_pipeline2_get_pair_candles",
                { request },
            );

            const candles = parseCandlesJson(payload.candlesJson);
            renderCandlesChart(
                chart,
                chartMeta,
                payload.pairId,
                payload.timeframeSeconds,
                candles,
            );

            appendLogLine(
                logTextarea,
                `[ui] loaded ${candles.length} candles for pair '${payload.pairId.toString()}'`,
            );
        } catch (error) {
            appendLogLine(logTextarea, `[ui] load candles error: ${String(error)}`);
            setEmptyChart(chart, chartMeta, "Erreur lors du chargement des candles.");
        }
    });

    await refreshCatalog();

    if (currentCatalog !== null && currentCatalog.pairs.length > 0) {
        pairSelect.value = currentCatalog.pairs[0].pairId.toString();
    }
});
