// file: kb_demo_app/src/demo_pipeline2.rs

//! Tauri commands for the focused pipeline demo window.
//!
//! This demo is intentionally narrower than `Demo Pipeline`:
//! - read the local catalog of tokens / pools / pairs,
//! - trigger targeted backfills from the chain,
//! - load candles for one selected pair and timeframe.

use tauri::Manager;
use ts_rs::TS;

/// Request payload for program instruction discriminator summaries.
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2ProgramInstructionDiscriminatorSummaryRequest.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2ProgramInstructionDiscriminatorSummaryRequest {
    /// Program id to inspect.
    pub program_id: std::string::String,
    /// Maximum number of instruction rows to inspect before grouping.
    pub limit: u32,
}

/// Response payload for program instruction discriminator summaries.
#[derive(Clone, Debug, serde::Serialize, ts_rs::TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload {
    /// Pretty JSON summary rows.
    pub summaries_json: std::string::String,
}

/// Request payload for protocol candidate summary diagnostics.
#[derive(Clone, Debug, serde::Deserialize, ts_rs::TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2ProtocolCandidateSummaryRequest.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2ProtocolCandidateSummaryRequest {
    /// Maximum number of summary rows to return.
    pub limit: u32,
}

/// Response payload for protocol candidate summary diagnostics.
#[derive(Clone, Debug, serde::Serialize, ts_rs::TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2ProtocolCandidateSummaryPayload.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2ProtocolCandidateSummaryPayload {
    /// Pretty JSON summary rows.
    pub summaries_json: std::string::String,
}

/// Local diagnostics payload returned to the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalDiagnosticsPayload.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalDiagnosticsPayload {
    /// Open database URL.
    pub database_url: std::string::String,
    /// Pretty JSON diagnostics summary.
    pub summary_json: std::string::String,
    /// Structured diagnostics summary.
    pub summary: DemoPipeline2LocalPipelineDiagnosticSummary,
}

/// Request payload for local pipeline validation.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalValidationRequest.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalValidationRequest {
    /// Stable validation profile code to execute.
    pub profile_code: std::string::String,
}

/// Local validation payload returned to the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalValidationPayload.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalValidationPayload {
    /// Open database URL.
    pub database_url: std::string::String,
    /// Pretty JSON diagnostics summary used by validation.
    pub summary_json: std::string::String,
    /// Pretty JSON validation run.
    pub validation_json: std::string::String,
    /// Structured validation run.
    pub run: DemoPipeline2LocalPipelineValidationRun,
}

/// Local pipeline validation run for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalPipelineValidationRun.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalPipelineValidationRun {
    /// Stable validation profile code used to produce the run.
    pub validation_profile_code: std::string::String,
    /// Whether the validation passed without blocking issues.
    pub validation_passed: bool,
    /// Number of blocking issues found by validation.
    #[ts(type = "number")]
    pub blocking_issue_count: i64,
    /// Number of non-blocking warnings found by validation.
    #[ts(type = "number")]
    pub warning_count: i64,
    /// Diagnostics summary that was validated.
    pub summary: DemoPipeline2LocalPipelineDiagnosticSummary,
    /// Detailed validation report produced from the diagnostics summary.
    pub report: DemoPipeline2LocalPipelineValidationReport,
}

/// Local pipeline validation report for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalPipelineValidationReport.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalPipelineValidationReport {
    /// Stable validation profile code used to produce the report.
    pub validation_profile_code: std::string::String,
    /// Whether the validation passed without blocking issues.
    pub validation_passed: bool,
    /// Number of blocking issues found by validation.
    #[ts(type = "number")]
    pub blocking_issue_count: i64,
    /// Number of non-blocking warnings found by validation.
    #[ts(type = "number")]
    pub warning_count: i64,
    /// Expected DEX codes used by the validation run.
    pub expected_dex_codes: std::vec::Vec<std::string::String>,
    /// Observed DEX codes found in diagnostics.
    pub observed_dex_codes: std::vec::Vec<std::string::String>,
    /// Number of entries currently exposed by the DEX support matrix.
    #[ts(type = "number")]
    pub dex_support_matrix_entry_count: i64,
    /// DEX support matrix snapshot exposed with the validation report.
    pub dex_support_matrix: std::vec::Vec<DemoPipeline2DexSupportMatrixEntry>,
    /// Issues produced by validation.
    pub issues: std::vec::Vec<DemoPipeline2LocalPipelineValidationIssue>,
}

/// One DEX support matrix entry for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2DexSupportMatrixEntry.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2DexSupportMatrixEntry {
    /// Stable internal protocol or surface code.
    pub code: std::string::String,
    /// Human-readable protocol or surface name.
    pub display_name: std::string::String,
    /// Protocol family.
    pub family: std::string::String,
    /// Protocol version or `unknown` when not verified locally.
    pub version: std::string::String,
    /// Surface type: launch, bonding curve, AMM, CLMM, DLMM, router, aggregator or unknown.
    pub surface_type: std::string::String,
    /// Primary Solana program id, when verified in local constants or docs.
    pub program_id: std::option::Option<std::string::String>,
    /// Optional router program id, when this entry uses a distinct router.
    pub router_program_id: std::option::Option<std::string::String>,
    /// Program id confidence: known, to_verify or unknown.
    pub program_id_status: std::string::String,
    /// Whether this protocol has been observed in the local replay corpus.
    pub observed: bool,
    /// Whether the code currently contains a decoder for this protocol.
    pub decoded: bool,
    /// Whether decoded events are currently materialized beyond raw decoded rows.
    pub materialized: bool,
    /// Whether this protocol can currently produce trade candidates.
    pub trade_candidate: bool,
    /// Whether this protocol can currently produce candle candidates.
    pub candle_candidate: bool,
    /// Whether this protocol can currently produce pair candidates.
    pub pair_candidate: bool,
    /// Whether this protocol can currently produce pool candidates.
    pub pool_candidate: bool,
    /// Operational support status.
    pub status: std::string::String,
    /// Confidence level attached to this matrix entry.
    pub confidence: std::string::String,
    /// Optional explicit skip reason for partial or ignored entries.
    pub skip_reason: std::option::Option<std::string::String>,
    /// Whether the entry should be inserted as an enabled DEX in the storage catalog.
    pub catalog_enabled: bool,
}

/// One local pipeline validation issue for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalPipelineValidationIssue.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalPipelineValidationIssue {
    /// Stable machine-readable issue code.
    pub code: std::string::String,
    /// Human-readable issue message.
    pub message: std::string::String,
    /// Optional subject associated with the issue.
    pub subject: std::option::Option<std::string::String>,
    /// Whether the issue blocks the validation report.
    pub blocking: bool,
}

/// Local pipeline diagnostics summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalPipelineDiagnosticSummary {
    /// Total persisted chain transactions.
    #[ts(type = "number")]
    pub transaction_count: i64,
    /// Total successful chain transactions.
    #[ts(type = "number")]
    pub ok_transaction_count: i64,
    /// Total failed chain transactions.
    #[ts(type = "number")]
    pub failed_transaction_count: i64,
    /// Total decoded DEX events.
    #[ts(type = "number")]
    pub decoded_event_count: i64,
    /// Total decoded DEX trade candidates.
    #[ts(type = "number")]
    pub decoded_trade_candidate_count: i64,
    /// Total decoded DEX candle candidates.
    #[ts(type = "number")]
    pub decoded_candle_candidate_count: i64,
    /// Whether the local persisted pipeline has no blocking diagnostic issue.
    pub diagnostics_clean: bool,
    /// Number of blocking diagnostic issues.
    #[ts(type = "number")]
    pub blocking_issue_count: i64,
    /// Total trade candidates without trade event, including ignored failed transactions.
    #[ts(type = "number")]
    pub missing_trade_event_count: i64,
    /// Explicit alias for decoded trade candidates without linked trade event.
    #[ts(type = "number")]
    pub decoded_trade_candidate_without_trade_event_count: i64,
    /// Trade candidates without linked trade event on successful transactions.
    #[ts(type = "number")]
    pub decoded_trade_candidate_without_trade_event_on_ok_transaction_count: i64,
    /// Trade candidates without linked trade event on failed transactions.
    #[ts(type = "number")]
    pub decoded_trade_candidate_without_trade_event_on_failed_transaction_count: i64,
    /// Trade candidates without linked trade event and without explicit base/quote payload amounts.
    /// Actionable missing trade events on successful transactions.
    #[ts(type = "number")]
    pub actionable_missing_trade_event_count: i64,
    /// Ignored missing trade events caused by failed transactions.
    #[ts(type = "number")]
    pub ignored_failed_transaction_trade_candidate_count: i64,
    #[ts(type = "number")]
    pub decoded_trade_candidate_without_amount_payload_count: i64,
    /// Total persisted trade events.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Total invalid trade events.
    #[ts(type = "number")]
    pub invalid_trade_event_count: i64,
    /// Total persisted pair candles.
    #[ts(type = "number")]
    pub pair_candle_count: i64,
    /// Real duplicate trade rows grouped by decoded event id.
    #[ts(type = "number")]
    pub duplicate_decoded_event_trade_count: i64,
    /// Multi-trade groups sharing the same signature and pair id.
    #[ts(type = "number")]
    pub multi_trade_signature_pair_count: i64,
    /// Total duplicate candle buckets.
    #[ts(type = "number")]
    pub duplicate_candle_bucket_count: i64,
    /// Total known tokens.
    #[ts(type = "number")]
    pub token_count: i64,
    /// Total tokens missing symbol or name.
    #[ts(type = "number")]
    pub token_metadata_missing_count: i64,
    /// Total known pools.
    #[ts(type = "number")]
    pub pool_count: i64,
    /// Total known pairs.
    #[ts(type = "number")]
    pub pair_count: i64,
    /// Total pairs without trade.
    #[ts(type = "number")]
    pub pair_without_trade_count: i64,
    /// Total pairs without candle.
    #[ts(type = "number")]
    pub pair_without_candle_count: i64,
    /// Diagnostics grouped by DEX.
    pub dex_summaries: std::vec::Vec<DemoPipeline2LocalDexDiagnosticSummary>,
    /// Diagnostics grouped by pair.
    pub pair_summaries: std::vec::Vec<DemoPipeline2LocalPairDiagnosticSummary>,
    /// Diagnostics grouped by decoded event kind.
    pub decoded_event_summaries: std::vec::Vec<DemoPipeline2LocalDecodedEventDiagnosticSummary>,
    /// Missing trade events grouped by diagnostic reason.
    pub missing_trade_event_reason_summaries:
        std::vec::Vec<DemoPipeline2LocalMissingTradeEventReasonSummary>,
    /// Total pairs with only non-actionable missing trade events.
    #[ts(type = "number")]
    pub non_actionable_pair_count: i64,
    /// Pair summaries for non-actionable missing trade events.
    pub non_actionable_pair_summaries:
        std::vec::Vec<DemoPipeline2LocalNonActionablePairDiagnosticSummary>,
    /// Samples of decoded trade candidates without linked trade event.
    pub missing_trade_event_samples:
        std::vec::Vec<DemoPipeline2LocalMissingTradeEventDiagnosticSample>,
    /// Samples of duplicated trade rows by decoded event id.
    pub duplicate_decoded_event_trade_samples:
        std::vec::Vec<DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample>,
    /// Samples of multi-trade signature/pair groups.
    pub multi_trade_signature_pair_samples:
        std::vec::Vec<DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample>,
    /// Samples of pairs without trade.
    pub pair_without_trade_samples: std::vec::Vec<DemoPipeline2LocalPairGapDiagnosticSample>,
    /// Samples of pairs without candle.
    pub pair_without_candle_samples: std::vec::Vec<DemoPipeline2LocalPairGapDiagnosticSample>,
}

/// Local DEX diagnostics summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalDexDiagnosticSummary.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalDexDiagnosticSummary {
    /// DEX code.
    pub dex_code: std::string::String,
    /// Pool count.
    #[ts(type = "number")]
    pub pool_count: i64,
    /// Pair count.
    #[ts(type = "number")]
    pub pair_count: i64,
    /// Decoded event count.
    #[ts(type = "number")]
    pub decoded_event_count: i64,
    /// Decoded trade candidate count.
    #[ts(type = "number")]
    pub decoded_trade_candidate_count: i64,
    /// Decoded candle candidate count.
    #[ts(type = "number")]
    pub decoded_candle_candidate_count: i64,
    /// Trade event count.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Pair candle count.
    #[ts(type = "number")]
    pub pair_candle_count: i64,
}

/// Local pair diagnostics summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalPairDiagnosticSummary.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalPairDiagnosticSummary {
    /// Pair id.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Pool address.
    pub pool_address: std::string::String,
    /// DEX code.
    pub dex_code: std::string::String,
    /// Base mint.
    pub base_mint: std::string::String,
    /// Base symbol.
    pub base_symbol: std::option::Option<std::string::String>,
    /// Quote mint.
    pub quote_mint: std::string::String,
    /// Quote symbol.
    pub quote_symbol: std::option::Option<std::string::String>,
    /// Pair symbol.
    pub pair_symbol: std::option::Option<std::string::String>,
    /// Decoded event count.
    #[ts(type = "number")]
    pub decoded_event_count: i64,
    /// Decoded trade candidate count.
    #[ts(type = "number")]
    pub decoded_trade_candidate_count: i64,
    /// Decoded candle candidate count.
    #[ts(type = "number")]
    pub decoded_candle_candidate_count: i64,
    /// Trade event count.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Invalid trade event count.
    #[ts(type = "number")]
    pub invalid_trade_event_count: i64,
    /// Pair candle count.
    #[ts(type = "number")]
    pub pair_candle_count: i64,
    /// Last known price.
    #[ts(type = "number | null")]
    pub last_price_quote_per_base: std::option::Option<f64>,
}

/// Local decoded-event diagnostics summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalDecodedEventDiagnosticSummary.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalDecodedEventDiagnosticSummary {
    /// Protocol name.
    pub protocol_name: std::string::String,
    /// Event kind.
    pub event_kind: std::string::String,
    /// Event category.
    pub event_category: std::option::Option<std::string::String>,
    /// Trade candidate flag.
    pub trade_candidate: std::option::Option<bool>,
    /// Candle candidate flag.
    pub candle_candidate: std::option::Option<bool>,
    /// Event count.
    #[ts(type = "number")]
    pub event_count: i64,
    /// Linked trade-event count.
    #[ts(type = "number")]
    pub trade_event_count: i64,
}

/// Local missing-trade-event reason summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalMissingTradeEventReasonSummary.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalMissingTradeEventReasonSummary {
    /// Diagnostic reason.
    pub reason: std::string::String,
    /// Whether grouped source transactions failed.
    pub transaction_failed: bool,
    /// Whether grouped missing trade events are actionable.
    pub actionable: bool,
    /// Total missing trade events in this group.
    #[ts(type = "number")]
    pub event_count: i64,
    /// Total events in this group with an explicit base amount payload.
    #[ts(type = "number")]
    pub has_base_amount_payload_count: i64,
    /// Total events in this group with an explicit quote amount payload.
    #[ts(type = "number")]
    pub has_quote_amount_payload_count: i64,
    /// Total events in this group with an explicit price payload.
    #[ts(type = "number")]
    pub has_price_payload_count: i64,
}

/// Local non-actionable pair diagnostic summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalNonActionablePairDiagnosticSummary.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalNonActionablePairDiagnosticSummary {
    /// Pair id.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Pool address.
    pub pool_address: std::string::String,
    /// DEX code.
    pub dex_code: std::string::String,
    /// Base token mint.
    pub base_mint: std::string::String,
    /// Base token symbol.
    pub base_symbol: std::option::Option<std::string::String>,
    /// Quote token mint.
    pub quote_mint: std::string::String,
    /// Quote token symbol.
    pub quote_symbol: std::option::Option<std::string::String>,
    /// Pair symbol.
    pub pair_symbol: std::option::Option<std::string::String>,
    /// Total decoded trade candidates attached to the pool.
    #[ts(type = "number")]
    pub decoded_trade_candidate_count: i64,
    /// Total non-actionable missing trade events attached to the pair.
    #[ts(type = "number")]
    pub non_actionable_missing_trade_event_count: i64,
    /// Total missing trade candidates caused by failed transactions.
    #[ts(type = "number")]
    pub failed_transaction_candidate_count: i64,
    /// Total successful missing trade candidates without token mints.
    #[ts(type = "number")]
    pub ok_transaction_without_token_mints_count: i64,
    /// Total successful missing trade candidates without amount payload.
    #[ts(type = "number")]
    pub ok_transaction_without_amount_payload_count: i64,
    /// Distinct non-actionable diagnostic reasons seen for this pair.
    pub reason_summary: std::option::Option<std::string::String>,
    /// Total trade events attached to the pair.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Total candle buckets attached to the pair.
    #[ts(type = "number")]
    pub pair_candle_count: i64,
}

/// Local missing-trade-event diagnostic sample for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalMissingTradeEventDiagnosticSample.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalMissingTradeEventDiagnosticSample {
    /// Decoded event id.
    #[ts(type = "number")]
    pub decoded_event_id: i64,
    /// Chain transaction id.
    #[ts(type = "number | null")]
    pub transaction_id: std::option::Option<i64>,
    /// Transaction signature.
    pub signature: std::option::Option<std::string::String>,
    /// Protocol name.
    pub protocol_name: std::string::String,
    /// Event kind.
    pub event_kind: std::string::String,
    /// Pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Whether the source transaction failed.
    pub transaction_failed: bool,
    /// Whether this missing trade event is actionable for validation.
    pub actionable: bool,
    /// Diagnostic reason explaining why no trade event was linked.
    pub reason: std::string::String,
    /// Whether payload has an explicit base amount.
    pub has_base_amount_payload: bool,
    /// Whether payload has an explicit quote amount.
    pub has_quote_amount_payload: bool,
    /// Whether payload has an explicit price.
    pub has_price_payload: bool,
}

/// Local duplicate decoded-event trade diagnostic sample for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample {
    /// Decoded event id.
    #[ts(type = "number")]
    pub decoded_event_id: i64,
    /// Protocol name.
    pub protocol_name: std::option::Option<std::string::String>,
    /// Event kind.
    pub event_kind: std::option::Option<std::string::String>,
    /// Pool account.
    pub pool_account: std::option::Option<std::string::String>,
    /// Trade event count.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Trade event ids.
    pub trade_event_ids: std::option::Option<std::string::String>,
    /// Signatures.
    pub signatures: std::option::Option<std::string::String>,
}

/// Local multi-trade signature/pair diagnostic sample for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample {
    /// Transaction signature.
    pub signature: std::string::String,
    /// Pair id.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Pool address.
    pub pool_address: std::option::Option<std::string::String>,
    /// DEX code.
    pub dex_code: std::option::Option<std::string::String>,
    /// Trade event count.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Distinct decoded event count.
    #[ts(type = "number")]
    pub decoded_event_count: i64,
    /// Trade event ids.
    pub trade_event_ids: std::option::Option<std::string::String>,
    /// Decoded event ids.
    pub decoded_event_ids: std::option::Option<std::string::String>,
}

/// Local pair gap diagnostic sample for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2LocalPairGapDiagnosticSample.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2LocalPairGapDiagnosticSample {
    /// Pair id.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Pool address.
    pub pool_address: std::string::String,
    /// DEX code.
    pub dex_code: std::string::String,
    /// Base mint.
    pub base_mint: std::string::String,
    /// Base symbol.
    pub base_symbol: std::option::Option<std::string::String>,
    /// Quote mint.
    pub quote_mint: std::string::String,
    /// Quote symbol.
    pub quote_symbol: std::option::Option<std::string::String>,
    /// Pair symbol.
    pub pair_symbol: std::option::Option<std::string::String>,
    /// Decoded event count.
    #[ts(type = "number")]
    pub decoded_event_count: i64,
    /// Decoded trade candidate count.
    #[ts(type = "number")]
    pub decoded_trade_candidate_count: i64,
    /// Trade event count.
    #[ts(type = "number")]
    pub trade_event_count: i64,
    /// Pair candle count.
    #[ts(type = "number")]
    pub pair_candle_count: i64,
}

/// One token item for the local catalog.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2TokenItem.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2TokenItem {
    /// Token mint.
    pub mint: std::string::String,
    /// Optional token symbol.
    pub symbol: std::option::Option<std::string::String>,
    /// Optional token name.
    pub name: std::option::Option<std::string::String>,
}

/// One pool item for the local catalog.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2PoolItem.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2PoolItem {
    /// Pool address.
    pub pool_address: std::string::String,
    /// Optional internal pair id when known.
    #[ts(type = "number | null")]
    pub pair_id: std::option::Option<i64>,
    /// Optional DEX code.
    pub dex_code: std::option::Option<std::string::String>,
}

/// One pair item for the local catalog.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2PairItem.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2PairItem {
    /// Internal pair id.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Related pool address.
    pub pool_address: std::string::String,
    /// Optional pair symbol.
    pub symbol: std::option::Option<std::string::String>,
    /// Optional DEX code.
    pub dex_code: std::option::Option<std::string::String>,
    /// Optional local trade count.
    #[ts(type = "number | null")]
    pub trade_count: std::option::Option<i64>,
    /// Optional local last price.
    #[ts(type = "number | null")]
    pub last_price_quote_per_base: std::option::Option<f64>,
}

/// Full local catalog payload.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2CatalogPayload.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2CatalogPayload {
    /// Open database URL.
    pub database_url: std::string::String,
    /// Observed token list.
    pub tokens: std::vec::Vec<DemoPipeline2TokenItem>,
    /// Known pool list.
    pub pools: std::vec::Vec<DemoPipeline2PoolItem>,
    /// Known pair list.
    pub pairs: std::vec::Vec<DemoPipeline2PairItem>,
}

/// Request payload for token backfill.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2BackfillTokenRequest.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2BackfillTokenRequest {
    /// Token mint to backfill.
    pub token_mint: std::string::String,
    /// Optional HTTP role.
    pub http_role: std::option::Option<std::string::String>,
    /// Limit for signatures fetched from the mint.
    pub mint_signature_limit: u32,
    /// Limit for signatures fetched from each discovered pool.
    pub pool_signature_limit: u32,
}

/// Request payload for pool backfill.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(
    export,
    export_to = "../frontend/ts/bindings/DemoPipeline2BackfillPoolRequest.ts"
)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2BackfillPoolRequest {
    /// Pool address to backfill.
    pub pool_address: std::string::String,
    /// Optional HTTP role.
    pub http_role: std::option::Option<std::string::String>,
    /// Limit for signatures fetched from the pool.
    pub pool_signature_limit: u32,
}

/// Shared backfill response payload.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2BackfillPayload.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2BackfillPayload {
    /// Object key used by the backfill.
    pub object_key: std::string::String,
    /// Mode: `tokenMint` or `poolAddress`.
    pub mode: std::string::String,
    /// HTTP role used.
    pub http_role: std::string::String,
    /// Pretty JSON summary.
    pub summary_json: std::string::String,
    /// Refreshed local catalog after backfill.
    pub catalog: DemoPipeline2CatalogPayload,
}

/// Request payload for pair candles.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2PairCandlesRequest.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2PairCandlesRequest {
    /// Pair id to load.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Timeframe in seconds.
    #[ts(type = "number")]
    pub timeframe_seconds: i64,
    /// Whether materialized candles should be preferred when available.
    pub prefer_materialized: bool,
}

/// Candle payload returned to the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/DemoPipeline2PairCandlesPayload.ts")]
#[serde(rename_all = "camelCase")]
pub(crate) struct DemoPipeline2PairCandlesPayload {
    /// Pair id.
    #[ts(type = "number")]
    pub pair_id: i64,
    /// Timeframe in seconds.
    #[ts(type = "number")]
    pub timeframe_seconds: i64,
    /// Pretty JSON array of candles.
    pub candles_json: std::string::String,
}

/// Lists program instruction discriminator summaries for one program id.
#[tauri::command]
pub(crate) async fn demo_pipeline2_get_program_instruction_discriminator_summaries(
    state: tauri::State<'_, crate::AppState>,
    request: DemoPipeline2ProgramInstructionDiscriminatorSummaryRequest,
) -> Result<DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload, std::string::String> {
    if request.program_id.trim().is_empty() {
        return Err("program id must not be empty".to_string());
    }
    if request.limit == 0 {
        return Err("instruction discriminator summary limit must be > 0".to_string());
    }
    let rows_result = kb_lib::query_program_instruction_discriminator_summaries_list_by_program_id(
        state.database.as_ref(),
        request.program_id.as_str(),
        request.limit,
    )
    .await;
    let rows = match rows_result {
        Ok(rows) => rows,
        Err(error) => {
            return Err(format!(
                "cannot list instruction discriminator summaries for program_id '{}': {}",
                request.program_id, error
            ));
        },
    };
    let summaries_json_result = serde_json::to_string_pretty(&rows);
    let summaries_json = match summaries_json_result {
        Ok(summaries_json) => summaries_json,
        Err(error) => {
            return Err(format!("cannot serialize instruction discriminator summaries: {}", error));
        },
    };
    return Ok(DemoPipeline2ProgramInstructionDiscriminatorSummaryPayload { summaries_json });
}

/// Lists protocol candidate summaries ordered by investigation priority.
#[tauri::command]
pub(crate) async fn demo_pipeline2_get_protocol_candidate_summaries(
    state: tauri::State<'_, crate::AppState>,
    request: DemoPipeline2ProtocolCandidateSummaryRequest,
) -> Result<DemoPipeline2ProtocolCandidateSummaryPayload, std::string::String> {
    if request.limit == 0 {
        return Err("protocol candidate summary limit must be > 0".to_string());
    }
    let summaries_result = kb_lib::query_protocol_candidate_summaries_list_by_priority(
        state.database.as_ref(),
        request.limit,
    )
    .await;
    let summaries = match summaries_result {
        Ok(summaries) => summaries,
        Err(error) => {
            return Err(format!(
                "cannot list protocol candidate summaries with limit '{}': {}",
                request.limit, error
            ));
        },
    };
    let summaries_json_result = serde_json::to_string_pretty(&summaries);
    let summaries_json = match summaries_json_result {
        Ok(summaries_json) => summaries_json,
        Err(error) => {
            return Err(format!("cannot serialize protocol candidate summaries: {}", error));
        },
    };
    return Ok(DemoPipeline2ProtocolCandidateSummaryPayload { summaries_json });
}

/// Runs local pipeline diagnostics from persisted data only.
#[tauri::command]
pub(crate) async fn demo_pipeline2_diagnose_local_pipeline(
    state: tauri::State<'_, crate::AppState>,
) -> Result<DemoPipeline2LocalDiagnosticsPayload, std::string::String> {
    let database = state.database.clone();
    let service = kb_lib::LocalPipelineDiagnosticsService::new(database.clone());
    let summary_result = service.diagnose().await;
    let summary = match summary_result {
        Ok(summary) => summary,
        Err(error) => {
            return Err(format!("local pipeline diagnostics failed: {}", error));
        },
    };
    let ui_summary = demo_pipeline2_map_local_diagnostics_summary(summary);
    let summary_json_result = serde_json::to_string_pretty(&ui_summary);
    let summary_json = match summary_json_result {
        Ok(summary_json) => summary_json,
        Err(error) => {
            return Err(format!("cannot serialize local pipeline diagnostics: {}", error));
        },
    };
    Ok(DemoPipeline2LocalDiagnosticsPayload {
        database_url: database.database_url().to_string(),
        summary_json,
        summary: ui_summary,
    })
}

/// Validates the local pipeline with a selected multi-DEX validation profile.
#[tauri::command]
pub(crate) async fn demo_pipeline2_validate_local_pipeline(
    state: tauri::State<'_, crate::AppState>,
    request: std::option::Option<DemoPipeline2LocalValidationRequest>,
) -> Result<DemoPipeline2LocalValidationPayload, std::string::String> {
    let database = state.database.clone();
    let service = kb_lib::LocalPipelineValidationService::new(database.clone());
    let profile_code = match request {
        Some(request) => request.profile_code,
        None => "0.7.29_multi_dex_matrix_baseline".to_string(),
    };
    let run_result = match profile_code.as_str() {
        "0.7.27" | "0.7.27_dexes_non_regression" => {
            service.validate_v0_7_27_current_database().await
        },
        "0.7.28" | "0.7.28_multi_dex_non_regression" => {
            service.validate_v0_7_28_current_database().await
        },
        "0.7.29" | "0.7.29_multi_dex_matrix_baseline" => {
            service.validate_v0_7_29_current_database().await
        },
        other => Err(kb_lib::Error::InvalidState(format!(
            "unsupported local pipeline validation profile: {other}"
        ))),
    };
    let run = match run_result {
        Ok(run) => run,
        Err(error) => {
            return Err(format!("local pipeline validation failed: {}", error));
        },
    };
    let ui_run = demo_pipeline2_map_local_validation_run(run);
    let summary_json_result = serde_json::to_string_pretty(&ui_run.summary);
    let summary_json = match summary_json_result {
        Ok(summary_json) => summary_json,
        Err(error) => {
            return Err(format!("cannot serialize local pipeline validation summary: {}", error));
        },
    };
    let validation_json_result = serde_json::to_string_pretty(&ui_run);
    let validation_json = match validation_json_result {
        Ok(validation_json) => validation_json,
        Err(error) => {
            return Err(format!("cannot serialize local pipeline validation run: {}", error));
        },
    };
    Ok(DemoPipeline2LocalValidationPayload {
        database_url: database.database_url().to_string(),
        summary_json,
        validation_json,
        run: ui_run,
    })
}

/// Opens the `Demo Pipeline 2` window.
#[tauri::command]
pub(crate) fn open_demo_pipeline2_window(
    app_handle: tauri::AppHandle,
) -> Result<(), std::string::String> {
    let existing_window_option = app_handle.get_webview_window("demo_pipeline2");

    let demo_window = match existing_window_option {
        Some(demo_window) => demo_window,
        None => {
            let builder = tauri::WebviewWindowBuilder::new(
                &app_handle,
                "demo_pipeline2",
                tauri::WebviewUrl::App("demo_pipeline2.html".into()),
            )
            .title("Demo Pipeline 2")
            .inner_size(1480.0, 920.0)
            .min_inner_size(1100.0, 720.0)
            .center()
            .visible(true)
            .transparent(false)
            .decorations(true);
            let build_result = builder.build();
            match build_result {
                Ok(window) => window,
                Err(error) => {
                    return Err(format!("cannot create demo_pipeline2 window: {error:?}"));
                },
            }
        },
    };
    let show_result = demo_window.show();
    if let Err(error) = show_result {
        return Err(format!("cannot show demo_pipeline2 window: {error:?}"));
    }
    let focus_result = demo_window.set_focus();
    if let Err(error) = focus_result {
        return Err(format!("cannot focus demo_pipeline2 window: {error:?}"));
    }
    Ok(())
}

/// Returns the local catalog of observed tokens, pools and pairs.
#[tauri::command]
pub(crate) async fn demo_pipeline2_get_catalog(
    state: tauri::State<'_, crate::AppState>,
) -> Result<DemoPipeline2CatalogPayload, std::string::String> {
    demo_pipeline2_build_catalog(state.database.clone()).await
}

/// Runs a targeted token backfill then returns the refreshed catalog.
#[tauri::command]
pub(crate) async fn demo_pipeline2_backfill_token_mint(
    state: tauri::State<'_, crate::AppState>,
    request: DemoPipeline2BackfillTokenRequest,
) -> Result<DemoPipeline2BackfillPayload, std::string::String> {
    let token_mint = request.token_mint.trim().to_string();
    if token_mint.is_empty() {
        return Err("token mint must not be empty".to_string());
    }
    if request.mint_signature_limit == 0 {
        return Err("mintSignatureLimit must be > 0".to_string());
    }
    if request.pool_signature_limit == 0 {
        return Err("poolSignatureLimit must be > 0".to_string());
    }
    let http_role = demo_pipeline2_normalize_http_role(request.http_role);
    let database = state.database.clone();
    let http_pool = std::sync::Arc::new(state.http_pool.clone());
    let service = kb_lib::TokenBackfillService::new(http_pool, database.clone(), http_role.clone());
    let result = service
        .backfill_token_by_mint(
            token_mint.as_str(),
            request.mint_signature_limit as usize,
            request.pool_signature_limit as usize,
        )
        .await;
    let backfill = match result {
        Ok(backfill) => backfill,
        Err(error) => {
            return Err(format!(
                "cannot backfill token mint '{}' with role '{}': {}",
                token_mint, http_role, error
            ));
        },
    };
    let summary_json_result = serde_json::to_string_pretty(&backfill);
    let summary_json = match summary_json_result {
        Ok(summary_json) => summary_json,
        Err(error) => {
            return Err(format!(
                "cannot serialize token backfill result for '{}': {}",
                token_mint, error
            ));
        },
    };
    let catalog = demo_pipeline2_build_catalog(database).await?;
    Ok(DemoPipeline2BackfillPayload {
        object_key: token_mint,
        mode: "tokenMint".to_string(),
        http_role,
        summary_json,
        catalog,
    })
}

/// Runs a targeted pool backfill then returns the refreshed catalog.
#[tauri::command]
pub(crate) async fn demo_pipeline2_backfill_pool_address(
    state: tauri::State<'_, crate::AppState>,
    request: DemoPipeline2BackfillPoolRequest,
) -> Result<DemoPipeline2BackfillPayload, std::string::String> {
    let pool_address = request.pool_address.trim().to_string();
    if pool_address.is_empty() {
        return Err("pool address must not be empty".to_string());
    }
    if request.pool_signature_limit == 0 {
        return Err("poolSignatureLimit must be > 0".to_string());
    }
    let http_role = demo_pipeline2_normalize_http_role(request.http_role);
    let database = state.database.clone();
    let http_pool = std::sync::Arc::new(state.http_pool.clone());
    let service = kb_lib::TokenBackfillService::new(http_pool, database.clone(), http_role.clone());
    let result = service
        .backfill_pool_by_address(pool_address.as_str(), request.pool_signature_limit as usize)
        .await;
    let backfill = match result {
        Ok(backfill) => backfill,
        Err(error) => {
            return Err(format!(
                "cannot backfill pool address '{}' with role '{}': {}",
                pool_address, http_role, error
            ));
        },
    };
    let summary_json_result = serde_json::to_string_pretty(&backfill);
    let summary_json = match summary_json_result {
        Ok(summary_json) => summary_json,
        Err(error) => {
            return Err(format!(
                "cannot serialize pool backfill result for '{}': {}",
                pool_address, error
            ));
        },
    };
    let catalog = demo_pipeline2_build_catalog(database).await?;
    Ok(DemoPipeline2BackfillPayload {
        object_key: pool_address,
        mode: "poolAddress".to_string(),
        http_role,
        summary_json,
        catalog,
    })
}

/// Loads candles for one pair and one timeframe.
#[tauri::command]
pub(crate) async fn demo_pipeline2_get_pair_candles(
    state: tauri::State<'_, crate::AppState>,
    request: DemoPipeline2PairCandlesRequest,
) -> Result<DemoPipeline2PairCandlesPayload, std::string::String> {
    if request.pair_id <= 0 {
        return Err("pairId must be > 0".to_string());
    }
    if request.timeframe_seconds <= 0 {
        return Err("timeframeSeconds must be > 0".to_string());
    }
    let query_service = kb_lib::PairCandleQueryService::new(state.database.clone());
    let candles_result = query_service
        .list_pair_candles(
            request.pair_id,
            request.timeframe_seconds,
            None,
            None,
            request.prefer_materialized,
        )
        .await;
    let candles = match candles_result {
        Ok(candles) => candles,
        Err(error) => {
            return Err(format!(
                "cannot load candles for pair '{}' timeframe '{}': {}",
                request.pair_id, request.timeframe_seconds, error
            ));
        },
    };
    let candles_json_result = serde_json::to_string_pretty(&candles);
    let candles_json = match candles_json_result {
        Ok(candles_json) => candles_json,
        Err(error) => {
            return Err(format!(
                "cannot serialize candles for pair '{}' timeframe '{}': {}",
                request.pair_id, request.timeframe_seconds, error
            ));
        },
    };
    Ok(DemoPipeline2PairCandlesPayload {
        pair_id: request.pair_id,
        timeframe_seconds: request.timeframe_seconds,
        candles_json,
    })
}

async fn demo_pipeline2_build_catalog(
    database: std::sync::Arc<kb_lib::Database>,
) -> Result<DemoPipeline2CatalogPayload, std::string::String> {
    let dexes_result = kb_lib::query_dexs_list(database.as_ref()).await;
    let dexes = match dexes_result {
        Ok(dexes) => dexes,
        Err(error) => {
            return Err(format!("cannot list DEXes: {}", error));
        },
    };
    let mut dex_code_by_id = std::collections::BTreeMap::<i64, std::string::String>::new();
    for dex in dexes {
        if let Some(dex_id) = dex.id {
            dex_code_by_id.insert(dex_id, dex.code);
        }
    }
    let tokens_result = kb_lib::query_tokens_list(database.as_ref()).await;
    let db_tokens = match tokens_result {
        Ok(db_tokens) => db_tokens,
        Err(error) => {
            return Err(format!("cannot list tokens: {}", error));
        },
    };
    let mut tokens = std::vec::Vec::<DemoPipeline2TokenItem>::new();
    for token in db_tokens {
        tokens.push(DemoPipeline2TokenItem {
            mint: token.mint,
            symbol: token.symbol,
            name: token.name,
        });
    }
    let pools_result = kb_lib::query_pools_list(database.as_ref()).await;
    let pools = match pools_result {
        Ok(pools) => pools,
        Err(error) => {
            return Err(format!("cannot list pools: {}", error));
        },
    };
    let pairs_result = kb_lib::query_pairs_list(database.as_ref()).await;
    let pairs = match pairs_result {
        Ok(pairs) => pairs,
        Err(error) => {
            return Err(format!("cannot list pairs: {}", error));
        },
    };
    let mut pair_by_pool_id = std::collections::BTreeMap::<i64, kb_lib::PairDto>::new();
    for pair in &pairs {
        pair_by_pool_id.insert(pair.pool_id, pair.clone());
    }
    let mut pair_items = std::vec::Vec::<DemoPipeline2PairItem>::new();
    for pair in pairs {
        let pair_id = match pair.id {
            Some(pair_id) => pair_id,
            None => continue,
        };
        let pool_result = kb_lib::query_pools_get_by_address(database.as_ref(), "").await;
        let _ = pool_result;
        let pool_address = {
            let all_pools_result = kb_lib::query_pools_list(database.as_ref()).await;
            let all_pools = match all_pools_result {
                Ok(all_pools) => all_pools,
                Err(error) => {
                    return Err(format!("cannot reload pools for pair catalog: {}", error));
                },
            };
            let mut found_address = std::string::String::new();
            for pool in all_pools {
                let pool_id = match pool.id {
                    Some(pool_id) => pool_id,
                    None => continue,
                };
                if pool_id == pair.pool_id {
                    found_address = pool.address;
                    break;
                }
            }
            found_address
        };
        let pair_metric_result =
            kb_lib::query_pair_metrics_get_by_pair_id(database.as_ref(), pair_id).await;
        let pair_metric_option = match pair_metric_result {
            Ok(pair_metric_option) => pair_metric_option,
            Err(error) => {
                return Err(format!("cannot fetch pair metric for pair '{}': {}", pair_id, error));
            },
        };
        let trade_count = pair_metric_option.as_ref().map(|metric| metric.trade_count);
        let last_price_quote_per_base =
            pair_metric_option.and_then(|metric| metric.last_price_quote_per_base);
        pair_items.push(DemoPipeline2PairItem {
            pair_id,
            pool_address,
            symbol: pair.symbol,
            dex_code: dex_code_by_id.get(&pair.dex_id).cloned(),
            trade_count,
            last_price_quote_per_base,
        });
    }
    let mut pool_items = std::vec::Vec::<DemoPipeline2PoolItem>::new();
    for pool in pools {
        let pool_id = match pool.id {
            Some(pool_id) => pool_id,
            None => continue,
        };
        let pair_id = pair_by_pool_id.get(&pool_id).and_then(|pair| pair.id);
        pool_items.push(DemoPipeline2PoolItem {
            pool_address: pool.address,
            pair_id,
            dex_code: dex_code_by_id.get(&pool.dex_id).cloned(),
        });
    }
    tokens.sort_by(|left, right| left.mint.cmp(&right.mint));
    pool_items.sort_by(|left, right| left.pool_address.cmp(&right.pool_address));
    pair_items.sort_by(|left, right| left.pair_id.cmp(&right.pair_id));
    Ok(DemoPipeline2CatalogPayload {
        database_url: database.database_url().to_string(),
        tokens,
        pools: pool_items,
        pairs: pair_items,
    })
}

/// Replays the local pipeline from persisted raw transactions.
#[tauri::command]
pub(crate) async fn demo_pipeline2_replay_local_pipeline(
    state: tauri::State<'_, crate::AppState>,
    limit: std::option::Option<i64>,
    refresh_missing_token_metadata: bool,
    token_metadata_limit: std::option::Option<i64>,
) -> Result<kb_lib::LocalPipelineReplayResult, std::string::String> {
    let config = kb_lib::LocalPipelineReplayConfig {
        limit,
        refresh_missing_token_metadata,
        token_metadata_limit,
    };
    let database = state.database.clone();
    let service = if refresh_missing_token_metadata {
        kb_lib::LocalPipelineReplayService::new_with_http_pool(
            std::sync::Arc::new(state.http_pool.clone()),
            database,
            "history_backfill".to_string(),
        )
    } else {
        kb_lib::LocalPipelineReplayService::new(database)
    };
    let replay_result = service.replay_local_pipeline(&config).await;
    match replay_result {
        Ok(result) => Ok(result),
        Err(error) => Err(format!("local pipeline replay failed: {}", error)),
    }
}

fn demo_pipeline2_map_local_validation_run(
    run: kb_lib::LocalPipelineValidationRunDto,
) -> DemoPipeline2LocalPipelineValidationRun {
    let validation_profile_code = run.validation_profile_code;
    let validation_passed = run.validation_passed;
    let blocking_issue_count = run.blocking_issue_count;
    let warning_count = run.warning_count;
    let summary = demo_pipeline2_map_local_diagnostics_summary(run.summary);
    let report = demo_pipeline2_map_local_validation_report(run.report);
    return DemoPipeline2LocalPipelineValidationRun {
        validation_profile_code,
        validation_passed,
        blocking_issue_count,
        warning_count,
        summary,
        report,
    };
}

fn demo_pipeline2_map_local_validation_report(
    report: kb_lib::LocalPipelineValidationReportDto,
) -> DemoPipeline2LocalPipelineValidationReport {
    let mut issues = std::vec::Vec::new();
    for issue in report.issues {
        issues.push(demo_pipeline2_map_local_validation_issue(issue));
    }
    let mut dex_support_matrix = std::vec::Vec::new();
    for entry in report.dex_support_matrix {
        dex_support_matrix.push(demo_pipeline2_map_dex_support_matrix_entry(entry));
    }
    return DemoPipeline2LocalPipelineValidationReport {
        validation_profile_code: report.validation_profile_code,
        validation_passed: report.validation_passed,
        blocking_issue_count: report.blocking_issue_count,
        warning_count: report.warning_count,
        expected_dex_codes: report.expected_dex_codes,
        observed_dex_codes: report.observed_dex_codes,
        dex_support_matrix_entry_count: report.dex_support_matrix_entry_count,
        dex_support_matrix,
        issues,
    };
}

fn demo_pipeline2_map_dex_support_matrix_entry(
    entry: kb_lib::DexSupportMatrixEntryDto,
) -> DemoPipeline2DexSupportMatrixEntry {
    return DemoPipeline2DexSupportMatrixEntry {
        code: entry.code,
        display_name: entry.display_name,
        family: entry.family,
        version: entry.version,
        surface_type: entry.surface_type,
        program_id: entry.program_id,
        router_program_id: entry.router_program_id,
        program_id_status: entry.program_id_status,
        observed: entry.observed,
        decoded: entry.decoded,
        materialized: entry.materialized,
        trade_candidate: entry.trade_candidate,
        candle_candidate: entry.candle_candidate,
        pair_candidate: entry.pair_candidate,
        pool_candidate: entry.pool_candidate,
        status: entry.status,
        confidence: entry.confidence,
        skip_reason: entry.skip_reason,
        catalog_enabled: entry.catalog_enabled,
    };
}

fn demo_pipeline2_map_local_validation_issue(
    issue: kb_lib::LocalPipelineValidationIssueDto,
) -> DemoPipeline2LocalPipelineValidationIssue {
    return DemoPipeline2LocalPipelineValidationIssue {
        code: issue.code,
        message: issue.message,
        subject: issue.subject,
        blocking: issue.blocking,
    };
}

fn demo_pipeline2_map_local_diagnostics_summary(
    summary: kb_lib::LocalPipelineDiagnosticSummaryDto,
) -> DemoPipeline2LocalPipelineDiagnosticSummary {
    let mut dex_summaries = std::vec::Vec::new();
    for dex_summary in summary.dex_summaries {
        dex_summaries.push(demo_pipeline2_map_local_dex_diagnostic_summary(dex_summary));
    }
    let mut pair_summaries = std::vec::Vec::new();
    for pair_summary in summary.pair_summaries {
        pair_summaries.push(demo_pipeline2_map_local_pair_diagnostic_summary(pair_summary));
    }
    let mut decoded_event_summaries = std::vec::Vec::new();
    for decoded_event_summary in summary.decoded_event_summaries {
        decoded_event_summaries
            .push(demo_pipeline2_map_local_decoded_event_diagnostic_summary(decoded_event_summary));
    }
    let mut missing_trade_event_reason_summaries = std::vec::Vec::new();
    for reason_summary in summary.missing_trade_event_reason_summaries {
        missing_trade_event_reason_summaries
            .push(demo_pipeline2_map_missing_trade_event_reason_summary(reason_summary));
    }
    let mut non_actionable_pair_summaries = std::vec::Vec::new();
    for pair_summary in summary.non_actionable_pair_summaries {
        non_actionable_pair_summaries
            .push(demo_pipeline2_map_non_actionable_pair_diagnostic_summary(pair_summary));
    }
    let mut missing_trade_event_samples = std::vec::Vec::new();
    for sample in summary.missing_trade_event_samples {
        missing_trade_event_samples.push(demo_pipeline2_map_missing_trade_event_sample(sample));
    }

    let mut duplicate_decoded_event_trade_samples = std::vec::Vec::new();
    for sample in summary.duplicate_decoded_event_trade_samples {
        duplicate_decoded_event_trade_samples
            .push(demo_pipeline2_map_duplicate_decoded_event_trade_sample(sample));
    }
    let mut multi_trade_signature_pair_samples = std::vec::Vec::new();
    for sample in summary.multi_trade_signature_pair_samples {
        multi_trade_signature_pair_samples
            .push(demo_pipeline2_map_multi_trade_signature_pair_sample(sample));
    }
    let mut pair_without_trade_samples = std::vec::Vec::new();
    for sample in summary.pair_without_trade_samples {
        pair_without_trade_samples.push(demo_pipeline2_map_pair_gap_sample(sample));
    }
    let mut pair_without_candle_samples = std::vec::Vec::new();
    for sample in summary.pair_without_candle_samples {
        pair_without_candle_samples.push(demo_pipeline2_map_pair_gap_sample(sample));
    }
    DemoPipeline2LocalPipelineDiagnosticSummary {
        transaction_count: summary.transaction_count,
        ok_transaction_count: summary.ok_transaction_count,
        failed_transaction_count: summary.failed_transaction_count,
        decoded_event_count: summary.decoded_event_count,
        decoded_trade_candidate_count: summary.decoded_trade_candidate_count,
        decoded_candle_candidate_count: summary.decoded_candle_candidate_count,
        diagnostics_clean: summary.diagnostics_clean,
        blocking_issue_count: summary.blocking_issue_count,
        missing_trade_event_count: summary.missing_trade_event_count,
        decoded_trade_candidate_without_trade_event_count: summary
            .decoded_trade_candidate_without_trade_event_count,
        decoded_trade_candidate_without_trade_event_on_ok_transaction_count: summary
            .decoded_trade_candidate_without_trade_event_on_ok_transaction_count,
        decoded_trade_candidate_without_trade_event_on_failed_transaction_count: summary
            .decoded_trade_candidate_without_trade_event_on_failed_transaction_count,
        actionable_missing_trade_event_count: summary.actionable_missing_trade_event_count,
        ignored_failed_transaction_trade_candidate_count: summary
            .ignored_failed_transaction_trade_candidate_count,
        decoded_trade_candidate_without_amount_payload_count: summary
            .decoded_trade_candidate_without_amount_payload_count,
        trade_event_count: summary.trade_event_count,
        invalid_trade_event_count: summary.invalid_trade_event_count,
        pair_candle_count: summary.pair_candle_count,
        duplicate_decoded_event_trade_count: summary.duplicate_decoded_event_trade_count,
        multi_trade_signature_pair_count: summary.multi_trade_signature_pair_count,
        duplicate_candle_bucket_count: summary.duplicate_candle_bucket_count,
        token_count: summary.token_count,
        token_metadata_missing_count: summary.token_metadata_missing_count,
        pool_count: summary.pool_count,
        pair_count: summary.pair_count,
        pair_without_trade_count: summary.pair_without_trade_count,
        pair_without_candle_count: summary.pair_without_candle_count,
        dex_summaries,
        pair_summaries,
        decoded_event_summaries,
        missing_trade_event_reason_summaries,
        non_actionable_pair_count: summary.non_actionable_pair_count,
        non_actionable_pair_summaries,
        missing_trade_event_samples,
        duplicate_decoded_event_trade_samples,
        multi_trade_signature_pair_samples,
        pair_without_trade_samples,
        pair_without_candle_samples,
    }
}

fn demo_pipeline2_map_local_dex_diagnostic_summary(
    summary: kb_lib::LocalDexDiagnosticSummaryDto,
) -> DemoPipeline2LocalDexDiagnosticSummary {
    DemoPipeline2LocalDexDiagnosticSummary {
        dex_code: summary.dex_code,
        pool_count: summary.pool_count,
        pair_count: summary.pair_count,
        decoded_event_count: summary.decoded_event_count,
        decoded_trade_candidate_count: summary.decoded_trade_candidate_count,
        decoded_candle_candidate_count: summary.decoded_candle_candidate_count,
        trade_event_count: summary.trade_event_count,
        pair_candle_count: summary.pair_candle_count,
    }
}

fn demo_pipeline2_map_local_pair_diagnostic_summary(
    summary: kb_lib::LocalPairDiagnosticSummaryDto,
) -> DemoPipeline2LocalPairDiagnosticSummary {
    DemoPipeline2LocalPairDiagnosticSummary {
        pair_id: summary.pair_id,
        pool_address: summary.pool_address,
        dex_code: summary.dex_code,
        base_mint: summary.base_mint,
        base_symbol: summary.base_symbol,
        quote_mint: summary.quote_mint,
        quote_symbol: summary.quote_symbol,
        pair_symbol: summary.pair_symbol,
        decoded_event_count: summary.decoded_event_count,
        decoded_trade_candidate_count: summary.decoded_trade_candidate_count,
        decoded_candle_candidate_count: summary.decoded_candle_candidate_count,
        trade_event_count: summary.trade_event_count,
        invalid_trade_event_count: summary.invalid_trade_event_count,
        pair_candle_count: summary.pair_candle_count,
        last_price_quote_per_base: summary.last_price_quote_per_base,
    }
}

fn demo_pipeline2_map_local_decoded_event_diagnostic_summary(
    summary: kb_lib::LocalDecodedEventDiagnosticSummaryDto,
) -> DemoPipeline2LocalDecodedEventDiagnosticSummary {
    DemoPipeline2LocalDecodedEventDiagnosticSummary {
        protocol_name: summary.protocol_name,
        event_kind: summary.event_kind,
        event_category: summary.event_category,
        trade_candidate: summary.trade_candidate,
        candle_candidate: summary.candle_candidate,
        event_count: summary.event_count,
        trade_event_count: summary.trade_event_count,
    }
}

fn demo_pipeline2_map_missing_trade_event_reason_summary(
    summary: kb_lib::LocalMissingTradeEventReasonSummaryDto,
) -> DemoPipeline2LocalMissingTradeEventReasonSummary {
    return DemoPipeline2LocalMissingTradeEventReasonSummary {
        reason: summary.reason,
        transaction_failed: summary.transaction_failed,
        actionable: summary.actionable,
        event_count: summary.event_count,
        has_base_amount_payload_count: summary.has_base_amount_payload_count,
        has_quote_amount_payload_count: summary.has_quote_amount_payload_count,
        has_price_payload_count: summary.has_price_payload_count,
    };
}

fn demo_pipeline2_map_non_actionable_pair_diagnostic_summary(
    summary: kb_lib::LocalNonActionablePairDiagnosticSummaryDto,
) -> DemoPipeline2LocalNonActionablePairDiagnosticSummary {
    return DemoPipeline2LocalNonActionablePairDiagnosticSummary {
        pair_id: summary.pair_id,
        pool_address: summary.pool_address,
        dex_code: summary.dex_code,
        base_mint: summary.base_mint,
        base_symbol: summary.base_symbol,
        quote_mint: summary.quote_mint,
        quote_symbol: summary.quote_symbol,
        pair_symbol: summary.pair_symbol,
        decoded_trade_candidate_count: summary.decoded_trade_candidate_count,
        non_actionable_missing_trade_event_count: summary.non_actionable_missing_trade_event_count,
        failed_transaction_candidate_count: summary.failed_transaction_candidate_count,
        ok_transaction_without_token_mints_count: summary.ok_transaction_without_token_mints_count,
        ok_transaction_without_amount_payload_count: summary
            .ok_transaction_without_amount_payload_count,
        reason_summary: summary.reason_summary,
        trade_event_count: summary.trade_event_count,
        pair_candle_count: summary.pair_candle_count,
    };
}

fn demo_pipeline2_map_missing_trade_event_sample(
    sample: kb_lib::LocalMissingTradeEventDiagnosticSampleDto,
) -> DemoPipeline2LocalMissingTradeEventDiagnosticSample {
    return DemoPipeline2LocalMissingTradeEventDiagnosticSample {
        decoded_event_id: sample.decoded_event_id,
        transaction_id: sample.transaction_id,
        signature: sample.signature,
        protocol_name: sample.protocol_name,
        event_kind: sample.event_kind,
        pool_account: sample.pool_account,
        transaction_failed: sample.transaction_failed,
        actionable: sample.actionable,
        reason: sample.reason,
        has_base_amount_payload: sample.has_base_amount_payload,
        has_quote_amount_payload: sample.has_quote_amount_payload,
        has_price_payload: sample.has_price_payload,
    };
}

fn demo_pipeline2_map_duplicate_decoded_event_trade_sample(
    sample: kb_lib::LocalDuplicateDecodedEventTradeDiagnosticSampleDto,
) -> DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample {
    return DemoPipeline2LocalDuplicateDecodedEventTradeDiagnosticSample {
        decoded_event_id: sample.decoded_event_id,
        protocol_name: sample.protocol_name,
        event_kind: sample.event_kind,
        pool_account: sample.pool_account,
        trade_event_count: sample.trade_event_count,
        trade_event_ids: sample.trade_event_ids,
        signatures: sample.signatures,
    };
}

fn demo_pipeline2_map_multi_trade_signature_pair_sample(
    sample: kb_lib::LocalMultiTradeSignaturePairDiagnosticSampleDto,
) -> DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample {
    return DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample {
        signature: sample.signature,
        pair_id: sample.pair_id,
        pool_address: sample.pool_address,
        dex_code: sample.dex_code,
        trade_event_count: sample.trade_event_count,
        decoded_event_count: sample.decoded_event_count,
        trade_event_ids: sample.trade_event_ids,
        decoded_event_ids: sample.decoded_event_ids,
    };
}

fn demo_pipeline2_map_pair_gap_sample(
    sample: kb_lib::LocalPairGapDiagnosticSampleDto,
) -> DemoPipeline2LocalPairGapDiagnosticSample {
    return DemoPipeline2LocalPairGapDiagnosticSample {
        pair_id: sample.pair_id,
        pool_address: sample.pool_address,
        dex_code: sample.dex_code,
        base_mint: sample.base_mint,
        base_symbol: sample.base_symbol,
        quote_mint: sample.quote_mint,
        quote_symbol: sample.quote_symbol,
        pair_symbol: sample.pair_symbol,
        decoded_event_count: sample.decoded_event_count,
        decoded_trade_candidate_count: sample.decoded_trade_candidate_count,
        trade_event_count: sample.trade_event_count,
        pair_candle_count: sample.pair_candle_count,
    };
}

fn demo_pipeline2_normalize_http_role(
    role: std::option::Option<std::string::String>,
) -> std::string::String {
    match role {
        Some(role) => {
            let trimmed = role.trim().to_string();
            if trimmed.is_empty() { "history_backfill".to_string() } else { trimmed }
        },
        None => "history_backfill".to_string(),
    }
}
