//! Query parameter DTOs for filtering and searching assertions.

use serde::{Deserialize, Serialize};
use stemedb_core::limits::DEFAULT_QUERY_LIMIT;
use utoipa::ToSchema;

use super::enums::{LensDto, LifecycleDto};

// ============================================================================
// Query Parameters
// ============================================================================

/// Query parameters for searching assertions.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(default)]
pub struct QueryParams {
    /// Filter by subject entity
    pub subject: Option<String>,

    /// Filter by predicate/relation
    pub predicate: Option<String>,

    /// Filter by lifecycle stage
    pub lifecycle: Option<LifecycleDto>,

    /// Filter by epoch (hex-encoded)
    pub epoch: Option<String>,

    /// Apply a lens for conflict resolution
    pub lens: Option<LensDto>,

    /// Maximum number of results to return (defaults to 100)
    #[serde(default = "default_limit")]
    pub limit: usize,

    /// Maximum acceptable staleness of materialized views in seconds.
    ///
    /// When set, the fast path (MV lookup) will be skipped if the materialized
    /// view is older than this threshold. This causes the query to re-compute
    /// the result from all candidate assertions.
    ///
    /// - Not set: Accept any MV age (default, fastest)
    /// - 0: Never use cached MV, always compute fresh
    /// - 60: Only use MV if materialized within the last 60 seconds
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_stale: Option<u64>,

    /// Filter by visual similarity to a reference pHash (hex-encoded, 16 chars).
    ///
    /// Returns assertions whose `visual_hash` has hamming distance <= `visual_threshold`.
    /// Assertions without a `visual_hash` are excluded when this is set.
    ///
    /// Example: `visual_near=a3f2b1c4d5e6f708` finds assertions with similar screenshots.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub visual_near: Option<String>,

    /// Maximum hamming distance for `visual_near` matching (0-64, default: 8).
    ///
    /// Lower values require closer visual similarity:
    /// - 0: Exact match only
    /// - 8 (default): ~12.5% bit difference, catches resizing/compression
    /// - 16: ~25% bit difference, more lenient
    #[serde(skip_serializing_if = "Option::is_none")]
    pub visual_threshold: Option<u32>,

    /// Query state as of this Unix timestamp (time-travel).
    ///
    /// Returns only assertions created at or before this timestamp.
    /// When set, the fast path (MV lookup) is bypassed.
    ///
    /// - Not set: Query current state (default)
    /// - 0: Return assertions from the beginning of time (all timestamps >= 0)
    /// - 1704067200: Return assertions as of 2024-01-01 00:00:00 UTC
    #[serde(skip_serializing_if = "Option::is_none")]
    pub as_of: Option<u64>,

    /// Decay half-life in seconds for confidence decay.
    ///
    /// When set, older assertions have their effective confidence reduced based on age.
    /// This implements semantic decay: older sources shouldn't compete equally
    /// with recent evidence.
    ///
    /// Formula: `effective_confidence = confidence * 2^(-(age / halflife))`
    ///
    /// - Not set: No decay (default, backward-compatible)
    /// - 31536000: 1-year half-life (~50% confidence loss per year)
    /// - 86400: 1-day half-life (fast decay for rapidly changing data)
    ///
    /// **Note**: When decay is enabled, materialized views (fast path) are bypassed
    /// because MVs store pre-computed winners without decay applied. Queries with
    /// decay always use the slow path to ensure accurate confidence ordering.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub decay_halflife: Option<u64>,

    /// Use source-class-aware decay instead of uniform decay.
    ///
    /// When `true` and `decay_halflife` is also set, each assertion's decay
    /// is based on its source_class tier (Regulatory=none, Clinical=2yr, etc.).
    /// Falls back to `decay_halflife` for assertions without source_class.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_class_decay: Option<bool>,

    /// Query by semantic vector similarity (k-nearest neighbors).
    ///
    /// When set, the QueryEngine uses the vector index for candidate retrieval
    /// instead of the standard SP/S indexes. This enables semantic similarity
    /// queries like "find assertions with embeddings similar to this one."
    ///
    /// Provide a JSON array of floats representing the query embedding vector.
    /// The dimension must match the vectors stored in the index.
    ///
    /// - Not set: Use standard index-based lookup (default)
    /// - Set: Use vector index for k-NN search
    ///
    /// **Note**: When `vector_near` is set, the fast path (MV lookup) is bypassed.
    /// Subject/predicate filters are applied AFTER vector search.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vector_near: Option<Vec<f32>>,

    /// Number of nearest neighbors to return for vector search.
    ///
    /// Only used when `vector_near` is set. Defaults to 10 if not specified.
    ///
    /// - Not set: Return 10 nearest neighbors (default)
    /// - Set: Return up to `k` nearest neighbors
    #[serde(skip_serializing_if = "Option::is_none")]
    pub k: Option<usize>,

    /// Return changelog entries for MV changes since this timestamp.
    ///
    /// When set, the response includes a `changes_since` list showing winner
    /// changes to materialized views that occurred after this timestamp.
    /// Enables incremental sync: "What changed since I last checked?"
    ///
    /// - Not set: No changelog (default, backward-compatible)
    /// - Set: Include MV winner changes since timestamp
    ///
    /// **Note**: When `since` is set, the fast path (MV lookup) is bypassed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub since: Option<u64>,

    /// Filter by source metadata: journal name (case-insensitive).
    ///
    /// Returns only assertions whose `source_metadata` JSON contains a `journal`
    /// field matching this value.
    ///
    /// Example: `source_journal=NEJM` returns assertions from New England Journal of Medicine.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(example = "NEJM")]
    pub source_journal: Option<String>,

    /// Filter by source metadata: DOI (Digital Object Identifier).
    ///
    /// Returns only assertions whose `source_metadata` JSON contains a `doi`
    /// field matching this value (case-insensitive).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(example = "10.1038/xyz")]
    pub source_doi: Option<String>,

    /// Filter by source metadata: platform (e.g., "PubMed", "Reddit").
    ///
    /// Returns only assertions whose `source_metadata` JSON contains a `platform`
    /// field matching this value (case-insensitive).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(example = "PubMed")]
    pub source_platform: Option<String>,

    /// Filter by source metadata: study design (e.g., "RCT", "observational").
    ///
    /// Returns only assertions whose `source_metadata` JSON contains a `study_design`
    /// field matching this value (case-insensitive).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(example = "RCT")]
    pub source_study_design: Option<String>,

    /// Minimum conflict score threshold (0.0 to 1.0).
    ///
    /// When set, only returns results where the resolved conflict_score is >= this value.
    /// Used to filter for controversial claims where assertions significantly disagree.
    ///
    /// - Not set: No conflict filtering (default)
    /// - 0.7: Only show claims with high conflict (disagreement)
    /// - 0.0: Show all claims (equivalent to not set)
    ///
    /// **Note**: This is a POST-resolution filter. The query executes normally,
    /// but results are filtered by conflict score after lens resolution.
    /// Only works with materialized views (fast path).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(example = 0.7)]
    pub min_conflict_score: Option<f32>,

    /// Maximum conflict score threshold (0.0 to 1.0).
    ///
    /// When set, only returns results where the resolved conflict_score is <= this value.
    /// Used to filter for claims with strong agreement.
    ///
    /// - Not set: No conflict filtering (default)
    /// - 0.2: Only show claims with high agreement
    /// - 1.0: Show all claims (equivalent to not set)
    ///
    /// **Note**: This is a POST-resolution filter. The query executes normally,
    /// but results are filtered by conflict score after lens resolution.
    /// Only works with materialized views (fast path).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(example = 0.2)]
    pub max_conflict_score: Option<f32>,

    /// Exclude assertions that cite quarantined sources.
    ///
    /// When true, assertions citing sources with status=Quarantined are
    /// filtered from results. When false (default), they appear with warnings.
    ///
    /// - Not set/false: Show all assertions, attach warnings to quarantined
    /// - true: Filter out assertions citing quarantined sources
    ///
    /// **Note**: This is a POST-retrieval filter applied after query execution.
    /// Deprecated sources are always shown with warnings regardless of this setting.
    #[serde(default)]
    pub exclude_quarantined_sources: bool,
}

fn default_limit() -> usize {
    DEFAULT_QUERY_LIMIT
}

impl Default for QueryParams {
    fn default() -> Self {
        Self {
            subject: None,
            predicate: None,
            lifecycle: None,
            epoch: None,
            lens: None,
            limit: DEFAULT_QUERY_LIMIT,
            max_stale: None,
            visual_near: None,
            visual_threshold: None,
            as_of: None,
            decay_halflife: None,
            source_class_decay: None,
            vector_near: None,
            k: None,
            since: None,
            source_journal: None,
            source_doi: None,
            source_platform: None,
            source_study_design: None,
            min_conflict_score: None,
            max_conflict_score: None,
            exclude_quarantined_sources: false,
        }
    }
}
