//! Local registry mapping `OntologyId → local_path + classification`.
//!
//! Phase 1 (D28) persists this as a JSON document inside
//! `.ontoref/ontology_registry.ncl`. Real NCL contract enforcement
//! arrives once the catalog/ schema for the registry is wired in;
//! parsing the registry by hand keeps O1.8 self-contained.

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

use serde::{Deserialize, Serialize};

use crate::classification::OntologyType;
use crate::ontology_id::OntologyId;
use crate::{Error, Result};

/// One registry entry — an ontology this actor knows about locally.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryEntry {
    /// Content address of the ontology.
    pub ontology_id: OntologyId,
    /// Path to the oplog database file for this ontology on the local
    /// filesystem.
    pub oplog_path: PathBuf,
    /// Classification of the ontology (governs sharing rules).
    pub classification: OntologyType,
    /// Most recent state root verified by this actor for this ontology.
    /// `None` when the ontology has been registered but its current
    /// state has not yet been confirmed.
    pub verified_state_root_hex: Option<String>,
}

/// In-memory snapshot of the local ontology registry.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct OntologyRegistry {
    /// All known ontologies, in insertion order.
    pub entries: Vec<RegistryEntry>,
}

impl OntologyRegistry {
    /// Open an empty registry — useful for tests and for actors that
    /// have never registered an ontology.
    pub fn empty() -> Self {
        Self::default()
    }

    /// Load the registry from the JSON document at `path`. A missing
    /// file is treated as an empty registry — this matches the
    /// expected on-disk state of a fresh actor.
    ///
    /// # Errors
    ///
    /// - [`Error::Io`] when the file exists but cannot be read.
    /// - [`Error::RegistryParse`] when the file exists but does not parse into
    ///   the registry schema.
    pub fn load(path: &Path) -> Result<Self> {
        match std::fs::read(path) {
            Ok(bytes) => {
                let reg =
                    serde_json::from_slice::<Self>(&bytes).map_err(|e| Error::RegistryParse {
                        path: path.display().to_string(),
                        message: e.to_string(),
                    })?;
                Ok(reg)
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
            Err(e) => Err(Error::Io {
                context: format!("reading {}", path.display()),
                source: e,
            }),
        }
    }

    /// Persist the registry to `path` as pretty-printed JSON. The parent
    /// directory is created on demand.
    ///
    /// # Errors
    ///
    /// IO error on directory creation or write.
    pub fn save(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).map_err(|e| Error::Io {
                context: format!("creating parent of {}", path.display()),
                source: e,
            })?;
        }
        let json = serde_json::to_string_pretty(self).map_err(|e| Error::RegistryParse {
            path: path.display().to_string(),
            message: e.to_string(),
        })?;
        std::fs::write(path, json).map_err(|e| Error::Io {
            context: format!("writing {}", path.display()),
            source: e,
        })
    }

    /// Look up an entry by ontology id.
    pub fn lookup(&self, id: &OntologyId) -> Option<&RegistryEntry> {
        self.entries.iter().find(|e| &e.ontology_id == id)
    }

    /// Register a new entry, replacing any existing one with the same
    /// ontology id.
    pub fn register(&mut self, entry: RegistryEntry) {
        if let Some(existing) = self
            .entries
            .iter_mut()
            .find(|e| e.ontology_id == entry.ontology_id)
        {
            *existing = entry;
        } else {
            self.entries.push(entry);
        }
    }

    /// Remove an entry by ontology id, returning the removed entry if
    /// it existed.
    pub fn unregister(&mut self, id: &OntologyId) -> Option<RegistryEntry> {
        let idx = self.entries.iter().position(|e| &e.ontology_id == id)?;
        Some(self.entries.remove(idx))
    }
}

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

    fn sample_id(seed: u8) -> OntologyId {
        OntologyId([seed; 32])
    }

    fn sample_entry(seed: u8, classification: OntologyType) -> RegistryEntry {
        RegistryEntry {
            ontology_id: sample_id(seed),
            oplog_path: PathBuf::from(format!("/tmp/ontology-{seed}.oplog")),
            classification,
            verified_state_root_hex: None,
        }
    }

    #[test]
    fn register_and_lookup() {
        let mut reg = OntologyRegistry::empty();
        let entry = sample_entry(1, OntologyType::Type1Protocol);
        let id = entry.ontology_id;
        reg.register(entry);
        assert!(reg.lookup(&id).is_some());
        assert!(reg.lookup(&sample_id(2)).is_none());
    }

    #[test]
    fn register_replaces_existing() {
        let mut reg = OntologyRegistry::empty();
        reg.register(sample_entry(1, OntologyType::Type1Protocol));
        reg.register(sample_entry(1, OntologyType::Type3ProjectSpecific));
        assert_eq!(reg.entries.len(), 1);
        assert_eq!(
            reg.lookup(&sample_id(1)).unwrap().classification,
            OntologyType::Type3ProjectSpecific
        );
    }

    #[test]
    fn unregister_returns_entry() {
        let mut reg = OntologyRegistry::empty();
        reg.register(sample_entry(1, OntologyType::Type1Protocol));
        let removed = reg.unregister(&sample_id(1)).expect("entry was registered");
        assert_eq!(removed.ontology_id, sample_id(1));
        assert!(reg.entries.is_empty());
    }

    #[test]
    fn save_and_load_roundtrip() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let path = tmp.path().join("registry.json");

        let mut reg = OntologyRegistry::empty();
        reg.register(sample_entry(1, OntologyType::Type1Protocol));
        reg.register(sample_entry(2, OntologyType::Type3ProjectSpecific));
        reg.save(&path).expect("save ok");

        let loaded = OntologyRegistry::load(&path).expect("load ok");
        assert_eq!(loaded.entries.len(), 2);
        assert_eq!(
            loaded.entries[0].classification,
            OntologyType::Type1Protocol
        );
        assert_eq!(
            loaded.entries[1].classification,
            OntologyType::Type3ProjectSpecific
        );
    }

    #[test]
    fn load_missing_file_returns_empty() {
        let tmp = tempfile::tempdir().expect("tempdir");
        let path = tmp.path().join("never_created.json");
        let reg = OntologyRegistry::load(&path).expect("missing is empty");
        assert!(reg.entries.is_empty());
    }
}
