//! Narrative explanation generation for project claims.
//!
//! Three distinct outputs:
//! - `generate_onboarding()` — lightweight summary for `aphoria explain`
//! - `generate_full_docs()` — comprehensive reference for `aphoria docs generate`
//! - `generate_explanation()` — legacy function (kept for backward compat, delegates to `generate_onboarding`)

use std::collections::BTreeMap;

use crate::coverage::CoverageReport;
use crate::types::authored_claim::{AuthoredClaim, ClaimStatus};
use crate::verify::{AuditVerdict, VerifyReport};

/// Generate an onboarding overview for `aphoria explain`.
///
/// Lightweight narrative: category counts, verification health, coverage snapshot.
/// Takes pre-computed data so the caller handles scanning.
pub fn generate_onboarding(
    claims: &[AuthoredClaim],
    verify_report: &VerifyReport,
    coverage_report: &CoverageReport,
    project_name: &str,
    format: &str,
) -> String {
    match format {
        "json" => generate_onboarding_json(claims, verify_report, coverage_report, project_name),
        _ => generate_onboarding_markdown(claims, verify_report, coverage_report, project_name),
    }
}

/// Generate comprehensive reference docs for `aphoria docs generate`.
///
/// Full claim details + verification results + coverage table.
/// Takes pre-computed data so the caller handles scanning.
pub fn generate_full_docs(
    claims: &[AuthoredClaim],
    verify_report: &VerifyReport,
    coverage_report: &CoverageReport,
    project_name: &str,
    format: &str,
) -> String {
    match format {
        "json" => generate_full_docs_json(claims, verify_report, coverage_report, project_name),
        _ => generate_full_docs_markdown(claims, verify_report, coverage_report, project_name),
    }
}

// --- Onboarding (aphoria explain) ---

fn generate_onboarding_markdown(
    claims: &[AuthoredClaim],
    verify_report: &VerifyReport,
    coverage_report: &CoverageReport,
    project_name: &str,
) -> String {
    let mut out = String::new();

    out.push_str(&format!("# {project_name} — Claim Overview\n\n"));

    // Category summary
    let categories = group_by_category(claims);
    let active_count = claims.iter().filter(|c| c.status == ClaimStatus::Active).count();

    out.push_str(&format!(
        "**{project_name}** has **{active_count}** active claims across **{}** categories.\n\n",
        categories.len()
    ));

    if !categories.is_empty() {
        out.push_str("## Categories\n\n");
        out.push_str("| Category | Active | Total |\n");
        out.push_str("|----------|--------|-------|\n");
        for (cat, cat_claims) in &categories {
            let active = cat_claims.iter().filter(|c| c.status == ClaimStatus::Active).count();
            out.push_str(&format!("| {} | {} | {} |\n", capitalize(cat), active, cat_claims.len()));
        }
        out.push('\n');
    }

    // Verification health
    let summary = &verify_report.summary;
    out.push_str("## Verification Health\n\n");
    out.push_str(&format!(
        "- **Pass:** {}\n- **Conflict:** {}\n- **Missing:** {}\n- **Unclaimed observations:** {}\n\n",
        summary.pass, summary.conflict, summary.missing, summary.unclaimed,
    ));

    if summary.conflict > 0 {
        out.push_str("*Conflicts indicate code behavior differs from authored claims.*\n\n");
    }

    // Coverage snapshot
    let cov = &coverage_report.summary;
    out.push_str("## Coverage Snapshot\n\n");
    out.push_str(&format!(
        "- **Claimed:** {:.1}% of {} observations\n",
        cov.claimed_percentage, cov.total_observations,
    ));
    out.push_str(&format!(
        "- **Modules with claims:** {} / {}\n",
        cov.modules_with_claims,
        cov.modules_with_claims + cov.modules_without_claims,
    ));

    // Top uncovered modules
    let uncovered: Vec<_> = coverage_report
        .modules
        .iter()
        .filter(|m| m.claim_count == 0 && m.observation_count > 0)
        .take(5)
        .collect();

    if !uncovered.is_empty() {
        out.push_str("\n**Top uncovered modules:**\n");
        for m in uncovered {
            out.push_str(&format!(
                "- `{}` ({} observations)\n",
                m.module_path, m.observation_count,
            ));
        }
    }

    out.push_str("\n---\n");
    out.push_str("Run `aphoria claims explain` for full claim details.\n");
    out.push_str("Run `aphoria docs generate` for comprehensive reference documentation.\n");

    out
}

fn generate_onboarding_json(
    claims: &[AuthoredClaim],
    verify_report: &VerifyReport,
    coverage_report: &CoverageReport,
    project_name: &str,
) -> String {
    let categories = group_by_category(claims);
    let active_count = claims.iter().filter(|c| c.status == ClaimStatus::Active).count();

    let cat_summary: Vec<serde_json::Value> = categories
        .iter()
        .map(|(cat, cat_claims)| {
            let active = cat_claims.iter().filter(|c| c.status == ClaimStatus::Active).count();
            serde_json::json!({
                "category": cat,
                "active": active,
                "total": cat_claims.len(),
            })
        })
        .collect();

    let json = serde_json::json!({
        "project": project_name,
        "type": "onboarding",
        "active_claims": active_count,
        "categories": cat_summary,
        "verification": {
            "pass": verify_report.summary.pass,
            "conflict": verify_report.summary.conflict,
            "missing": verify_report.summary.missing,
            "unclaimed": verify_report.summary.unclaimed,
        },
        "coverage": {
            "claimed_percentage": coverage_report.summary.claimed_percentage,
            "total_observations": coverage_report.summary.total_observations,
            "modules_with_claims": coverage_report.summary.modules_with_claims,
            "modules_without_claims": coverage_report.summary.modules_without_claims,
        },
    });

    serde_json::to_string_pretty(&json).unwrap_or_else(|_| "{}".to_string())
}

// --- Full docs (aphoria docs generate) ---

fn generate_full_docs_markdown(
    claims: &[AuthoredClaim],
    verify_report: &VerifyReport,
    coverage_report: &CoverageReport,
    project_name: &str,
) -> String {
    let mut out = String::new();

    out.push_str(&format!("# {project_name} — Reference Documentation\n\n"));

    // Section 1: Full claim details (reuse claims_explain)
    out.push_str(&crate::claims_explain::render_claims_markdown(claims, project_name));
    out.push('\n');

    // Section 2: Verification results
    out.push_str("---\n\n");
    out.push_str("# Verification Results\n\n");

    let summary = &verify_report.summary;
    out.push_str(&format!(
        "**Total:** {} claims verified — {} pass, {} conflict, {} missing, {} unclaimed observations\n\n",
        summary.total_claims, summary.pass, summary.conflict, summary.missing, summary.unclaimed,
    ));

    // Group verify results by verdict
    let mut conflicts = Vec::new();
    let mut missing = Vec::new();
    for result in &verify_report.results {
        match result.verdict {
            AuditVerdict::Conflict => conflicts.push(result),
            AuditVerdict::Missing => missing.push(result),
            _ => {}
        }
    }

    if !conflicts.is_empty() {
        out.push_str("## Conflicts\n\n");
        for r in &conflicts {
            if let Some(ref claim) = r.claim {
                out.push_str(&format!("- **{}**: {}\n", claim.id, r.explanation));
            }
        }
        out.push('\n');
    }

    if !missing.is_empty() {
        out.push_str("## Missing Observations\n\n");
        for r in &missing {
            if let Some(ref claim) = r.claim {
                out.push_str(&format!("- **{}**: {}\n", claim.id, r.explanation));
            }
        }
        out.push('\n');
    }

    // Section 3: Coverage table
    out.push_str("---\n\n");
    out.push_str(&crate::coverage::format_coverage_markdown(coverage_report));

    out
}

fn generate_full_docs_json(
    claims: &[AuthoredClaim],
    verify_report: &VerifyReport,
    coverage_report: &CoverageReport,
    project_name: &str,
) -> String {
    let claims_json: Vec<serde_json::Value> = claims
        .iter()
        .map(|c| {
            serde_json::json!({
                "id": c.id,
                "concept_path": c.concept_path,
                "predicate": c.predicate,
                "value": format!("{}", c.value),
                "provenance": c.provenance,
                "invariant": c.invariant,
                "consequence": c.consequence,
                "authority_tier": c.authority_tier,
                "category": c.category,
                "status": format!("{:?}", c.status),
            })
        })
        .collect();

    let verify_json: Vec<serde_json::Value> = verify_report
        .results
        .iter()
        .filter(|r| r.claim.is_some())
        .map(|r| {
            let claim = r.claim.as_ref().unwrap_or_else(|| {
                // Safety: filtered above
                unreachable!()
            });
            serde_json::json!({
                "claim_id": claim.id,
                "verdict": format!("{}", r.verdict),
                "explanation": r.explanation,
                "matching_observations": r.matching_observations.len(),
            })
        })
        .collect();

    let json = serde_json::json!({
        "project": project_name,
        "type": "full_docs",
        "claims": claims_json,
        "verification": {
            "summary": {
                "total_claims": verify_report.summary.total_claims,
                "pass": verify_report.summary.pass,
                "conflict": verify_report.summary.conflict,
                "missing": verify_report.summary.missing,
                "unclaimed": verify_report.summary.unclaimed,
            },
            "results": verify_json,
        },
        "coverage": coverage_report,
    });

    serde_json::to_string_pretty(&json).unwrap_or_else(|_| "{}".to_string())
}

// --- Helpers ---

fn group_by_category(claims: &[AuthoredClaim]) -> BTreeMap<String, Vec<&AuthoredClaim>> {
    let mut categories: BTreeMap<String, Vec<&AuthoredClaim>> = BTreeMap::new();
    for claim in claims {
        categories.entry(claim.category.clone()).or_default().push(claim);
    }
    categories
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::coverage::{CoverageSummary, ModuleCoverage};
    use crate::types::authored_claim::{AuthoredValue, ComparisonMode};
    use crate::verify::{VerifyResult, VerifySummary};

    fn sample_claim(id: &str, category: &str) -> AuthoredClaim {
        AuthoredClaim {
            id: id.to_string(),
            concept_path: "test/concept".to_string(),
            predicate: "test_pred".to_string(),
            value: AuthoredValue::Text("test_value".to_string()),
            comparison: ComparisonMode::default(),
            provenance: "Test provenance".to_string(),
            invariant: "Test invariant".to_string(),
            consequence: "Bad things happen".to_string(),
            authority_tier: "expert".to_string(),
            evidence: vec![],
            category: category.to_string(),
            status: ClaimStatus::Active,
            supersedes: None,
            created_by: "tester".to_string(),
            created_at: "2026-02-08".to_string(),
            updated_at: None,
        }
    }

    fn empty_verify_report() -> VerifyReport {
        VerifyReport { results: vec![], summary: VerifySummary::default() }
    }

    fn empty_coverage_report() -> CoverageReport {
        CoverageReport {
            project: "test".to_string(),
            modules: vec![],
            summary: CoverageSummary {
                total_observations: 0,
                total_claims: 0,
                claimed_percentage: 0.0,
                unclaimed_count: 0,
                modules_with_claims: 0,
                modules_without_claims: 0,
            },
        }
    }

    fn sample_verify_report() -> VerifyReport {
        VerifyReport {
            results: vec![
                VerifyResult {
                    claim: Some(sample_claim("c1", "safety")),
                    verdict: AuditVerdict::Pass,
                    matching_observations: vec![],
                    explanation: "Matches".to_string(),
                },
                VerifyResult {
                    claim: Some(sample_claim("c2", "architecture")),
                    verdict: AuditVerdict::Conflict,
                    matching_observations: vec![],
                    explanation: "Value mismatch".to_string(),
                },
            ],
            summary: VerifySummary {
                total_claims: 2,
                pass: 1,
                conflict: 1,
                missing: 0,
                unclaimed: 3,
            },
        }
    }

    fn sample_coverage_report() -> CoverageReport {
        CoverageReport {
            project: "test".to_string(),
            modules: vec![
                ModuleCoverage {
                    module_path: "tls".to_string(),
                    files: vec!["src/tls/config.rs".to_string()],
                    observation_count: 5,
                    claim_count: 2,
                    claimed_observations: 3,
                    unclaimed_observations: 2,
                    missing_claims: 0,
                    density: 0.4,
                },
                ModuleCoverage {
                    module_path: "auth".to_string(),
                    files: vec!["src/auth/jwt.rs".to_string()],
                    observation_count: 3,
                    claim_count: 0,
                    claimed_observations: 0,
                    unclaimed_observations: 3,
                    missing_claims: 0,
                    density: 0.0,
                },
            ],
            summary: CoverageSummary {
                total_observations: 8,
                total_claims: 2,
                claimed_percentage: 37.5,
                unclaimed_count: 5,
                modules_with_claims: 1,
                modules_without_claims: 1,
            },
        }
    }

    #[test]
    fn test_onboarding_has_category_table() {
        let claims = vec![
            sample_claim("s1", "safety"),
            sample_claim("a1", "architecture"),
            sample_claim("s2", "safety"),
        ];
        let out = generate_onboarding(
            &claims,
            &empty_verify_report(),
            &empty_coverage_report(),
            "myproject",
            "markdown",
        );
        assert!(out.contains("# myproject"));
        assert!(out.contains("3** active claims"));
        assert!(out.contains("| Safety"));
        assert!(out.contains("| Architecture"));
    }

    #[test]
    fn test_onboarding_shows_verification_health() {
        let claims = vec![sample_claim("c1", "safety")];
        let vr = sample_verify_report();
        let out = generate_onboarding(&claims, &vr, &empty_coverage_report(), "proj", "markdown");
        assert!(out.contains("**Pass:** 1"));
        assert!(out.contains("**Conflict:** 1"));
        assert!(out.contains("Conflicts indicate"));
    }

    #[test]
    fn test_onboarding_shows_coverage_snapshot() {
        let claims = vec![sample_claim("c1", "safety")];
        let cr = sample_coverage_report();
        let out = generate_onboarding(&claims, &empty_verify_report(), &cr, "proj", "markdown");
        assert!(out.contains("37.5%"));
        assert!(out.contains("`auth`"));
    }

    #[test]
    fn test_onboarding_pointers() {
        let out = generate_onboarding(
            &[],
            &empty_verify_report(),
            &empty_coverage_report(),
            "proj",
            "markdown",
        );
        assert!(out.contains("aphoria claims explain"));
        assert!(out.contains("aphoria docs generate"));
    }

    #[test]
    fn test_full_docs_includes_claim_details() {
        let claims = vec![sample_claim("c1", "safety")];
        let out = generate_full_docs(
            &claims,
            &empty_verify_report(),
            &empty_coverage_report(),
            "proj",
            "markdown",
        );
        // Should contain per-claim fields from claims_explain
        assert!(out.contains("**Concept:**"));
        assert!(out.contains("**Invariant:**"));
    }

    #[test]
    fn test_full_docs_includes_verify_results() {
        let claims = vec![sample_claim("c1", "safety")];
        let vr = sample_verify_report();
        let out = generate_full_docs(&claims, &vr, &empty_coverage_report(), "proj", "markdown");
        assert!(out.contains("# Verification Results"));
        assert!(out.contains("## Conflicts"));
        assert!(out.contains("Value mismatch"));
    }

    #[test]
    fn test_full_docs_includes_coverage_table() {
        let claims = vec![sample_claim("c1", "safety")];
        let cr = sample_coverage_report();
        let out = generate_full_docs(&claims, &empty_verify_report(), &cr, "proj", "markdown");
        assert!(out.contains("# Aphoria Coverage:"));
        assert!(out.contains("Coverage Gaps"));
    }

    #[test]
    fn test_onboarding_and_full_docs_differ() {
        let claims = vec![sample_claim("c1", "safety")];
        let vr = sample_verify_report();
        let cr = sample_coverage_report();
        let onboarding = generate_onboarding(&claims, &vr, &cr, "proj", "markdown");
        let full_docs = generate_full_docs(&claims, &vr, &cr, "proj", "markdown");
        assert_ne!(onboarding, full_docs);
        // Onboarding should NOT have per-claim concept details
        assert!(!onboarding.contains("**Concept:**"));
        // Full docs should NOT have the "Run `aphoria claims explain`" pointer
        assert!(!full_docs.contains("Run `aphoria claims explain`"));
    }

    #[test]
    fn test_onboarding_json() {
        let claims = vec![sample_claim("c1", "safety")];
        let out = generate_onboarding(
            &claims,
            &empty_verify_report(),
            &empty_coverage_report(),
            "proj",
            "json",
        );
        let parsed: serde_json::Value = serde_json::from_str(&out).unwrap_or_default();
        assert_eq!(parsed["type"], "onboarding");
        assert_eq!(parsed["active_claims"], 1);
    }

    #[test]
    fn test_full_docs_json() {
        let claims = vec![sample_claim("c1", "safety")];
        let out = generate_full_docs(
            &claims,
            &empty_verify_report(),
            &empty_coverage_report(),
            "proj",
            "json",
        );
        let parsed: serde_json::Value = serde_json::from_str(&out).unwrap_or_default();
        assert_eq!(parsed["type"], "full_docs");
        assert!(parsed["claims"].is_array());
        assert!(parsed["verification"].is_object());
        assert!(parsed["coverage"].is_object());
    }
}
