//! Multi-consumer config coherence verification.
//!
//! For each section in a project's `config_surface`, this module compares the
//! fields present in the exported NCL JSON against the fields declared by each
//! consumer. A field absent from all consumers is "unclaimed" — it exists in
//! the config but nothing reads it.
//!
//! Coherence is checked from two directions:
//! - **NCL-only fields**: present in NCL, not claimed by any consumer.
//! - **Consumer-only fields**: a consumer declares a field that doesn't exist
//!   in the NCL export. The consumer either references a renamed/removed field
//!   or the NCL contract is incomplete.

use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;

use serde::{Deserialize, Serialize};
use tracing::warn;

use crate::cache::NclCache;
use crate::registry::{ConfigSection, ConfigSurface};

/// Merge optional NCL `_meta_*` record fields into a quickref section object.
/// Only sets `rationale` when the section object currently has an empty value.
fn merge_meta_into_section(
    obj: &mut serde_json::Map<String, serde_json::Value>,
    meta_val: &serde_json::Value,
) {
    if let Some(rationale) = meta_val.get("rationale").and_then(|v| v.as_str()) {
        if obj["rationale"].as_str().unwrap_or("").is_empty() {
            obj["rationale"] = serde_json::Value::String(rationale.to_owned());
        }
    }
    if let Some(alt) = meta_val.get("alternatives_rejected") {
        obj.insert("alternatives_rejected".to_owned(), alt.clone());
    }
    if let Some(constraints) = meta_val.get("constraints") {
        obj.insert("constraints".to_owned(), constraints.clone());
    }
    if let Some(see_also) = meta_val.get("see_also") {
        obj.insert("see_also".to_owned(), see_also.clone());
    }
}

/// Status of a section's coherence check.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CoherenceStatus {
    /// All fields are claimed by at least one consumer, and no consumer
    /// references a field absent from the NCL export.
    Ok,
    /// Some fields are unclaimed or a consumer references missing fields —
    /// worth reviewing but not necessarily a bug.
    Warning,
    /// The NCL export failed; coherence could not be checked.
    Error,
}

/// Per-consumer coherence result within a section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsumerCoherenceReport {
    pub consumer_id: String,
    pub kind: String,
    /// Fields the consumer declared but are absent in the NCL export.
    pub missing_in_ncl: Vec<String>,
    /// Fields in the NCL export that this consumer doesn't declare.
    /// Non-empty when the consumer has an explicit field list (not "reads
    /// all").
    pub extra_in_ncl: Vec<String>,
}

/// Full coherence report for one config section.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SectionCoherenceReport {
    pub section_id: String,
    /// All top-level keys in the NCL export for this section (excluding
    /// `_meta_*` and `_overrides_meta` keys).
    pub ncl_fields: Vec<String>,
    pub consumers: Vec<ConsumerCoherenceReport>,
    /// Fields present in NCL but claimed by no consumer.
    pub unclaimed_fields: Vec<String>,
    pub status: CoherenceStatus,
}

/// Coherence report for an entire project.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectCoherenceReport {
    pub project_slug: String,
    pub sections: Vec<SectionCoherenceReport>,
    pub has_config_surface: bool,
}

impl ProjectCoherenceReport {
    /// Overall status: worst status across all sections.
    pub fn overall_status(&self) -> CoherenceStatus {
        if self
            .sections
            .iter()
            .any(|s| s.status == CoherenceStatus::Error)
        {
            CoherenceStatus::Error
        } else if self
            .sections
            .iter()
            .any(|s| s.status == CoherenceStatus::Warning)
        {
            CoherenceStatus::Warning
        } else {
            CoherenceStatus::Ok
        }
    }
}

/// Run coherence check for all sections of a project's config surface.
///
/// `section_filter` — if `Some`, only check this section id.
pub async fn check_project(
    slug: &str,
    surface: &ConfigSurface,
    project_root: &Path,
    cache: &NclCache,
    import_path: Option<&str>,
    section_filter: Option<&str>,
) -> ProjectCoherenceReport {
    let sections_to_check: Vec<&ConfigSection> = surface
        .sections
        .iter()
        .filter(|s| section_filter.is_none_or(|f| s.id == f))
        .collect();

    let mut section_reports = Vec::with_capacity(sections_to_check.len());

    for section in sections_to_check {
        let report = check_section(section, surface, project_root, cache, import_path).await;
        section_reports.push(report);
    }

    ProjectCoherenceReport {
        project_slug: slug.to_owned(),
        sections: section_reports,
        has_config_surface: true,
    }
}

async fn check_section(
    section: &ConfigSection,
    surface: &ConfigSurface,
    project_root: &Path,
    cache: &NclCache,
    import_path: Option<&str>,
) -> SectionCoherenceReport {
    let ncl_path = project_root.join(&surface.config_root).join(&section.file);

    let ncl_export = cache.export(&ncl_path, import_path).await;
    let (json, _) = match ncl_export {
        Ok(pair) => pair,
        Err(e) => {
            warn!(
                section = %section.id,
                path = %ncl_path.display(),
                error = %e,
                "config coherence: nickel export failed"
            );
            return SectionCoherenceReport {
                section_id: section.id.clone(),
                ncl_fields: vec![],
                consumers: vec![],
                unclaimed_fields: vec![],
                status: CoherenceStatus::Error,
            };
        }
    };

    // Collect top-level keys for this section from the export.
    // A section NCL file may export { server = { ... }, _meta_server = {...} }
    // We want the section key matching section.id; if the whole file is the
    // section value, use all keys.
    let ncl_fields: BTreeSet<String> = extract_section_fields(&json, &section.id);

    // Build consumer reports.
    let mut all_claimed: BTreeSet<String> = BTreeSet::new();
    let mut consumer_reports = Vec::with_capacity(section.consumers.len());

    for consumer in &section.consumers {
        let consumer_fields: BTreeSet<String> = if consumer.fields.is_empty() {
            // Empty field list means the consumer claims all NCL fields.
            all_claimed.extend(ncl_fields.iter().cloned());
            consumer_reports.push(ConsumerCoherenceReport {
                consumer_id: consumer.id.clone(),
                kind: format!("{:?}", consumer.kind),
                missing_in_ncl: vec![],
                extra_in_ncl: vec![],
            });
            continue;
        } else {
            consumer.fields.iter().cloned().collect()
        };

        all_claimed.extend(consumer_fields.iter().cloned());

        let missing_in_ncl: Vec<String> =
            consumer_fields.difference(&ncl_fields).cloned().collect();
        let extra_in_ncl: Vec<String> = ncl_fields.difference(&consumer_fields).cloned().collect();

        consumer_reports.push(ConsumerCoherenceReport {
            consumer_id: consumer.id.clone(),
            kind: format!("{:?}", consumer.kind),
            missing_in_ncl,
            extra_in_ncl,
        });
    }

    let unclaimed_fields: Vec<String> = ncl_fields.difference(&all_claimed).cloned().collect();

    let has_missing = consumer_reports
        .iter()
        .any(|c| !c.missing_in_ncl.is_empty());
    let status = if !unclaimed_fields.is_empty() || has_missing {
        CoherenceStatus::Warning
    } else {
        CoherenceStatus::Ok
    };

    SectionCoherenceReport {
        section_id: section.id.clone(),
        ncl_fields: ncl_fields.into_iter().collect(),
        consumers: consumer_reports,
        unclaimed_fields,
        status,
    }
}

/// Extract the field names for a section from the NCL export JSON.
///
/// If the JSON has a top-level key matching `section_id`, returns the keys of
/// that sub-object. Otherwise treats the entire top-level object as the section
/// fields. Strips `_meta_*` and `_overrides_meta` keys from the result.
fn extract_section_fields(json: &serde_json::Value, section_id: &str) -> BTreeSet<String> {
    let obj = if let Some(sub) = json.get(section_id).and_then(|v| v.as_object()) {
        sub.keys().cloned().collect()
    } else if let Some(top) = json.as_object() {
        top.keys()
            .filter(|k| !k.starts_with("_meta_") && *k != "_overrides_meta")
            .cloned()
            .collect()
    } else {
        BTreeSet::new()
    };
    obj
}

/// Generate a quickref document for a project's config surface.
///
/// Combines: NCL export values + manifest metadata (descriptions, rationales,
/// consumers) + override history from `_overrides_meta` + coherence status.
pub async fn build_quickref(
    slug: &str,
    surface: &ConfigSurface,
    project_root: &Path,
    cache: &NclCache,
    import_path: Option<&str>,
    section_filter: Option<&str>,
) -> serde_json::Value {
    let entry_point = surface.entry_point_path(project_root);
    let full_export = cache.export(&entry_point, import_path).await.ok();

    let coherence = check_project(
        slug,
        surface,
        project_root,
        cache,
        import_path,
        section_filter,
    )
    .await;

    let coherence_by_id: BTreeMap<String, &SectionCoherenceReport> = coherence
        .sections
        .iter()
        .map(|s| (s.section_id.clone(), s))
        .collect();

    let sections: Vec<serde_json::Value> = surface
        .sections
        .iter()
        .filter(|s| section_filter.is_none_or(|f| s.id == f))
        .map(|section| {
            let current_values = full_export
                .as_ref()
                .and_then(|(json, _)| json.get(&section.id))
                .cloned()
                .unwrap_or(serde_json::Value::Null);

            // Extract _meta_{section} from the section's own NCL file.
            let meta_key = format!("_meta_{}", section.id);
            let section_ncl_path =
                project_root.join(&surface.config_root).join(&section.file);
            let meta = tokio::task::block_in_place(|| {
                // Use a sync export via the cached path — avoids async recursion.
                std::process::Command::new("nickel")
                    .args(["export", "--format", "json"])
                    .arg(&section_ncl_path)
                    .current_dir(project_root)
                    .output()
                    .ok()
                    .and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())
                    .and_then(|j| j.get(&meta_key).cloned())
            });

            // Extract override history from _overrides_meta if present.
            let overrides = current_values
                .as_object()
                .and(full_export.as_ref())
                .and_then(|(j, _)| {
                    j.get("_overrides_meta")
                        .and_then(|m| m.get("entries"))
                        .cloned()
                })
                .unwrap_or(serde_json::Value::Array(vec![]));

            let coh = coherence_by_id.get(&section.id);
            let coherence_summary = serde_json::json!({
                "unclaimed_fields": coh.map(|c| c.unclaimed_fields.as_slice()).unwrap_or(&[]),
                "status": coh.map(|c| format!("{:?}", c.status)).unwrap_or_else(|| "unknown".into()),
            });

            let consumers: Vec<serde_json::Value> = section
                .consumers
                .iter()
                .map(|c| {
                    serde_json::json!({
                        "id": c.id,
                        "kind": format!("{:?}", c.kind),
                        "ref": c.reference,
                        "fields": c.fields,
                    })
                })
                .collect();

            let mut s = serde_json::json!({
                "id": section.id,
                "file": section.file,
                "mutable": section.mutable,
                "description": section.description,
                "rationale": section.rationale,
                "contract": section.contract,
                "current_values": current_values,
                "overrides": overrides,
                "consumers": consumers,
                "coherence": coherence_summary,
            });

            if let Some(meta_val) = meta {
                if let Some(obj) = s.as_object_mut() {
                    merge_meta_into_section(obj, &meta_val);
                }
            }

            s
        })
        .collect();

    serde_json::json!({
        "project": slug,
        "generated_at": std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0),
        "config_root": surface.config_root.display().to_string(),
        "entry_point": surface.entry_point,
        "kind": format!("{:?}", surface.kind),
        "sections": sections,
        "overall_coherence": format!("{:?}", coherence.overall_status()),
    })
}
