//! Domain graph navigation — project ↔ domain bidirectional discovery.
//!
//! Detects domain implementations from:
//!   (a) `config.ncl`: `domain_<id> = true`
//!   (b) `manifest.ncl`: `domain_origin = { id = "...", path = "...", ... }`
//!   (c) `.{domain}.ontoref/` directory presence

use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::registry::ProjectRegistry;

/// How a domain was declared in a project.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DomainDeclKind {
    ConfigFlag,
    DomainOrigin,
    ImplDir,
}

/// A domain implementation declaration found in a project.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainDecl {
    pub id: String,
    pub kind: DomainDeclKind,
    /// Source path from `domain_origin`, empty otherwise.
    pub path: String,
}

/// A project that implements a given domain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainConsumer {
    pub slug: String,
    pub root: String,
    pub kind: DomainDeclKind,
}

/// Extract `domain_<id> = true` declarations from a file's text content.
fn extract_config_flags(content: &str) -> Vec<String> {
    content
        .lines()
        .filter_map(|line| {
            let trimmed = line.trim();
            // Match: domain_<alphanum_> = true
            let rest = trimmed.strip_prefix("domain_")?;
            let eq_pos = rest.find('=')?;
            let id = rest[..eq_pos].trim();
            let val = rest[eq_pos + 1..].trim();
            if val.starts_with("true")
                && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
                && !id.is_empty()
            {
                Some(id.to_string())
            } else {
                None
            }
        })
        .collect()
}

/// Extract `domain_origin.id` and `domain_origin.path` from manifest text.
fn extract_domain_origin(content: &str) -> Option<(String, String)> {
    if !content.contains("domain_origin") {
        return None;
    }
    // Find the domain_origin block start then scan for id and path fields.
    let block_start = content.find("domain_origin")?;
    let block = &content[block_start..];
    let open = block.find('{')? + 1;
    // Find matching closing brace.
    let mut depth = 1usize;
    let mut close = open;
    for (i, ch) in block[open..].char_indices() {
        match ch {
            '{' => depth += 1,
            '}' => {
                depth -= 1;
                if depth == 0 {
                    close = open + i;
                    break;
                }
            }
            _ => {}
        }
    }
    let inner = &block[open..close];

    let id = inner.lines().find_map(|l| {
        let t = l.trim();
        let rest = t.strip_prefix("id")?.trim_start_matches([' ', '=']).trim();
        rest.strip_prefix('"')?
            .split('"')
            .next()
            .map(|s| s.replace("-framework", "").replace("-domain", ""))
    })?;

    let path = inner
        .lines()
        .find_map(|l| {
            let t = l.trim();
            let rest = t
                .strip_prefix("path")?
                .trim_start_matches([' ', '='])
                .trim();
            rest.strip_prefix('"')?.split('"').next().map(String::from)
        })
        .unwrap_or_default();

    Some((id, path))
}

/// Read domain declarations for a project root (offline, fast grep).
pub fn project_domains(root: &Path) -> Vec<DomainDecl> {
    let mut domains: Vec<DomainDecl> = Vec::new();

    // (a) config.ncl: domain_<id> = true
    let config_path = root.join(".ontoref").join("config.ncl");
    if let Ok(content) = std::fs::read_to_string(&config_path) {
        for id in extract_config_flags(&content) {
            if !domains.iter().any(|d| d.id == id) {
                domains.push(DomainDecl {
                    id,
                    kind: DomainDeclKind::ConfigFlag,
                    path: String::new(),
                });
            }
        }
    }

    // (b) manifest.ncl: domain_origin block
    let manifest_path = root.join(".ontoref").join("ontology").join("manifest.ncl");
    if let Ok(content) = std::fs::read_to_string(&manifest_path) {
        if let Some((id, path)) = extract_domain_origin(&content) {
            if !domains.iter().any(|d| d.id == id) {
                domains.push(DomainDecl {
                    id,
                    kind: DomainDeclKind::DomainOrigin,
                    path,
                });
            }
        }
    }

    // (c) .{domain}.ontoref/ directory presence
    if let Ok(entries) = std::fs::read_dir(root) {
        for entry in entries.flatten() {
            let name = entry.file_name().to_string_lossy().into_owned();
            if name.starts_with('.')
                && name.ends_with(".ontoref")
                && name != ".ontoref"
                && entry.path().is_dir()
            {
                let id = name
                    .trim_start_matches('.')
                    .trim_end_matches(".ontoref")
                    .to_string();
                if !domains.iter().any(|d| d.id == id) {
                    domains.push(DomainDecl {
                        id,
                        kind: DomainDeclKind::ImplDir,
                        path: String::new(),
                    });
                }
            }
        }
    }

    domains
}

/// List all registered projects implementing a given domain.
pub fn consumers_of(registry: &ProjectRegistry, domain_id: &str) -> Vec<DomainConsumer> {
    registry
        .all()
        .into_iter()
        .filter_map(|ctx| {
            let domains = project_domains(&ctx.root);
            domains
                .into_iter()
                .find(|d| d.id == domain_id)
                .map(|d| DomainConsumer {
                    slug: ctx.slug.clone(),
                    root: ctx.root.to_string_lossy().into_owned(),
                    kind: d.kind,
                })
        })
        .collect()
}

/// Registered domain ids (directories under $ONTOREF_ROOT/domains/ with
/// domain.ncl).
pub fn registered_domain_ids(ontoref_root: &Path) -> Vec<String> {
    let domains_dir = ontoref_root.join("domains");
    std::fs::read_dir(&domains_dir)
        .into_iter()
        .flatten()
        .flatten()
        .filter(|e| e.path().is_dir() && e.path().join("domain.ncl").exists())
        .map(|e| e.file_name().to_string_lossy().into_owned())
        .collect()
}

/// Light metadata from domain.ncl — fast grep, no nickel export.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DomainMeta {
    pub id: String,
    pub name: String,
    pub description: String,
    pub schema_cmd: String,
}

fn extract_ncl_string_field(content: &str, field: &str) -> String {
    content
        .lines()
        .find_map(|l| {
            let t = l.trim();
            let rest = t.strip_prefix(field)?.trim_start_matches([' ', '=']).trim();
            rest.strip_prefix('"')?.split('"').next().map(String::from)
        })
        .unwrap_or_default()
}

pub fn domain_meta(ontoref_root: &Path, domain_id: &str) -> Option<DomainMeta> {
    let ncl = ontoref_root
        .join("domains")
        .join(domain_id)
        .join("domain.ncl");
    let content = std::fs::read_to_string(&ncl).ok()?;
    Some(DomainMeta {
        id: domain_id.to_string(),
        name: extract_ncl_string_field(&content, "name"),
        description: extract_ncl_string_field(&content, "description"),
        schema_cmd: extract_ncl_string_field(&content, "schema_cmd"),
    })
}

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

    #[test]
    fn extract_config_flags_finds_domain_declarations() {
        let content = "ops = { tier = 'Tier0 },\n  domain_provisioning = true,\n  domain_rustelo \
                       = true,\n  other = false,";
        let flags = extract_config_flags(content);
        assert!(flags.contains(&"provisioning".to_string()));
        assert!(flags.contains(&"rustelo".to_string()));
        assert_eq!(flags.len(), 2);
    }

    #[test]
    fn extract_domain_origin_parses_block() {
        let content = r#"
  domain_origin = {
    id   = "rustelo-framework",
    path = "/dev/rustelo",
  },
"#;
        let result = extract_domain_origin(content);
        assert!(result.is_some());
        let (id, path) = result.unwrap();
        assert_eq!(id, "rustelo");
        assert_eq!(path, "/dev/rustelo");
    }

    #[test]
    fn extract_domain_origin_strips_framework_suffix() {
        let content = r#"domain_origin = { id = "librosys-domain", path = "/dev/lb" },"#;
        let (id, _) = extract_domain_origin(content).unwrap();
        assert_eq!(id, "librosys");
    }
}
