use std::path::{Path, PathBuf};

use anyhow::Result;
use stratum_ontology_core::{Membrana, Node, Ontology, TipoSenal};
use tracing::instrument;

/// Vapora-specific ontology accessor.
///
/// Wraps [`Ontology`] to provide query helpers specific to Vapora's axioms and
/// gate structure. The three NCL files (`core.ncl`, `state.ncl`, `gate.ncl`)
/// are expected at `{ontology_dir}/*.ncl`.
pub struct VaporaOntology {
    inner: Ontology,
    dir: PathBuf,
}

impl VaporaOntology {
    /// Load Vapora's ontology from the given directory.
    /// Expects `core.ncl`, `state.ncl`, and `gate.ncl` inside `dir`.
    pub fn load(dir: impl AsRef<Path>) -> Result<Self> {
        let dir = dir.as_ref().to_owned();
        let inner = Ontology::load(&dir)?;
        Ok(Self { inner, dir })
    }

    /// Reload all three NCL files from disk without recreating the instance.
    pub fn reload(&mut self) -> Result<()> {
        self.inner.reload(&self.dir)
    }

    /// Access the raw [`Ontology`] for advanced queries.
    pub fn raw(&self) -> &Ontology {
        &self.inner
    }

    /// Returns all invariant axiom nodes — the architectural invariants that
    /// must never be violated in Vapora's design.
    pub fn invariant_axioms(&self) -> Vec<&Node> {
        self.inner.core.invariants().collect()
    }

    /// Returns the node IDs of all invariant axioms.
    pub fn invariant_axiom_ids(&self) -> Vec<&str> {
        self.invariant_axioms()
            .into_iter()
            .map(|n| n.id.as_str())
            .collect()
    }

    /// Check whether a proposed signal passes through any active gate membrane.
    ///
    /// Returns the list of active membranas that accept the signal.
    #[instrument(skip(self), fields(signal = ?signal))]
    pub fn check_signal(&self, signal: &TipoSenal) -> Vec<&Membrana> {
        self.inner
            .gate
            .active_membranas()
            .filter(|m| m.acepta.contains(signal))
            .collect()
    }

    /// Returns `true` if the signal is accepted by at least one active membrane.
    pub fn signal_passes_gate(&self, signal: &TipoSenal) -> bool {
        !self.check_signal(signal).is_empty()
    }

    /// Returns all active membranas protecting the given node ID.
    pub fn membranas_protecting(&self, node_id: &str) -> Vec<&Membrana> {
        self.inner.gate.protecting(node_id).collect()
    }

    /// Returns dimension IDs where `estado_actual != estado_deseado` —
    /// dimensions still in transition toward their desired state.
    pub fn open_dimensions(&self) -> Vec<String> {
        self.inner
            .state
            .dimensions()
            .iter()
            .filter(|d| d.estado_actual != d.estado_deseado)
            .map(|d| d.id.clone())
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    fn ontology_dir() -> PathBuf {
        let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let crates_dir = manifest
            .parent()
            .expect("CARGO_MANIFEST_DIR has a parent (crates/)");
        let project_dir = crates_dir
            .parent()
            .expect("crates/ has a parent (project root)");
        project_dir.join(".ontology")
    }

    #[test]
    fn vapora_ontology_loads_invariants() {
        let dir = ontology_dir();
        if !dir.exists() {
            return;
        }
        let ontology = VaporaOntology::load(&dir).expect("load vapora ontology");
        let axioms = ontology.invariant_axiom_ids();
        assert!(
            axioms.contains(&"async-first"),
            "async-first axiom must be present"
        );
        assert!(
            axioms.contains(&"budget-boundary"),
            "budget-boundary axiom must be present"
        );
        assert!(
            axioms.contains(&"provider-abstraction"),
            "provider-abstraction axiom must be present"
        );
    }

    #[test]
    fn cost_dimension_is_open() {
        let dir = ontology_dir();
        if !dir.exists() {
            return;
        }
        let ontology = VaporaOntology::load(&dir).expect("load vapora ontology");
        let open = ontology.open_dimensions();
        assert!(open.contains(&"cost".to_string()), "cost dimension should be open");
    }
}
