//! Background Materializer for O(1) read performance.
//!
//! The Materializer scans all subject+predicate combinations, resolves each
//! through a lens (default: VoteAwareConsensus), and writes the winning assertion
//! to `MV:{subject}:{predicate}` in the KV store.
//!
//! # Architecture
//!
//! From the architecture spec (Section 3, Step 4):
//! > "A background worker aggregates Votes + TrustRank to update the
//! > `MV:{Subject}:{Predicate}` key. This ensures that Read queries are O(1)
//! > lookups on the Materialized View, not O(N) calculations."
//!
//! # Design Philosophy
//!
//! Following the "Deep Module" principle:
//! - Simple interface: `step()` performs one full materialization pass
//! - Complex implementation: Discovers SP: keys, fetches candidates,
//!   runs async lenses, writes MV: keys, tracks statistics
//! - `run()` wraps `step()` in a background loop with error resilience
//!
//! # Storage Layout
//!
//! | Key Pattern | Value | Purpose |
//! |-------------|-------|---------|
//! | `MV:{subject}:{predicate}` | Serialized `MaterializedView` | Pre-computed winner |

use crate::error::{QueryError, Result};
use std::sync::Arc;
use stemedb_core::types::{Assertion, EscalationEvent, EscalationPolicy, MaterializedView};
use stemedb_lens::AsyncLens;
use stemedb_storage::{key_codec, EscalationStore, GenericIndexStore, KVStore};
use tokio::sync::Notify;
use tracing::{debug, error, info, instrument, warn};

/// Report from a single materialization pass.
#[derive(Debug, Default)]
pub struct MaterializeReport {
    /// Number of subject+predicate pairs scanned.
    pub pairs_scanned: usize,
    /// Number of views that were updated (winner changed or new).
    pub views_updated: usize,
    /// Number of pairs skipped due to errors.
    pub errors: usize,
    /// Number of pairs with no candidates (empty after filter).
    pub empty_pairs: usize,
    /// Number of escalation events triggered.
    pub escalations_triggered: usize,
}

/// Background worker that maintains materialized views for O(1) reads.
///
/// The Materializer takes an `AsyncLens` implementation and uses it to
/// resolve all known subject+predicate pairs into winning assertions,
/// storing the results at `MV:{subject}:{predicate}`.
///
/// # Example
///
/// ```ignore
/// use stemedb_query::Materializer;
/// use stemedb_storage::{HybridStore, GenericVoteStore};
/// use stemedb_lens::VoteAwareConsensusLens;
/// use std::sync::Arc;
///
/// let store = Arc::new(HybridStore::open("./data")?);
/// let vote_store = Arc::new(GenericVoteStore::new(store.clone()));
/// let lens = VoteAwareConsensusLens::new(vote_store);
///
/// let materializer = Materializer::new(store, Box::new(lens));
/// let report = materializer.step().await?;
/// println!("Updated {} views", report.views_updated);
/// ```
pub struct Materializer<S> {
    store: Arc<S>,
    index_store: GenericIndexStore<Arc<S>>,
    lens: Box<dyn AsyncLens>,
    escalation_policies: Vec<EscalationPolicy>,
    escalation_store: Option<Arc<dyn EscalationStore>>,
}

impl<S: KVStore + 'static> Materializer<S> {
    /// Create a new Materializer with the given store and lens.
    ///
    /// The lens determines the resolution strategy for materialized views.
    /// Use `VoteAwareConsensusLens` for the standard Ballot Box consensus,
    /// or `TrustAwareAuthorityLens` for reputation-weighted resolution.
    pub fn new(store: Arc<S>, lens: Box<dyn AsyncLens>) -> Self {
        let index_store = GenericIndexStore::new(store.clone());
        Self { store, index_store, lens, escalation_policies: Vec::new(), escalation_store: None }
    }

    /// Configure escalation policies and storage.
    ///
    /// When a conflict score exceeds a policy threshold, an escalation event
    /// will be recorded in the escalation store for external review.
    ///
    /// # Example
    ///
    /// ```ignore
    /// use stemedb_storage::GenericEscalationStore;
    /// use stemedb_core::types::{EscalationPolicy, EscalationLevel};
    ///
    /// let esc_store = Arc::new(GenericEscalationStore::new(store.clone()));
    /// let policy = EscalationPolicy {
    ///     name: "HighConflict".to_string(),
    ///     min_conflict_score: 0.7,
    ///     level: EscalationLevel::High,
    ///     predicate_pattern: None,
    /// };
    ///
    /// let materializer = Materializer::new(store, lens)
    ///     .with_escalation(esc_store, vec![policy]);
    /// ```
    pub fn with_escalation(
        mut self,
        store: Arc<dyn EscalationStore>,
        policies: Vec<EscalationPolicy>,
    ) -> Self {
        self.escalation_store = Some(store);
        self.escalation_policies = policies;
        self
    }

    /// Perform one full materialization pass.
    ///
    /// Scans all `SP:` keys to discover subject+predicate pairs, fetches
    /// candidate assertions for each pair, runs the lens, and writes the
    /// winning assertion to `MV:{subject}:{predicate}`.
    ///
    /// Returns a report with statistics about the pass.
    #[instrument(skip(self), fields(lens = %self.lens.name()))]
    pub async fn step(&self) -> Result<MaterializeReport> {
        let mut report = MaterializeReport::default();

        // Discover all subject+predicate pairs from subject-prefixed SP: keys
        // We scan all subjects first, then fetch their SP: keys
        let subject_entries = self.store.scan_prefix(&key_codec::subjects_scan_prefix()).await?;
        let mut sp_pairs: Vec<(String, String)> = Vec::new();

        for (key, _) in &subject_entries {
            let subject = match key_codec::extract_subject_from_subjects_key(key) {
                Some(s) => s,
                None => continue,
            };

            // Scan this subject's SP: keys
            let sp_prefix = key_codec::subject_predicate_scan_prefix(&subject);
            let sp_entries = self.store.scan_prefix(&sp_prefix).await?;
            for (sp_key, _) in sp_entries {
                if let Some((s, p)) = key_codec::extract_sp_key(&sp_key) {
                    sp_pairs.push((s, p));
                }
            }
        }

        for (subject, predicate) in &sp_pairs {
            report.pairs_scanned += 1;

            // Materialize this subject+predicate pair
            match self.materialize_pair(subject, predicate).await {
                Ok(Some(view)) => {
                    report.views_updated += 1;
                    // Check escalation policies
                    if self.check_escalation_policies(&view, &mut report).await {
                        report.escalations_triggered += 1;
                    }
                }
                Ok(None) => {
                    report.empty_pairs += 1;
                }
                Err(e) => {
                    debug!(
                        subject = %subject,
                        predicate = %predicate,
                        error = %e,
                        "Failed to materialize pair, skipping"
                    );
                    report.errors += 1;
                }
            }
        }

        info!(
            pairs_scanned = report.pairs_scanned,
            views_updated = report.views_updated,
            empty_pairs = report.empty_pairs,
            errors = report.errors,
            escalations_triggered = report.escalations_triggered,
            "Materialization pass complete"
        );

        Ok(report)
    }

    /// Materialize a single subject+predicate pair.
    ///
    /// Fetches all candidate assertions, runs the lens, and writes the
    /// result to `MV:{subject}:{predicate}` if a winner is found.
    ///
    /// Returns the `MaterializedView` if a winner was found, `None` if
    /// there were no candidates.
    #[instrument(skip(self), fields(subject = %subject, predicate = %predicate, lens = %self.lens.name()))]
    pub async fn materialize_pair(
        &self,
        subject: &str,
        predicate: &str,
    ) -> Result<Option<MaterializedView>> {
        // Fetch candidate assertions via compound index
        let candidates = self.fetch_candidates(subject, predicate).await?;

        if candidates.is_empty() {
            debug!(subject, predicate, "No candidates found");
            return Ok(None);
        }

        debug!(
            subject,
            predicate,
            candidates_count = candidates.len(),
            "Resolving candidates through lens"
        );

        // Run the lens
        let resolution = self.lens.resolve_async(&candidates).await;

        let winner = match resolution.winner {
            Some(winner) => winner,
            None => {
                debug!(subject, predicate, "Lens returned no winner");
                return Ok(None);
            }
        };

        // Build the materialized view
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);

        let view = MaterializedView {
            winner,
            lens_name: self.lens.name().to_string(),
            resolution_confidence: resolution.resolution_confidence,
            conflict_score: resolution.conflict_score,
            candidates_count: resolution.candidates_count,
            materialized_at: now,
        };

        // Write to {subject}\x00MV:{predicate}
        let mv_key = key_codec::mv_key(subject, predicate);
        let serialized = stemedb_core::serde::serialize(&view)
            .map_err(|e| QueryError::Deserialization(e.to_string()))?;
        self.store.put(&mv_key, &serialized).await?;

        debug!(
            subject,
            predicate,
            confidence = view.resolution_confidence,
            candidates = view.candidates_count,
            "Materialized view updated"
        );

        Ok(Some(view))
    }

    /// Read a materialized view from the store.
    ///
    /// Returns `None` if no materialized view exists for this subject+predicate.
    /// This is the O(1) fast-path read.
    #[instrument(skip(self), fields(subject = %subject, predicate = %predicate))]
    pub async fn get_materialized_view(
        &self,
        subject: &str,
        predicate: &str,
    ) -> Result<Option<MaterializedView>> {
        let key = key_codec::mv_key(subject, predicate);
        match self.store.get(&key).await? {
            Some(data) => {
                let view: MaterializedView = stemedb_core::serde::deserialize(&data)
                    .map_err(|e| QueryError::Deserialization(e.to_string()))?;
                Ok(Some(view))
            }
            None => Ok(None),
        }
    }

    /// Run the materializer continuously in the background (polling mode).
    ///
    /// Performs periodic materialization passes with a configurable interval.
    /// Errors during a pass are logged but do not stop the loop.
    ///
    /// For event-driven mode, use [`run_notified`] instead.
    pub async fn run(&self, interval: std::time::Duration) {
        info!(
            interval_ms = interval.as_millis() as u64,
            lens = %self.lens.name(),
            "Starting materializer loop (polling mode)"
        );

        loop {
            self.run_step().await;
            tokio::time::sleep(interval).await;
        }
    }

    /// Run the materializer in event-driven mode.
    ///
    /// Waits for a notification from the ingestion pipeline (via `Notify`)
    /// before performing a materialization pass. Uses a timeout as a safety
    /// net to catch missed notifications and ensure eventual consistency.
    ///
    /// # Arguments
    ///
    /// * `notify` - Shared notification channel, signaled by the IngestWorker
    /// * `max_interval` - Maximum time to wait between passes (fallback timeout)
    pub async fn run_notified(&self, notify: Arc<Notify>, max_interval: std::time::Duration) {
        info!(
            max_interval_ms = max_interval.as_millis() as u64,
            lens = %self.lens.name(),
            "Starting materializer loop (event-driven mode)"
        );

        loop {
            // Wait for notification or timeout — whichever comes first
            let was_notified = tokio::time::timeout(max_interval, notify.notified()).await.is_ok();

            if was_notified {
                debug!("Materialization triggered by ingestion notification");
            } else {
                debug!("Materialization triggered by timeout fallback");
            }

            self.run_step().await;
        }
    }

    /// Execute a single materialization pass, logging results.
    async fn run_step(&self) {
        match self.step().await {
            Ok(report) => {
                if report.views_updated > 0 {
                    info!(
                        updated = report.views_updated,
                        scanned = report.pairs_scanned,
                        "Materialization pass updated views"
                    );
                }
            }
            Err(e) => {
                error!(error = %e, "Materialization pass failed");
            }
        }
    }

    /// Fetch candidate assertions for a subject+predicate pair.
    async fn fetch_candidates(&self, subject: &str, predicate: &str) -> Result<Vec<Assertion>> {
        use stemedb_storage::IndexStore;

        let hash_list = self.index_store.get_by_subject_predicate(subject, predicate).await?;

        let mut candidates = Vec::with_capacity(hash_list.len());
        for hash in hash_list {
            let key = key_codec::assertion_key(subject, &hex::encode(hash));
            if let Some(data) = self.store.get(&key).await? {
                match stemedb_core::serde::deserialize::<Assertion>(&data) {
                    Ok(assertion) => candidates.push(assertion),
                    Err(e) => {
                        debug!(
                            hash = %hex::encode(hash),
                            error = %e,
                            "Skipping malformed assertion"
                        );
                    }
                }
            }
        }

        Ok(candidates)
    }

    /// Check if any escalation policies should trigger for this materialized view.
    ///
    /// If a policy triggers, write an escalation event to storage.
    /// Returns `true` if at least one escalation was triggered.
    async fn check_escalation_policies(
        &self,
        view: &MaterializedView,
        _report: &mut MaterializeReport,
    ) -> bool {
        // No escalation store configured - skip
        let escalation_store = match &self.escalation_store {
            Some(store) => store,
            None => return false,
        };

        if self.escalation_policies.is_empty() {
            return false;
        }

        let mut triggered = false;

        // Compute timestamp once before the loop so all policies get the same timestamp
        let timestamp_nanos = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_nanos() as u64)
            .unwrap_or(0);

        for policy in &self.escalation_policies {
            if policy.should_trigger(view.conflict_score, &view.winner.predicate) {
                let id = EscalationEvent::compute_id(
                    &view.winner.subject,
                    &view.winner.predicate,
                    view.conflict_score,
                    timestamp_nanos,
                );

                let event = EscalationEvent {
                    id,
                    subject: view.winner.subject.clone(),
                    predicate: view.winner.predicate.clone(),
                    conflict_score: view.conflict_score,
                    level: policy.level,
                    reason: format!(
                        "Policy '{}' triggered: conflict score {:.2} >= threshold {:.2}",
                        policy.name, view.conflict_score, policy.min_conflict_score
                    ),
                    timestamp: timestamp_nanos,
                    resolved: false,
                };

                match escalation_store.write_escalation(&event).await {
                    Ok(()) => {
                        info!(
                            subject = %view.winner.subject,
                            predicate = %view.winner.predicate,
                            conflict_score = view.conflict_score,
                            policy = %policy.name,
                            level = ?policy.level,
                            "Escalation triggered"
                        );
                        triggered = true;
                    }
                    Err(e) => {
                        warn!(
                            subject = %view.winner.subject,
                            predicate = %view.winner.predicate,
                            error = %e,
                            "Failed to write escalation event"
                        );
                    }
                }
            }
        }

        triggered
    }
}

#[cfg(test)]
mod tests;
