//! Event-driven cache invalidation via NATS.
//!
//! Subscribes to `provisioning.workspace.ncl.*` subjects. When the orchestrator
//! (or any other writer) publishes a file-change event, ncl-sync re-exports
//! the affected NCL file — no filesystem polling, no file-watcher debounce delay.
//!
//! Contract for publishers:
//!
//! Subject: `provisioning.workspace.ncl.changed` | `provisioning.workspace.ncl.removed`
//!
//! Payload:
//! ```json
//! {
//!   "workspace": "/path/to/workspace",
//!   "path":      "/path/to/workspace/infra/.../file.ncl",
//!   "import_paths": ["/path/to/workspace", "/provisioning"],
//!   "source":    "orchestrator" | "cli" | "..."
//! }
//! ```
//!
//! The subscriber ignores events whose `workspace` does not match the daemon's
//! watched workspace — each workspace runs its own ncl-sync daemon.

use crate::exporter::{evict, export_ncl, ExportRequest};
use crate::manifest::Manifest;
use anyhow::Result;
use futures::StreamExt;
use platform_nats::NatsBridge;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn};

/// Event payload published by writers (orchestrator, future publishers).
#[derive(Debug, Serialize, Deserialize)]
pub struct NclChangeEvent {
    /// Absolute path to the workspace root that contains this file.
    pub workspace: String,
    /// Absolute path to the NCL file that changed.
    pub path: String,
    /// Import paths the writer wants ncl-sync to use for re-export (optional).
    #[serde(default)]
    pub import_paths: Vec<String>,
    /// Origin of the event (orchestrator, cli, etc.) — for logging only.
    #[serde(default)]
    pub source: String,
}

pub struct SubscriberConfig {
    pub workspace: PathBuf,
    pub cache_dir: PathBuf,
    pub default_import_paths: Vec<String>,
}

/// Spawn a background task that subscribes to ncl change events.
/// Returns immediately — subscriber runs until the process exits or the bridge disconnects.
pub async fn spawn(bridge: Arc<NatsBridge>, cfg: SubscriberConfig) -> Result<()> {
    let changed = bridge.subscribe("provisioning.workspace.ncl.changed").await?;
    let removed = bridge.subscribe("provisioning.workspace.ncl.removed").await?;

    info!(
        workspace = %cfg.workspace.display(),
        "NATS subscriber started on provisioning.workspace.ncl.{{changed,removed}}"
    );

    let cfg = Arc::new(cfg);

    let cfg_a = Arc::clone(&cfg);
    tokio::spawn(handle_subject(changed, cfg_a, SubjectKind::Changed));

    let cfg_b = Arc::clone(&cfg);
    tokio::spawn(handle_subject(removed, cfg_b, SubjectKind::Removed));

    Ok(())
}

#[derive(Clone, Copy)]
enum SubjectKind {
    Changed,
    Removed,
}

async fn handle_subject(
    mut sub: async_nats::Subscriber,
    cfg: Arc<SubscriberConfig>,
    kind: SubjectKind,
) {
    let manifest_lock: Arc<Mutex<()>> = Arc::new(Mutex::new(()));

    while let Some(msg) = sub.next().await {
        let event: NclChangeEvent = match serde_json::from_slice(&msg.payload) {
            Ok(e) => e,
            Err(e) => {
                warn!("parse NATS payload on {}: {}", msg.subject, e);
                continue;
            }
        };

        if !workspace_matches(&cfg.workspace, &event.workspace) {
            debug!(
                "skip event for foreign workspace {} (watching {})",
                event.workspace,
                cfg.workspace.display()
            );
            continue;
        }

        let path = PathBuf::from(&event.path);
        let import_paths = if event.import_paths.is_empty() {
            cfg.default_import_paths.clone()
        } else {
            event.import_paths.clone()
        };

        // Serialize manifest access — concurrent events could race on save().
        let _guard = manifest_lock.lock().await;
        let mut manifest = match Manifest::load(&cfg.cache_dir) {
            Ok(m) => m,
            Err(e) => {
                error!("load manifest on NATS event: {}", e);
                continue;
            }
        };

        match kind {
            SubjectKind::Changed => {
                let req = ExportRequest { path, import_paths };
                if let Err(e) = export_ncl(&req, &cfg.cache_dir, &mut manifest).await {
                    error!("NATS-driven export failed for {}: {}", req.path.display(), e);
                    continue;
                }
                debug!(source = %event.source, "NATS: cache refreshed");
            }
            SubjectKind::Removed => {
                evict(&path, &cfg.cache_dir, &mut manifest).await;
                debug!(source = %event.source, "NATS: cache evicted");
            }
        }

        if let Err(e) = manifest.save(&cfg.cache_dir) {
            error!("save manifest after NATS event: {}", e);
        }
    }

    warn!("NATS subscription ended — reconnect not implemented");
}

fn workspace_matches(watched: &Path, event_ws: &str) -> bool {
    let event_ws_pb = PathBuf::from(event_ws);
    let canon_watched = watched.canonicalize().unwrap_or_else(|_| watched.to_path_buf());
    let canon_event = event_ws_pb.canonicalize().unwrap_or(event_ws_pb);
    canon_watched == canon_event
}
