use crate::models::{DetectedCapability, FileEntry, Receipt};
use anyhow::Result;

/// Bare crate directory name from a scanned path (`…/crates/<name>/…`).
fn crate_of(path: &str) -> Option<&str> {
    let start = path.find("crates/")? + "crates/".len();
    path[start..].split('/').next().filter(|s| !s.is_empty())
}

/// Generate the per-crate audit dashboard from real scan data.
///
/// Crate-centric view of capability health: for each crate, its dominant
/// classification (the audit "status": LIVE / DORMANT / …) plus file and
/// capability-evidence counts. This is what a workspace `AUDIT_DASHBOARD.md`
/// tracks — now generated rather than hand-maintained.
pub fn generate_audit_dashboard(
    files: &[FileEntry], caps: &[DetectedCapability], out: &std::path::Path,
) -> Result<()> {
    use std::collections::BTreeMap;
    std::fs::create_dir_all(out.join("reports"))?;

    let mut files_by_crate: BTreeMap<&str, usize> = BTreeMap::new();
    for f in files {
        if let Some(c) = crate_of(&f.path) {
            *files_by_crate.entry(c).or_default() += 1;
        }
    }
    let mut evid_by_crate: BTreeMap<&str, usize> = BTreeMap::new();
    let mut class_by_crate: BTreeMap<&str, BTreeMap<&str, usize>> = BTreeMap::new();
    for c in caps {
        if let Some(cr) = crate_of(&c.file_path) {
            *evid_by_crate.entry(cr).or_default() += 1;
            *class_by_crate
                .entry(cr)
                .or_default()
                .entry(c.classification.as_str())
                .or_default() += 1;
        }
    }

    let mut md = String::from("# Audit Dashboard\n\n");
    md.push_str("_Per-crate capability health, generated by cpmp from a real scan._\n\n");
    md.push_str(&format!("Crates: **{}**\n\n", files_by_crate.len()));
    md.push_str("| Crate | Status | Files | Capability evidences |\n|---|---|---:|---:|\n");
    for (cr, fcount) in &files_by_crate {
        // Status priority: a crate the build excludes is DORMANT; any crate with
        // real live code is LIVE (even if most evidence is in its tests); only a
        // crate that is *purely* test/doc/etc. takes that plurality label.
        let status = class_by_crate
            .get(cr)
            .map(|m| {
                let total: usize = m.values().sum();
                let dormant = m.get("DORMANT").copied().unwrap_or(0);
                if total > 0 && dormant * 2 >= total {
                    "DORMANT"
                } else if m.get("LIVE").copied().unwrap_or(0) > 0 {
                    "LIVE"
                } else {
                    m.iter()
                        .max_by_key(|(_, n)| **n)
                        .map(|(k, _)| *k)
                        .unwrap_or("—")
                }
            })
            .unwrap_or("—");
        let evid = evid_by_crate.get(cr).copied().unwrap_or(0);
        md.push_str(&format!(
            "| {} | {} | {} | {} |\n",
            cr, status, fcount, evid
        ));
    }

    std::fs::write(out.join("reports/AUDIT_DASHBOARD.md"), md)?;
    Ok(())
}

/// Generate the human-readable capability inventory from real scan data.
///
/// The previous version wrote static placeholder text; this projects the actual
/// detected capabilities, grouped and broken down by classification (`LIVE` /
/// `DORMANT` / `BROKEN_BUT_REAL` / `DOC_ONLY` / …) — the raw material for a
/// workspace audit dashboard.
pub fn generate_reports(
    files: &[FileEntry], caps: &[DetectedCapability], out: &std::path::Path,
) -> Result<()> {
    use std::collections::{BTreeMap, BTreeSet};
    std::fs::create_dir_all(out.join("reports"))?;

    // Distinct capabilities → their evidences.
    let mut by_cap: BTreeMap<&str, Vec<&DetectedCapability>> = BTreeMap::new();
    for c in caps {
        by_cap.entry(c.capability.as_str()).or_default().push(c);
    }
    // Classification totals across all evidences.
    let mut by_class: BTreeMap<&str, usize> = BTreeMap::new();
    for c in caps {
        *by_class.entry(c.classification.as_str()).or_default() += 1;
    }

    let mut md = String::new();
    md.push_str("# Capability Inventory\n\n");
    md.push_str("_Generated by cpmp from a real scan._\n\n");
    md.push_str(&format!(
        "- Files scanned: **{}**\n- Capability evidences: **{}**\n- Distinct capabilities: **{}**\n\n",
        files.len(),
        caps.len(),
        by_cap.len()
    ));

    md.push_str("## Classification breakdown\n\n| Classification | Evidences |\n|---|---:|\n");
    for (class, n) in &by_class {
        md.push_str(&format!("| {} | {} |\n", class, n));
    }
    md.push('\n');

    md.push_str("## Capabilities\n\n| Capability | Evidences | Files |\n|---|---:|---:|\n");
    for (cap, evs) in &by_cap {
        let cap_files: BTreeSet<&str> = evs.iter().map(|c| c.file_path.as_str()).collect();
        md.push_str(&format!(
            "| {} | {} | {} |\n",
            cap,
            evs.len(),
            cap_files.len()
        ));
    }

    std::fs::write(out.join("reports/CAPABILITY_INVENTORY.md"), md)?;
    Ok(())
}

pub fn generate_rdf_fallback(
    files: &[FileEntry], caps: &[DetectedCapability], receipt: &Receipt, out: &std::path::Path,
) -> Result<()> {
    std::fs::create_dir_all(out.join("catalog"))?;

    let mut ttl = String::from("@prefix prov: <http://www.w3.org/ns/prov#> .\n@prefix dcat: <http://www.w3.org/ns/dcat#> .\n@prefix spdx: <http://spdx.org/rdf/terms#> .\n@prefix skos: <http://www.w3.org/2004/02/skos/core#> .\n@prefix doap: <http://usefulinc.com/ns/doap#> .\n@prefix cpmp: <urn:cpmp:> .\n\n");

    ttl.push_str(&format!("<urn:cpmp:scan:{}> a prov:Activity ;\n  prov:startedAtTime \"{}\"^^<http://www.w3.org/2001/XMLSchema#dateTime> .\n\n", receipt.id, receipt.timestamp));
    ttl.push_str("<urn:cpmp:project:local> a doap:Project .\n\n");

    for f in files {
        ttl.push_str(&format!(
            "<file://{}> a spdx:File ;\n  prov:wasGeneratedBy <urn:cpmp:scan:{}> .\n\n",
            f.path, receipt.id
        ));
    }

    for c in caps {
        ttl.push_str(&format!(
            "<urn:cpmp:capability:{}> a skos:Concept .\n",
            c.capability.replace(' ', "_")
        ));
        ttl.push_str(&format!(
            "<file://{}> cpmp:implementsCapability <urn:cpmp:capability:{}> .\n\n",
            c.file_path,
            c.capability.replace(' ', "_")
        ));
    }

    std::fs::write(out.join("catalog/cpmp-catalog.ttl"), ttl)?;
    std::fs::write(
        out.join("catalog/cpmp-shapes.ttl"),
        "# SHACL Shapes for CPMP\n",
    )?;
    std::fs::write(
        out.join("catalog/cpmp-catalog.nq"),
        "# N-Quads representation\n",
    )?;

    Ok(())
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;
    use crate::models::Language;
    use std::time::SystemTime;

    fn file(path: &str) -> FileEntry {
        FileEntry {
            path: path.to_string(),
            language: Language::Rust,
            size_bytes: 0,
            hash: String::new(),
            modified_time: SystemTime::UNIX_EPOCH,
            git_root: None,
            is_test: false,
            is_binary: false,
        }
    }
    fn cap(file_path: &str, capability: &str, classification: &str) -> DetectedCapability {
        DetectedCapability {
            file_path: file_path.to_string(),
            capability: capability.to_string(),
            matched_term: capability.to_string(),
            name: capability.to_string(),
            confidence: 1.0,
            evidence_type: "CODE".to_string(),
            classification: classification.to_string(),
            line_number: 1,
        }
    }
    fn temp_out() -> std::path::PathBuf {
        let p = std::env::temp_dir().join(format!("cpmp_proj_{}", uuid::Uuid::new_v4()));
        std::fs::create_dir_all(&p).unwrap();
        p
    }

    #[test]
    fn crate_of_extracts_crate_name() {
        assert_eq!(crate_of("crates/ggen-core/src/lib.rs"), Some("ggen-core"));
        assert_eq!(crate_of("/home/u/ggen/crates/cpmp/src/x.rs"), Some("cpmp"));
        assert_eq!(crate_of("src/main.rs"), None);
    }

    #[test]
    fn inventory_has_real_counts_and_breakdown() {
        let out = temp_out();
        let files = vec![file("crates/a/src/x.rs"), file("crates/a/src/y.rs")];
        let caps = vec![
            cap("crates/a/src/x.rs", "OCEL", "LIVE"),
            cap("crates/a/src/y.rs", "OCEL", "LIVE"),
            cap("crates/a/src/y.rs", "SHACL", "DORMANT"),
        ];
        generate_reports(&files, &caps, &out).unwrap();
        let md = std::fs::read_to_string(out.join("reports/CAPABILITY_INVENTORY.md")).unwrap();
        assert!(md.contains("Capability evidences: **3**"), "{md}");
        assert!(md.contains("Distinct capabilities: **2**"), "{md}");
        assert!(md.contains("| LIVE | 2 |"), "{md}");
        assert!(md.contains("| DORMANT | 1 |"), "{md}");
        assert!(md.contains("| OCEL | 2 | 2 |"), "{md}"); // 2 evidences, 2 files
        let _ = std::fs::remove_dir_all(&out);
    }

    #[test]
    fn dashboard_status_prioritizes_dormant_then_live() {
        let out = temp_out();
        let files = vec![file("crates/live/src/x.rs"), file("crates/dead/src/y.rs")];
        let caps = vec![
            // `live` has real LIVE code plus a test-evidence plurality — still LIVE.
            cap("crates/live/src/x.rs", "A", "LIVE"),
            cap("crates/live/src/x.rs", "B", "TEST_ONLY"),
            cap("crates/live/src/x.rs", "C", "TEST_ONLY"),
            // `dead` is wholly dormant.
            cap("crates/dead/src/y.rs", "A", "DORMANT"),
            cap("crates/dead/src/y.rs", "B", "DORMANT"),
        ];
        generate_audit_dashboard(&files, &caps, &out).unwrap();
        let md = std::fs::read_to_string(out.join("reports/AUDIT_DASHBOARD.md")).unwrap();
        assert!(md.contains("| live | LIVE |"), "live code → LIVE: {md}");
        assert!(
            md.contains("| dead | DORMANT |"),
            "all dormant → DORMANT: {md}"
        );
        let _ = std::fs::remove_dir_all(&out);
    }
}
