//! Query engine for executing queries against the KV store.
//!
//! The QueryEngine provides a simple interface for reading assertions
//! from the knowledge graph with filtering support.

use std::sync::Arc;

use stemedb_core::types::Assertion;
use stemedb_storage::{AliasStore, GenericIndexStore, KVStore, VectorIndex, VisualIndex};
// Trait import required for IndexStore methods on GenericIndexStore
#[allow(unused_imports)]
use stemedb_storage::IndexStore;
use tracing::{debug, instrument};

use crate::error::{QueryError, Result};
use crate::query::{Query, QueryResult};

mod candidates;
mod execution;

#[cfg(test)]
mod tests;

/// The query engine executes queries against the KV store.
///
/// # Design Philosophy
///
/// Following the "Deep Module" principle, QueryEngine presents a simple
/// interface (`execute`) that hides the complexity of index lookup,
/// deserialization, and filtering.
///
/// # Similarity Search
///
/// When configured with vector or visual indexes, the engine supports:
/// - **Vector search**: k-NN queries over embedding vectors (`vector_near`)
/// - **Visual search**: Hamming distance queries over perceptual hashes (`visual_near`)
///
/// These use O(log N) index lookups instead of O(N) brute-force scans.
pub struct QueryEngine<S> {
    pub(super) store: Arc<S>,
    pub(super) index_store: GenericIndexStore<Arc<S>>,
    /// Optional vector index for k-NN similarity search.
    pub(super) vector_index: Option<Arc<dyn VectorIndex>>,
    /// Optional visual index for hamming distance search.
    pub(super) visual_index: Option<Arc<dyn VisualIndex>>,
    /// Optional alias store for cross-scheme subject resolution.
    pub(super) alias_store: Option<Arc<dyn AliasStore>>,
}

impl<S: KVStore + 'static> QueryEngine<S> {
    /// Create a new query engine backed by the given store.
    pub fn new(store: Arc<S>) -> Self {
        let index_store = GenericIndexStore::new(store.clone());
        Self { store, index_store, vector_index: None, visual_index: None, alias_store: None }
    }

    /// Attach a vector index for k-NN similarity search.
    ///
    /// When set, queries with `vector_near` use O(log N) HNSW lookup
    /// instead of brute-force scanning.
    pub fn with_vector_index(mut self, index: Arc<dyn VectorIndex>) -> Self {
        self.vector_index = Some(index);
        self
    }

    /// Attach a visual index for perceptual hash similarity search.
    ///
    /// When set, queries with `visual_near` use O(log N) BK-tree lookup
    /// instead of brute-force hamming distance scanning.
    pub fn with_visual_index(mut self, index: Arc<dyn VisualIndex>) -> Self {
        self.visual_index = Some(index);
        self
    }

    /// Attach an alias store for cross-scheme subject resolution.
    ///
    /// When set and a query has `resolve_aliases: true`, the engine will
    /// expand the subject to all aliased paths before fetching assertions.
    /// This enables queries like `code://rust/myapp/tls/cert_verification`
    /// to also return assertions from `rfc://5246/tls/cert_verification`.
    pub fn with_alias_store(mut self, alias_store: Arc<dyn AliasStore>) -> Self {
        self.alias_store = Some(alias_store);
        self
    }

    /// Execute a query and return matching assertions.
    ///
    /// # Query Execution Strategy
    ///
    /// 0. **Similarity search path**: If `vector_near` or `visual_near` is set, use indexed lookup
    /// 1. **Fast path**: If both `subject` and `predicate` are specified, check `MV:{subject}:{predicate}` for a pre-computed winner (O(1))
    /// 2. If both `subject` and `predicate` are specified: Use compound index `SP:{subject}:{predicate}` for O(1) lookup
    /// 3. If only `subject` is specified: Use subject index `S:{subject}` for O(1) lookup
    /// 4. Otherwise: Full scan of `H:` prefix (O(n), avoid in production)
    ///
    /// After fetching candidates, apply lifecycle and epoch filters.
    #[instrument(skip(self, query), fields(
        subject = ?query.subject,
        predicate = ?query.predicate,
        lifecycle = ?query.lifecycle
    ))]
    pub async fn execute(&self, query: &Query) -> Result<QueryResult> {
        // Similarity search path: use vector or visual index when available
        if let Some(ref query_vector) = query.vector_near {
            if let Some(ref vector_index) = self.vector_index {
                let k = query.k.unwrap_or(10);
                debug!(k, dimension = query_vector.len(), "Using vector index for k-NN search");
                let candidates =
                    self.fetch_by_vector_similarity(vector_index, query_vector, k).await?;
                return self.apply_filters_and_return(candidates, query).await;
            } else {
                debug!("vector_near specified but no vector index configured, falling back to standard path");
            }
        }

        if let Some(ref visual_hash_hex) = query.visual_near {
            if let Some(ref visual_index) = self.visual_index {
                let threshold = query.visual_threshold.unwrap_or(8);
                debug!(threshold, "Using visual index for hamming distance search");
                let candidates = self
                    .fetch_by_visual_similarity(visual_index, visual_hash_hex, threshold)
                    .await?;
                return self.apply_filters_and_return(candidates, query).await;
            } else {
                debug!(
                    "visual_near specified but no visual index configured, using brute-force scan"
                );
                // Fall through to standard path which applies visual_near filter in query.matches()
            }
        }

        // Fast path: check materialized view when both subject and predicate are specified
        // Skip fast path if as_of is set (MVs reflect current state, time-travel needs full scan)
        // Skip fast path if resolve_aliases is true (aliases expand to multiple subjects)
        if query.as_of.is_none() && !query.resolve_aliases {
            if let (Some(subject), Some(predicate)) = (&query.subject, &query.predicate) {
                if let Some(result) = self.try_fast_path(subject, predicate, query).await? {
                    debug!(subject, predicate, "Fast path: used materialized view");
                    return Ok(result);
                }
            }
        }

        // Slow path: determine scan strategy based on query filters
        // When resolve_aliases is true, expand subject to all aliased paths
        let candidates = match (&query.subject, &query.predicate, query.resolve_aliases) {
            // Alias-expanded compound index lookup
            (Some(subject), Some(predicate), true) => {
                let subjects = self.resolve_subject_aliases(subject).await?;
                debug!(
                    original_subject = subject,
                    resolved_count = subjects.len(),
                    predicate,
                    "Alias-expanded compound index lookup"
                );
                self.fetch_by_subjects_predicate(&subjects, predicate).await?
            }
            // Alias-expanded subject index lookup
            (Some(subject), None, true) => {
                let subjects = self.resolve_subject_aliases(subject).await?;
                debug!(
                    original_subject = subject,
                    resolved_count = subjects.len(),
                    "Alias-expanded subject index lookup"
                );
                self.fetch_by_subjects(&subjects).await?
            }
            // O(1) compound index lookup (no alias expansion)
            (Some(subject), Some(predicate), false) => {
                debug!(
                    subject,
                    predicate, "Slow path: using compound index SP:{subject}:{predicate}"
                );
                self.fetch_by_subject_predicate(subject, predicate).await?
            }
            // O(1) subject index lookup (no alias expansion)
            (Some(subject), None, false) => {
                debug!(subject, "Using subject index S:{subject}");
                self.fetch_by_subject(subject).await?
            }
            // O(n) full scan (resolve_aliases has no effect without subject)
            _ => {
                debug!("Using full scan (no subject filter)");
                self.fetch_all_assertions().await?
            }
        };

        debug!(candidate_count = candidates.len(), "Fetched candidates");

        // Delegate to execution module for filtering and decay
        self.apply_filters_and_return(candidates, query).await
    }

    /// Fetch changelog entries for a subject+predicate since a timestamp.
    ///
    /// Scans `MVC:{subject}:{predicate}:*` keys and returns entries where
    /// the key's timestamp component is >= `since_ts`.
    pub async fn fetch_changes_since(
        &self,
        subject: &str,
        predicate: &str,
        since_ts: u64,
    ) -> Result<Vec<stemedb_core::types::ChangeEntry>> {
        let prefix = format!("MVC:{}:{}:", subject, predicate).into_bytes();
        let entries = self.store.scan_prefix(&prefix).await?;

        let mut changes = Vec::new();
        // Convert since_ts (seconds) to nanoseconds for comparison with key timestamps
        let since_ts_nanos = (since_ts as u128) * 1_000_000_000;

        for (key, data) in entries {
            // Parse timestamp from key: MVC:{subject}:{predicate}:{timestamp_nanos}
            // The key uses nanosecond precision to avoid collisions
            let key_str = match std::str::from_utf8(&key) {
                Ok(s) => s,
                Err(_) => continue, // Skip malformed keys
            };
            if let Some(ts_str) = key_str.rsplit(':').next() {
                // Parse as u128 (nanoseconds) and compare against since_ts_nanos
                if let Ok(ts_nanos) = ts_str.parse::<u128>() {
                    if ts_nanos >= since_ts_nanos {
                        match stemedb_core::serde::deserialize(&data) {
                            Ok(entry) => changes.push(entry),
                            Err(e) => {
                                debug!(key = %key_str, error = %e, "Skipping malformed changelog entry");
                            }
                        }
                    }
                }
            }
        }

        // Sort by timestamp ascending (oldest first)
        changes.sort_by_key(|e: &stemedb_core::types::ChangeEntry| e.timestamp);

        Ok(changes)
    }

    /// Deserialize an assertion using the canonical serde module.
    pub(super) fn deserialize_assertion(&self, data: &[u8]) -> Result<Assertion> {
        stemedb_core::serde::deserialize(data)
            .map_err(|e| QueryError::Deserialization(e.to_string()))
    }

    /// Resolve a subject to all aliased paths via the AliasStore.
    ///
    /// If no alias store is configured, returns just the original subject.
    /// This allows queries with `resolve_aliases: true` to gracefully degrade
    /// when no alias store is available.
    async fn resolve_subject_aliases(&self, subject: &str) -> Result<Vec<String>> {
        match &self.alias_store {
            Some(store) => {
                let resolved = store.resolve_all(subject).await.map_err(QueryError::from)?;
                debug!(
                    subject,
                    resolved_count = resolved.len(),
                    resolved_subjects = ?resolved,
                    "Resolved subject aliases"
                );
                Ok(resolved)
            }
            None => {
                debug!(
                    subject,
                    "resolve_aliases: true but no alias_store configured, using exact subject"
                );
                Ok(vec![subject.to_string()])
            }
        }
    }
}
