//! Read-query functions for the constellation-memory surface (component J,
//! ADR-051 / ADR-053). Each function reads NCL files via the cache and returns
//! typed vectors — the same underlying data regardless of which surface calls
//! it (HTTP GET, GraphQL resolver, MCP tool). This is the G-4 equivalence
//! contract made concrete: one data path, four display surfaces.

use std::path::Path;
use std::sync::Arc;

use crate::cache::NclCache;
use crate::memory_types::{ChronicleEntry, Criterion, PanelSpec, RoadmapItem};

fn str_val<'a>(v: &'a serde_json::Value, key: &str) -> &'a str {
    v.get(key).and_then(|s| s.as_str()).unwrap_or("")
}

/// Chronicle entries woven from ADRs and protocol migrations.
///
/// Sources: `.ontoref/adrs/*.ncl` (Accepted status) +
/// `.ontoref/reflection/migrations/*.ncl`. Optional filters: `since` (ISO date
/// prefix), `kind` ("adr" | "migration"), `domain`. Result is sorted descending
/// by date.
pub async fn chronicle_entries(
    root: &Path,
    cache: &Arc<NclCache>,
    import_path: Option<&str>,
    since: Option<&str>,
    kind_filter: Option<&str>,
    domain_filter: Option<&str>,
) -> Vec<ChronicleEntry> {
    let mut entries = Vec::new();

    if !matches!(kind_filter, Some("migration")) {
        let adrs_dir = ontoref_ontology::layout::resolve_section(root, "adrs");
        if let Ok(rd) = std::fs::read_dir(&adrs_dir) {
            for entry in rd.flatten() {
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) != Some("ncl") {
                    continue;
                }
                let Ok((json, _)) = cache.export(&path, import_path).await else {
                    continue;
                };
                let status = str_val(&json, "status");
                if !status.eq_ignore_ascii_case("accepted") {
                    continue;
                }
                let id = str_val(&json, "id").to_owned();
                let date = str_val(&json, "date").to_owned();
                let title_raw = str_val(&json, "title");
                let title = if title_raw.is_empty() {
                    id.clone()
                } else {
                    title_raw.to_owned()
                };
                entries.push(ChronicleEntry {
                    id: format!("adr:{id}"),
                    date,
                    kind: "adr".to_owned(),
                    title,
                    summary: json
                        .get("context")
                        .and_then(|v| v.as_str())
                        .map(str::to_owned),
                    domain: None,
                    refs: if id.is_empty() { vec![] } else { vec![id] },
                });
            }
        }
    }

    if !matches!(kind_filter, Some("adr")) {
        let mig_dir = ontoref_ontology::layout::resolve_section(root, "reflection/migrations");
        if let Ok(rd) = std::fs::read_dir(&mig_dir) {
            for entry in rd.flatten() {
                let path = entry.path();
                if path.extension().and_then(|e| e.to_str()) != Some("ncl") {
                    continue;
                }
                let Ok((json, _)) = cache.export(&path, import_path).await else {
                    continue;
                };
                let id = str_val(&json, "id").to_owned();
                let slug = str_val(&json, "slug").to_owned();
                let title = if slug.is_empty() {
                    id.clone()
                } else {
                    format!("{id}-{slug}")
                };
                let summary = json
                    .get("description")
                    .and_then(|v| v.as_str())
                    .map(|s| s.lines().next().unwrap_or(s).to_owned());
                entries.push(ChronicleEntry {
                    id: format!("migration:{id}"),
                    date: String::new(),
                    kind: "migration".to_owned(),
                    title,
                    summary,
                    domain: None,
                    refs: vec![],
                });
            }
        }
    }

    entries.retain(|e| {
        if let Some(k) = kind_filter {
            if e.kind != k {
                return false;
            }
        }
        if let Some(d) = domain_filter {
            if e.domain.as_deref() != Some(d) {
                return false;
            }
        }
        if let Some(s) = since {
            if !e.date.is_empty() && e.date.as_str() < s {
                return false;
            }
        }
        true
    });

    entries.sort_by(|a, b| b.date.cmp(&a.date).then(a.id.cmp(&b.id)));
    entries
}

/// Roadmap items derived from backlog entries.
///
/// Source: `.ontoref/reflection/backlog.ncl`. Optional filters: `status`,
/// `horizon`.
pub async fn roadmap_items(
    root: &Path,
    cache: &Arc<NclCache>,
    import_path: Option<&str>,
    status_filter: Option<&str>,
    horizon_filter: Option<&str>,
) -> Vec<RoadmapItem> {
    let path = ontoref_ontology::layout::resolve_section(root, "reflection/backlog.ncl");
    if !path.exists() {
        return vec![];
    }
    let Ok((json, _)) = cache.export(&path, import_path).await else {
        return vec![];
    };
    let items = json
        .get("items")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default();

    items
        .iter()
        .filter_map(|item| {
            let id = str_val(item, "id");
            if id.is_empty() {
                return None;
            }
            let status = str_val(item, "status").to_owned();
            let horizon = str_val(item, "horizon").to_owned();
            if let Some(sf) = status_filter {
                if !status.eq_ignore_ascii_case(sf) {
                    return None;
                }
            }
            if let Some(hf) = horizon_filter {
                if !horizon.eq_ignore_ascii_case(hf) {
                    return None;
                }
            }
            Some(RoadmapItem {
                id: id.to_owned(),
                title: str_val(item, "title").to_owned(),
                status,
                horizon,
                graduates_to: item
                    .get("graduates_to")
                    .and_then(|v| v.as_str())
                    .map(str::to_owned),
                mechanism: item
                    .get("mechanism")
                    .and_then(|v| v.as_str())
                    .map(str::to_owned),
                target_date: item
                    .get("target_date")
                    .and_then(|v| v.as_str())
                    .map(str::to_owned),
            })
        })
        .collect()
}

/// Criteria derived from catalog validator declarations.
///
/// Source: `.ontoref/catalog/validators/*.ncl`. Optional filter: `kind`
/// ("normative"). All catalog-declared validators are normative (explicitly
/// promoted per ADR-050).
pub async fn criteria_list(
    root: &Path,
    cache: &Arc<NclCache>,
    import_path: Option<&str>,
    kind_filter: Option<&str>,
) -> Vec<Criterion> {
    if let Some(k) = kind_filter {
        if k != "normative" {
            return vec![];
        }
    }

    let validators_dir = ontoref_ontology::layout::resolve_section(root, "catalog/validators");
    let Ok(rd) = std::fs::read_dir(&validators_dir) else {
        return vec![];
    };

    let mut criteria = Vec::new();
    for entry in rd.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("ncl") {
            continue;
        }
        let stem = path
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("")
            .to_owned();
        if stem == "schema" {
            continue;
        }
        let Ok((json, _)) = cache.export(&path, import_path).await else {
            continue;
        };
        let id = str_val(&json, "id");
        if id.is_empty() {
            continue;
        }
        criteria.push(Criterion {
            id: id.to_owned(),
            description: str_val(&json, "description").to_owned(),
            kind: "normative".to_owned(),
            provenance: None,
            validator_ref: {
                let r = str_val(&json, "predicate_ref");
                if r.is_empty() {
                    None
                } else {
                    Some(r.to_owned())
                }
            },
            related_adr: {
                let r = str_val(&json, "criterion_ref");
                if r.is_empty() {
                    None
                } else {
                    Some(r.to_owned())
                }
            },
        });
    }

    criteria.sort_by(|a, b| a.id.cmp(&b.id));
    criteria
}

/// Panel specs resolved from dedicated panel-*.ncl files in the presentation
/// directory.
///
/// Source: `.ontoref/presentation/panel-*.ncl`. Optional filter: `kind`
/// (Timeline/Gantt/…).
pub async fn panels_list(
    root: &Path,
    cache: &Arc<NclCache>,
    import_path: Option<&str>,
    kind_filter: Option<&str>,
) -> Vec<PanelSpec> {
    let pres_dir = root.join(".ontoref").join("presentation");
    if !pres_dir.exists() {
        return vec![];
    }

    let Ok(rd) = std::fs::read_dir(&pres_dir) else {
        return vec![];
    };

    let mut panels = Vec::new();
    for entry in rd.flatten() {
        let path = entry.path();
        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
        if path.extension().and_then(|e| e.to_str()) != Some("ncl") || !stem.starts_with("panel-") {
            continue;
        }
        let Ok((json, _)) = cache.export(&path, import_path).await else {
            continue;
        };
        let id = str_val(&json, "id");
        if id.is_empty() {
            continue;
        }
        let kind = str_val(&json, "kind").to_owned();
        if let Some(kf) = kind_filter {
            if !kind.eq_ignore_ascii_case(kf) {
                continue;
            }
        }
        let data_source_op = json
            .get("data_source")
            .and_then(|ds| ds.get("op"))
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_owned();
        let surfaces = json
            .get("surfaces")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|s| s.as_str().map(str::to_owned))
                    .collect()
            })
            .unwrap_or_default();
        let actors = json
            .get("actors")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|s| s.as_str().map(str::to_owned))
                    .collect()
            })
            .unwrap_or_default();
        panels.push(PanelSpec {
            id: id.to_owned(),
            title: str_val(&json, "title").to_owned(),
            kind,
            data_source_op,
            surfaces,
            actors,
        });
    }

    panels.sort_by(|a, b| a.id.cmp(&b.id));
    panels
}
