//! Tera-based ontology template rendering.
//!
//! Templates are loaded from a directory (default: `ontology_templates/` in the
//! crate root, overridable via `PROVISIONING_ONTOLOGY_TEMPLATES`) and
//! re-parsed on demand. Context data is built from the live Registry and
//! DomainState.

use anyhow::{Context as _, Result};
use provisioning_core::Registry;
use serde::Serialize;
use serde_json::json;
use std::path::Path;
use tera::{Context, Tera};

use crate::domain_state::DomainState;

// ── Context builders ──────────────────────────────────────────────────────────

#[derive(Serialize)]
struct ToolView {
    name: &'static str,
    description: &'static str,
    category: String,
}

/// Build a Tera context populated with live registry + domain state snapshots.
pub fn build_context(project: &str, registry: &Registry, state: &DomainState) -> Context {
    let tools: Vec<ToolView> = registry
        .list()
        .into_iter()
        .map(|m| ToolView {
            name: m.name,
            description: m.description,
            category: format!("{:?}", m.category),
        })
        .collect();

    let stats = json!({
        "total_invocations": state.total_invocations(),
        "active_sessions":   state.sessions().len(),
        "secs_since_config_reload": state.secs_since_reload(),
    });

    let mut ctx = Context::new();
    ctx.insert("project",      project);
    ctx.insert("generated_at", &chrono::Utc::now().to_rfc3339());
    ctx.insert("tools",        &tools);
    ctx.insert("stats",        &stats);
    ctx
}

// ── Renderer ──────────────────────────────────────────────────────────────────

pub struct OntologyRenderer {
    tera: Tera,
}

impl OntologyRenderer {
    /// Build a renderer from `dir/*.tera`. Returns a parse error if any
    /// template fails to compile.
    pub fn from_dir(dir: &Path) -> Result<Self> {
        let pattern = format!("{}/**/*.tera", dir.display());
        let tera = Tera::new(&pattern)
            .with_context(|| format!("tera parse {}", dir.display()))?;
        Ok(Self { tera })
    }

    pub fn template_names(&self) -> Vec<String> {
        self.tera.get_template_names().map(|s| s.to_owned()).collect()
    }

    pub fn render(&self, template: &str, ctx: &Context) -> Result<String> {
        self.tera
            .render(template, ctx)
            .with_context(|| format!("render {template}"))
    }
}

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

    #[test]
    fn from_dir_parses_templates() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("hello.md.tera"), "Hi {{ name }}!").unwrap();
        let r = OntologyRenderer::from_dir(dir.path()).unwrap();
        assert!(r.template_names().iter().any(|n| n == "hello.md.tera"));
    }

    #[test]
    fn render_substitutes_variables() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("t.tera"), "value: {{ v }}").unwrap();
        let r = OntologyRenderer::from_dir(dir.path()).unwrap();
        let mut ctx = Context::new();
        ctx.insert("v", &42);
        assert_eq!(r.render("t.tera", &ctx).unwrap(), "value: 42");
    }

    #[test]
    fn unknown_template_errors() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("t.tera"), "x").unwrap();
        let r = OntologyRenderer::from_dir(dir.path()).unwrap();
        assert!(r.render("no_such.tera", &Context::new()).is_err());
    }

    #[test]
    fn malformed_template_fails_at_parse() {
        let dir = TempDir::new().unwrap();
        std::fs::write(dir.path().join("bad.tera"), "{% if %}broken").unwrap();
        assert!(OntologyRenderer::from_dir(dir.path()).is_err());
    }
}
