use anyhow::{anyhow, Context, Result};
use regex::Regex;
use serde::Serialize;
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::contract;

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EvaluationSeverity {
    Warning,
    Critical,
}

#[derive(Clone, Debug, Serialize)]
pub struct EvaluationIssue {
    pub severity: EvaluationSeverity,
    pub check: String,
    pub message: String,
}

#[derive(Clone, Debug, Serialize)]
pub struct StageEvaluation {
    pub orchestration: String,
    pub slug: String,
    pub stage: String,
    pub artifact_type: Option<String>,
    pub artifact_path: String,
    pub traceability_map: String,
    pub status: String,
    pub issues: Vec<EvaluationIssue>,
}

impl StageEvaluation {
    pub fn has_critical(&self) -> bool {
        self.issues
            .iter()
            .any(|issue| issue.severity == EvaluationSeverity::Critical)
    }
}

#[derive(Clone, Debug, Serialize)]
pub struct OrchestrationEvaluation {
    pub orchestration: String,
    pub slug: String,
    pub status: String,
    pub stages: Vec<StageEvaluation>,
    pub issue_count: usize,
    pub critical_count: usize,
}

#[derive(Clone, Debug, Serialize)]
pub struct QualityReport {
    pub orchestration: String,
    pub slug: String,
    pub status: String,
    pub evaluation: OrchestrationEvaluation,
    pub tools: Vec<QualityToolStatus>,
    pub recommendations: Vec<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct QualityToolStatus {
    pub id: String,
    pub available: bool,
    pub required: bool,
    pub command: String,
    pub fallback: String,
}

pub fn evaluate_stage(root: &Path, name: &str, stage_key: &str) -> Result<StageEvaluation> {
    let slug = crate::artifact_slug(name);
    let artifact_dir = root.join("docs").join(&slug);
    let traceability_map = artifact_dir.join("traceability-map.yaml");
    let stage = contract::stage_by_key(stage_key)
        .ok_or_else(|| anyhow!("unknown stage for evaluation: {stage_key}"))?;
    let artifact_path = artifact_dir.join(&stage.filename);
    let artifact_type = stage.artifact_type.clone();
    let mut issues = Vec::new();

    let map_value = read_yaml(&traceability_map);
    if map_value.is_none() {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "traceability-map",
            format!("traceability-map ausente em {}", traceability_map.display()),
        ));
    }
    check_traceability_state(
        &mut issues,
        map_value.as_ref(),
        stage_key,
        &stage.filename,
        artifact_path.exists(),
    );

    if !artifact_path.exists() {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "artifact-file",
            format!("artefato ausente: {}", artifact_path.display()),
        ));
        return Ok(finalize_stage_report(
            name,
            slug,
            stage_key,
            artifact_type,
            artifact_path,
            traceability_map,
            issues,
        ));
    }

    let markdown = fs::read_to_string(&artifact_path)
        .with_context(|| format!("reading artifact {}", artifact_path.display()))?;
    if let Some(artifact_type) = artifact_type.as_deref() {
        validate_required_sections(&mut issues, artifact_type, &markdown)?;
        validate_semantics(&mut issues, artifact_type, &markdown)?;
    }
    validate_context_pack_freshness(&mut issues, root, &slug, stage_key, &artifact_path);

    Ok(finalize_stage_report(
        name,
        slug,
        stage_key,
        artifact_type,
        artifact_path,
        traceability_map,
        issues,
    ))
}

pub fn evaluate_orchestration(root: &Path, name: &str) -> Result<OrchestrationEvaluation> {
    let slug = crate::artifact_slug(name);
    let artifact_dir = root.join("docs").join(&slug);
    let traceability_map = artifact_dir.join("traceability-map.yaml");
    let mut stages = Vec::new();

    if !traceability_map.exists() {
        let stage = contract::stage_by_key("idea").ok_or_else(|| anyhow!("missing idea stage"))?;
        let missing = StageEvaluation {
            orchestration: name.to_string(),
            slug: slug.clone(),
            stage: "idea".to_string(),
            artifact_type: stage.artifact_type.clone(),
            artifact_path: artifact_dir.join(&stage.filename).display().to_string(),
            traceability_map: traceability_map.display().to_string(),
            status: "fail".to_string(),
            issues: vec![issue(
                EvaluationSeverity::Critical,
                "traceability-map",
                format!("traceability-map ausente em {}", traceability_map.display()),
            )],
        };
        return Ok(orchestration_report(name, slug, vec![missing]));
    }

    let value = read_yaml(&traceability_map);
    for stage in contract::stages() {
        let file_exists = artifact_dir.join(&stage.filename).exists();
        let state = artifact_state(value.as_ref(), &stage.key);
        let should_eval = file_exists
            || matches!(
                state.as_deref(),
                Some("approved" | "recorded" | "draft" | "in_progress")
            );
        if should_eval {
            stages.push(evaluate_stage(root, name, &stage.key)?);
        }
    }
    Ok(orchestration_report(name, slug, stages))
}

pub fn quality_report(root: &Path, name: &str) -> Result<QualityReport> {
    let evaluation = evaluate_orchestration(root, name)?;
    let tools = quality_tools(root);
    let mut recommendations = Vec::new();
    if evaluation.critical_count > 0 {
        recommendations
            .push("Corrigir avaliações críticas antes de avançar o motor autônomo.".to_string());
    }
    for tool in &tools {
        if !tool.available && tool.required {
            recommendations.push(format!(
                "Instalar ou configurar `{}` para fortalecer o gate de qualidade.",
                tool.command
            ));
        }
    }
    if !root.join(".sdd/intelligence/learnings.jsonl").exists() {
        recommendations.push(
            "Rodar `sdd intelligence learn --all` para popular o índice derivado.".to_string(),
        );
    }
    let status = if evaluation.critical_count == 0 {
        "pass"
    } else {
        "fail"
    };
    Ok(QualityReport {
        orchestration: name.to_string(),
        slug: evaluation.slug.clone(),
        status: status.to_string(),
        evaluation,
        tools,
        recommendations,
    })
}

pub fn evaluation_event(kind: &str, report: &impl Serialize) -> serde_json::Value {
    json!({
        "schema_version": 1,
        "kind": kind,
        "report": report,
    })
}

fn orchestration_report(
    name: &str,
    slug: String,
    stages: Vec<StageEvaluation>,
) -> OrchestrationEvaluation {
    let issue_count = stages.iter().map(|stage| stage.issues.len()).sum();
    let critical_count = stages
        .iter()
        .flat_map(|stage| &stage.issues)
        .filter(|issue| issue.severity == EvaluationSeverity::Critical)
        .count();
    let status = if critical_count == 0 { "pass" } else { "fail" };
    OrchestrationEvaluation {
        orchestration: name.to_string(),
        slug,
        status: status.to_string(),
        stages,
        issue_count,
        critical_count,
    }
}

fn finalize_stage_report(
    name: &str,
    slug: String,
    stage: &str,
    artifact_type: Option<String>,
    artifact_path: PathBuf,
    traceability_map: PathBuf,
    issues: Vec<EvaluationIssue>,
) -> StageEvaluation {
    let status = if issues
        .iter()
        .any(|issue| issue.severity == EvaluationSeverity::Critical)
    {
        "fail"
    } else {
        "pass"
    };
    StageEvaluation {
        orchestration: name.to_string(),
        slug,
        stage: stage.to_string(),
        artifact_type,
        artifact_path: artifact_path.display().to_string(),
        traceability_map: traceability_map.display().to_string(),
        status: status.to_string(),
        issues,
    }
}

fn issue(
    severity: EvaluationSeverity,
    check: impl Into<String>,
    message: impl Into<String>,
) -> EvaluationIssue {
    EvaluationIssue {
        severity,
        check: check.into(),
        message: message.into(),
    }
}

fn validate_required_sections(
    issues: &mut Vec<EvaluationIssue>,
    artifact_type: &str,
    markdown: &str,
) -> Result<()> {
    let required = contract::artifact_spec(artifact_type)
        .ok_or_else(|| anyhow!("unknown artifact type: {artifact_type}"))?
        .required_sections
        .clone();
    let headings = collect_headings(markdown)?;
    for section in required {
        if !headings.contains(&normalize_heading(&section)) {
            issues.push(issue(
                EvaluationSeverity::Critical,
                "required-section",
                format!("seção obrigatória ausente: {section}"),
            ));
        }
    }
    Ok(())
}

fn validate_semantics(
    issues: &mut Vec<EvaluationIssue>,
    artifact_type: &str,
    markdown: &str,
) -> Result<()> {
    let sections = collect_sections(markdown)?;
    if contract::artifact_semantic_validations(artifact_type).contains(&"traceability") {
        validate_traceability_rule(issues, artifact_type, &sections);
    }
    if contract::artifact_semantic_validations(artifact_type).contains(&"diagrams") {
        validate_diagrams_rule(issues, &sections);
    }
    if contract::artifact_semantic_validations(artifact_type).contains(&"prompts-agent") {
        validate_prompts_agent_rule(issues, &sections)?;
    }
    if contract::artifact_semantic_validations(artifact_type).contains(&"decision-package") {
        validate_checkpoint_rule(issues, &sections);
    }
    Ok(())
}

fn validate_traceability_rule(
    issues: &mut Vec<EvaluationIssue>,
    artifact_type: &str,
    sections: &BTreeMap<String, String>,
) {
    let Some(body) = section_body(sections, "Rastreabilidade") else {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "traceability",
            "seção `Rastreabilidade` vazia ou sem conteúdo verificável",
        ));
        return;
    };
    let dependencies = contract::stage_traceability_from(artifact_type);
    if dependencies.is_empty() {
        return;
    }
    let haystack = body.to_lowercase();
    let related = dependencies.iter().any(|stage| {
        haystack.contains(&stage.key.to_lowercase())
            || haystack.contains(&stage.filename.to_lowercase())
            || haystack.contains(&stage.label.to_lowercase())
    });
    if !related {
        let expected = dependencies
            .iter()
            .map(|stage| stage.filename.as_str())
            .collect::<Vec<_>>()
            .join(", ");
        issues.push(issue(
            EvaluationSeverity::Critical,
            "traceability",
            format!("Rastreabilidade deve citar artefato de origem esperado: {expected}"),
        ));
    }
}

fn validate_diagrams_rule(issues: &mut Vec<EvaluationIssue>, sections: &BTreeMap<String, String>) {
    let Some(body) = section_body(sections, "Diagramas") else {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "diagrams",
            "seção `Diagramas` vazia ou sem conteúdo verificável",
        ));
        return;
    };
    let body_lower = body.to_lowercase();
    let valid = contract::artifact_allowed_diagram_markers()
        .into_iter()
        .map(|marker| marker.to_lowercase())
        .any(|marker| body_lower.contains(&marker));
    let valid_visual_companion = Regex::new(r#"(?i)assets/diagrams/[^\s`)'"<>]+\.(html|svg)"#)
        .map(|regex| regex.is_match(body))
        .unwrap_or(false);
    if !valid && !valid_visual_companion {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "diagrams",
            "Diagramas deve conter Mermaid, referência `.excalidraw`, companion `assets/diagrams/*.html|*.svg` ou `Não aplicável`",
        ));
    }
}

fn validate_prompts_agent_rule(
    issues: &mut Vec<EvaluationIssue>,
    sections: &BTreeMap<String, String>,
) -> Result<()> {
    let backlog_ids = section_body(sections, "Backlog")
        .map(extract_task_ids)
        .transpose()?
        .unwrap_or_default();
    let prompt_ids = section_body(sections, "Prompts Agent")
        .map(extract_task_ids)
        .transpose()?
        .unwrap_or_default();
    if prompt_ids.is_empty() {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "prompts-agent",
            "Prompts Agent deve declarar pelo menos um ID executável (`T-01` ou `TSK-01`)",
        ));
    }
    let missing_prompts = backlog_ids
        .difference(&prompt_ids)
        .cloned()
        .collect::<Vec<_>>();
    if !missing_prompts.is_empty() {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "prompts-agent",
            format!(
                "Prompts Agent está sem prompts para: {}",
                missing_prompts.join(", ")
            ),
        ));
    }
    Ok(())
}

fn validate_checkpoint_rule(
    issues: &mut Vec<EvaluationIssue>,
    sections: &BTreeMap<String, String>,
) {
    for heading in [
        "Artefato",
        "Origem",
        "Decisão necessária",
        "Evidências",
        "Riscos pendentes",
    ] {
        if section_body(sections, heading).is_none() {
            issues.push(issue(
                EvaluationSeverity::Critical,
                "decision-package",
                format!("Checkpoint deve preencher `{heading}`"),
            ));
        }
    }
}

fn validate_context_pack_freshness(
    issues: &mut Vec<EvaluationIssue>,
    root: &Path,
    slug: &str,
    stage: &str,
    artifact_path: &Path,
) {
    if !matches!(stage, "techspec" | "execution" | "review" | "memory") {
        return;
    }
    let pack = root
        .join(".sdd/intelligence/context-packs")
        .join(slug)
        .join(format!("{stage}.md"));
    if !pack.exists() {
        issues.push(issue(
            EvaluationSeverity::Warning,
            "context-pack",
            format!("Context Pack ausente: {}", pack.display()),
        ));
        return;
    }
    let pack_mtime = modified(&pack);
    let artifact_mtime = modified(artifact_path);
    if let (Some(pack_mtime), Some(artifact_mtime)) = (pack_mtime, artifact_mtime) {
        if pack_mtime < artifact_mtime {
            issues.push(issue(
                EvaluationSeverity::Warning,
                "context-pack",
                format!(
                    "Context Pack {} é mais antigo que o artefato avaliado",
                    pack.display()
                ),
            ));
        }
    }
}

fn check_traceability_state(
    issues: &mut Vec<EvaluationIssue>,
    map: Option<&serde_yaml::Value>,
    stage: &str,
    expected_file: &str,
    artifact_exists: bool,
) {
    let Some(map) = map else {
        return;
    };
    let Some(stage_node) = map.get("artifacts").and_then(|node| node.get(stage)) else {
        issues.push(issue(
            EvaluationSeverity::Critical,
            "traceability-map",
            format!("traceability-map sem entrada para stage `{stage}`"),
        ));
        return;
    };
    let file = stage_node.get("file").and_then(serde_yaml::Value::as_str);
    if file != Some(expected_file) {
        issues.push(issue(
            EvaluationSeverity::Warning,
            "traceability-map",
            format!(
                "arquivo registrado para `{stage}` diverge do contrato: {:?} != {expected_file}",
                file
            ),
        ));
    }
    let state = stage_node
        .get("state")
        .and_then(serde_yaml::Value::as_str)
        .unwrap_or("unknown");
    if artifact_exists && matches!(state, "pending" | "optional") {
        issues.push(issue(
            EvaluationSeverity::Warning,
            "traceability-map",
            format!("artefato existe, mas estado no mapa ainda é `{state}`"),
        ));
    }
}

fn artifact_state(map: Option<&serde_yaml::Value>, stage: &str) -> Option<String> {
    map.and_then(|map| map.get("artifacts"))
        .and_then(|artifacts| artifacts.get(stage))
        .and_then(|stage| stage.get("state"))
        .and_then(serde_yaml::Value::as_str)
        .map(ToString::to_string)
}

fn collect_headings(markdown: &str) -> Result<BTreeSet<String>> {
    let re = Regex::new(r"^#{1,6}\s+(.+?)\s*$")?;
    Ok(markdown
        .lines()
        .filter_map(|line| re.captures(line).and_then(|cap| cap.get(1)))
        .map(|m| normalize_heading(m.as_str()))
        .collect())
}

fn collect_sections(markdown: &str) -> Result<BTreeMap<String, String>> {
    let re = Regex::new(r"^#{1,2}\s+(.+?)\s*$")?;
    let mut sections = BTreeMap::new();
    let mut current_heading: Option<String> = None;
    let mut current_body = Vec::new();
    for line in markdown.lines() {
        if let Some(capture) = re.captures(line) {
            if let Some(heading) = current_heading.take() {
                sections.insert(heading, current_body.join("\n").trim().to_string());
            }
            current_heading = capture.get(1).map(|item| normalize_heading(item.as_str()));
            current_body.clear();
        } else if current_heading.is_some() {
            current_body.push(line.to_string());
        }
    }
    if let Some(heading) = current_heading {
        sections.insert(heading, current_body.join("\n").trim().to_string());
    }
    Ok(sections)
}

fn section_body<'a>(sections: &'a BTreeMap<String, String>, heading: &str) -> Option<&'a str> {
    sections
        .get(&normalize_heading(heading))
        .map(String::as_str)
        .filter(|body| !body.trim().is_empty())
}

fn extract_task_ids(text: &str) -> Result<BTreeSet<String>> {
    let regexes = contract::artifact_prompt_id_patterns()
        .into_iter()
        .map(Regex::new)
        .collect::<std::result::Result<Vec<_>, _>>()?;
    let mut ids = BTreeSet::new();
    for raw in text.split_whitespace() {
        let token = raw.trim_matches(|ch: char| !ch.is_ascii_alphanumeric() && ch != '-');
        if regexes.iter().any(|regex| regex.is_match(token)) {
            ids.insert(token.to_string());
        }
    }
    Ok(ids)
}

fn normalize_heading(text: &str) -> String {
    text.split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
        .trim()
        .to_lowercase()
}

fn read_yaml(path: &Path) -> Option<serde_yaml::Value> {
    fs::read_to_string(path)
        .ok()
        .and_then(|text| serde_yaml::from_str(&text).ok())
}

fn modified(path: &Path) -> Option<SystemTime> {
    fs::metadata(path)
        .ok()
        .and_then(|metadata| metadata.modified().ok())
}

fn command_available(command: &str) -> bool {
    crate::runtime::platform::find_executable(command).is_some()
}

fn quality_tools(root: &Path) -> Vec<QualityToolStatus> {
    vec![
        QualityToolStatus {
            id: "codegraph".to_string(),
            available: command_available("codegraph") && root.join(".codegraph").exists(),
            required: false,
            command: "codegraph".to_string(),
            fallback: "rg/git/context pack".to_string(),
        },
        QualityToolStatus {
            id: "lexa".to_string(),
            available: command_available("lexa") && root.join(".lexa/graph.lexa").exists(),
            required: false,
            command: "lexa".to_string(),
            fallback: "rg/git/context pack".to_string(),
        },
        QualityToolStatus {
            id: "coverage".to_string(),
            available: command_available("cargo-llvm-cov") || command_available("cargo-tarpaulin"),
            required: false,
            command: "cargo llvm-cov | cargo tarpaulin".to_string(),
            fallback: "cargo test".to_string(),
        },
        QualityToolStatus {
            id: "audit".to_string(),
            available: command_available("cargo-audit"),
            required: false,
            command: "cargo audit".to_string(),
            fallback: "dependabot/GitHub advisory review".to_string(),
        },
        QualityToolStatus {
            id: "deny".to_string(),
            available: command_available("cargo-deny"),
            required: false,
            command: "cargo deny check".to_string(),
            fallback: "manual dependency/license review".to_string(),
        },
        QualityToolStatus {
            id: "secrets".to_string(),
            available: command_available("gitleaks"),
            required: false,
            command: "gitleaks detect".to_string(),
            fallback: "redaction + git diff review".to_string(),
        },
    ]
}
