//! Passive drift observer.
//!
//! Watches `crates/`, `.ontology/`, and `adrs/` for changes. After a debounce
//! window, spawns `./ontoref sync scan` followed by `./ontoref sync diff`. If
//! the diff output contains MISSING, STALE, DRIFT, or BROKEN items it pushes a
//! custom notification into every registered project's notification ring
//! buffer.
//!
//! Intentionally read-only: no `apply` step is ever triggered automatically.

use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};

use crate::notifications::NotificationStore;

const DEBOUNCE: Duration = Duration::from_secs(15);

pub struct DriftWatcher {
    _watcher: RecommendedWatcher,
    _task: tokio::task::JoinHandle<()>,
}

impl DriftWatcher {
    /// Start watching `project_root` for source and ontology changes.
    ///
    /// Watches `crates/`, `.ontology/`, `adrs/` (and `reflection/modes/`).
    /// When drift is detected after `./ontoref sync scan && sync diff`, emits
    /// a custom notification via `notifications`.
    pub fn start(
        project_root: &Path,
        project_name: String,
        notifications: Arc<NotificationStore>,
    ) -> Result<Self, crate::error::DaemonError> {
        let root = project_root.to_path_buf();
        let (tx, rx) = mpsc::channel::<PathBuf>(64);

        let tx2 = tx.clone();
        let mut watcher = RecommendedWatcher::new(
            move |res: std::result::Result<Event, notify::Error>| match res {
                Ok(event) => {
                    for path in event.paths {
                        let _ = tx2.try_send(path);
                    }
                }
                Err(e) => warn!(error = %e, "drift watcher notify error"),
            },
            Config::default(),
        )
        .map_err(|e| crate::error::DaemonError::Watcher(e.to_string()))?;

        // Watch relevant subtrees; ignore missing dirs gracefully.
        for subdir in &["crates", ".ontology", "adrs", "reflection/modes"] {
            let path = root.join(subdir);
            if path.exists() {
                watcher
                    .watch(&path, RecursiveMode::Recursive)
                    .map_err(|e| crate::error::DaemonError::Watcher(e.to_string()))?;
                debug!(path = %path.display(), "drift watcher: watching");
            }
        }

        info!(root = %root.display(), "drift watcher started");

        let task = tokio::spawn(drift_loop(rx, root, project_name, notifications));

        Ok(Self {
            _watcher: watcher,
            _task: task,
        })
    }
}

async fn drift_loop(
    mut rx: mpsc::Receiver<PathBuf>,
    root: PathBuf,
    project: String,
    notifications: Arc<NotificationStore>,
) {
    loop {
        // Block until first change arrives.
        let Some(_first) = rx.recv().await else {
            return;
        };

        // Drain events within debounce window.
        tokio::time::sleep(DEBOUNCE).await;
        while rx.try_recv().is_ok() {}

        info!(
            project,
            "drift watcher: change detected — running scan+diff"
        );
        run_scan_diff(&root, &project, &notifications).await;
    }
}

async fn run_scan_diff(root: &Path, project: &str, notifications: &Arc<NotificationStore>) {
    let ontoref_bin = root.join("ontoref");
    if !ontoref_bin.exists() {
        debug!("drift watcher: ontoref binary not found, skipping scan+diff");
        return;
    }

    // Phase 1: scan.
    let scan_ok = tokio::process::Command::new(&ontoref_bin)
        .args(["sync", "scan"])
        .current_dir(root)
        .output()
        .await
        .map(|o| o.status.success())
        .unwrap_or(false);

    if !scan_ok {
        warn!(project, "drift watcher: sync scan failed");
        return;
    }

    // Phase 2: diff — capture stdout to detect drift.
    let diff_out = match tokio::process::Command::new(&ontoref_bin)
        .args(["sync", "diff"])
        .current_dir(root)
        .output()
        .await
    {
        Ok(o) => o,
        Err(e) => {
            warn!(project, error = %e, "drift watcher: sync diff spawn failed");
            return;
        }
    };

    let stdout = String::from_utf8_lossy(&diff_out.stdout);
    let stderr = String::from_utf8_lossy(&diff_out.stderr);
    let combined = format!("{stdout}{stderr}");

    let counts = DriftCounts::parse(&combined);

    if counts.is_clean() {
        debug!(project, "drift watcher: no drift detected");
        return;
    }

    let summary = counts.summary();
    info!(project, %summary, "drift watcher: ontology drift detected");

    notifications.push_custom(
        project,
        "ontology_drift",
        format!("Ontology drift detected — {summary}"),
        Some(serde_json::json!({
            "kind":    "drift",
            "counts":  {
                "missing": counts.missing,
                "stale":   counts.stale,
                "drift":   counts.drift,
                "broken":  counts.broken,
            },
            "hint": "Run `./ontoref sync-ontology` to review and apply patches.",
        })),
        Some("drift-watcher".to_string()),
        None,
    );
}

struct DriftCounts {
    missing: usize,
    stale: usize,
    drift: usize,
    broken: usize,
}

impl DriftCounts {
    /// Parse diff output looking for MISSING / STALE / DRIFT / BROKEN markers.
    ///
    /// The format from `./ontoref sync diff` uses these keywords as line
    /// prefixes or inline labels. We count distinct occurrences.
    fn parse(output: &str) -> Self {
        let missing = count_marker(output, "MISSING");
        let stale = count_marker(output, "STALE");
        let drift = count_marker(output, "DRIFT");
        let broken = count_marker(output, "BROKEN");
        Self {
            missing,
            stale,
            drift,
            broken,
        }
    }

    fn is_clean(&self) -> bool {
        self.missing == 0 && self.stale == 0 && self.drift == 0 && self.broken == 0
    }

    fn summary(&self) -> String {
        let mut parts = Vec::new();
        if self.missing > 0 {
            parts.push(format!("{} MISSING", self.missing));
        }
        if self.stale > 0 {
            parts.push(format!("{} STALE", self.stale));
        }
        if self.drift > 0 {
            parts.push(format!("{} DRIFT", self.drift));
        }
        if self.broken > 0 {
            parts.push(format!("{} BROKEN", self.broken));
        }
        parts.join(", ")
    }
}

fn count_marker(output: &str, marker: &str) -> usize {
    output.lines().filter(|l| l.contains(marker)).count()
}
