use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

use harn_lint::LintSeverity;
use serde::Serialize;

use crate::cli::PersonaDoctorArgs;
use crate::package::{self, PersonaManifestEntry, ResolvedPersonaManifest};
use crate::test_runner;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DoctorStatus {
    Green,
    Yellow,
    Red,
}

impl DoctorStatus {
    fn label(self) -> &'static str {
        match self {
            Self::Green => "green",
            Self::Yellow => "yellow",
            Self::Red => "red",
        }
    }
}

#[derive(Debug, Serialize)]
pub struct DoctorCheck {
    pub name: String,
    pub status: DoctorStatus,
    pub message: String,
}

#[derive(Debug, Serialize)]
pub struct PersonaDoctorReport {
    pub persona: String,
    pub manifest_path: PathBuf,
    pub checks: Vec<DoctorCheck>,
}

impl PersonaDoctorReport {
    fn has_red(&self) -> bool {
        self.checks
            .iter()
            .any(|check| check.status == DoctorStatus::Red)
    }
}

pub(crate) async fn run_doctor(
    manifest_arg: Option<&Path>,
    args: &PersonaDoctorArgs,
) -> Result<(), String> {
    let report = doctor_report(manifest_arg, args).await;
    match report {
        Ok(report) => {
            if args.json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&report)
                        .map_err(|error| format!("failed to serialize doctor report: {error}"))?
                );
            } else {
                print_report(&report);
            }
            Ok(())
        }
        Err(error_report) => {
            if args.json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&error_report)
                        .map_err(|error| format!("failed to serialize doctor report: {error}"))?
                );
            } else {
                print_report(&error_report);
            }
            Err("persona doctor found red checks".to_string())
        }
    }
}

pub(crate) async fn doctor_report(
    manifest_arg: Option<&Path>,
    args: &PersonaDoctorArgs,
) -> Result<PersonaDoctorReport, PersonaDoctorReport> {
    let manifest_path = resolve_manifest_path(manifest_arg, &args.name);
    let mut checks = Vec::new();
    let catalog = match package::load_personas_from_manifest_path(&manifest_path) {
        Ok(catalog) => {
            checks.push(check(
                "manifest",
                DoctorStatus::Green,
                format!("{} validates", catalog.manifest_path.display()),
            ));
            catalog
        }
        Err(errors) => {
            checks.push(check(
                "manifest",
                DoctorStatus::Red,
                errors
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
                    .join("; "),
            ));
            return Err(PersonaDoctorReport {
                persona: args.name.clone(),
                manifest_path,
                checks,
            });
        }
    };

    let Some(persona) = catalog
        .personas
        .iter()
        .find(|persona| persona.name.as_deref() == Some(args.name.as_str()))
        .or_else(|| {
            catalog
                .personas
                .iter()
                .find(|persona| persona.name.as_deref() == Some(path_name(&args.name).as_str()))
        })
    else {
        checks.push(check(
            "manifest-persona",
            DoctorStatus::Red,
            format!(
                "persona '{}' not found in {}",
                args.name,
                catalog.manifest_path.display()
            ),
        ));
        return Err(PersonaDoctorReport {
            persona: args.name.clone(),
            manifest_path: catalog.manifest_path,
            checks,
        });
    };
    let persona_name = persona.name.clone().unwrap_or_else(|| args.name.clone());

    let entry_source = resolve_entry_source(&catalog, persona);
    checks.push(source_shape_check(&entry_source));
    checks.push(lint_check(&catalog, &entry_source));
    checks.push(prompt_asset_check(&catalog, &entry_source));
    checks.push(step_metadata_check(persona));
    checks.push(cost_check(persona));
    checks.push(smoke_check(&catalog, &persona_name, args.timeout_ms).await);

    let report = PersonaDoctorReport {
        persona: persona_name,
        manifest_path: catalog.manifest_path,
        checks,
    };
    if report.has_red() {
        Err(report)
    } else {
        Ok(report)
    }
}

pub async fn doctor_report_for_persona(
    manifest_arg: Option<&Path>,
    name: &str,
    timeout_ms: u64,
) -> Result<PersonaDoctorReport, PersonaDoctorReport> {
    let args = PersonaDoctorArgs {
        name: name.to_string(),
        json: false,
        timeout_ms,
    };
    doctor_report(manifest_arg, &args).await
}

fn print_report(report: &PersonaDoctorReport) {
    println!(
        "persona doctor: {} ({})",
        report.persona,
        report.manifest_path.display()
    );
    for check in &report.checks {
        println!(
            "  {:<6} {:<20} {}",
            check.status.label(),
            check.name,
            check.message
        );
    }
}

fn check(name: &str, status: DoctorStatus, message: impl Into<String>) -> DoctorCheck {
    DoctorCheck {
        name: name.to_string(),
        status,
        message: message.into(),
    }
}

fn resolve_manifest_path(manifest_arg: Option<&Path>, name: &str) -> PathBuf {
    if let Some(path) = manifest_arg {
        return path.to_path_buf();
    }
    let raw = PathBuf::from(name);
    if raw.is_dir() {
        let manifest = raw.join("harn.toml");
        if manifest.exists() {
            return manifest;
        }
    }
    if raw.is_file() {
        return raw;
    }
    let normalized = name.replace('-', "_");
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    for candidate in [
        cwd.join("personas").join(&normalized).join("harn.toml"),
        cwd.join(&normalized).join("harn.toml"),
    ] {
        if candidate.exists() {
            return candidate;
        }
    }
    PathBuf::from("harn.toml")
}

fn path_name(value: &str) -> String {
    Path::new(value)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(value)
        .replace('-', "_")
}

fn resolve_entry_source(
    catalog: &ResolvedPersonaManifest,
    persona: &PersonaManifestEntry,
) -> Option<PathBuf> {
    let entry = persona.entry_workflow.as_deref()?;
    let (path, _) = entry.split_once('#')?;
    Some(catalog.manifest_dir.join(path))
}

fn source_shape_check(entry_source: &Option<PathBuf>) -> DoctorCheck {
    let Some(path) = entry_source else {
        return check(
            "entry-source",
            DoctorStatus::Red,
            "entry_workflow must point at a .harn file with #run",
        );
    };
    match fs::read_to_string(path) {
        Ok(source) => {
            let banned = [
                "__host_agent",
                "workflow_stage_agent_loop",
                "harn_vm::",
                "RustAgent",
            ];
            if let Some(token) = banned.iter().find(|token| source.contains(**token)) {
                return check(
                    "entry-source",
                    DoctorStatus::Red,
                    format!(
                        "{} references removed/private runtime token {token}",
                        path.display()
                    ),
                );
            }
            if !source.contains("@persona") {
                return check(
                    "entry-source",
                    DoctorStatus::Red,
                    format!("{} does not declare @persona", path.display()),
                );
            }
            check(
                "entry-source",
                DoctorStatus::Green,
                format!("{} is Harn-first and declares @persona", path.display()),
            )
        }
        Err(error) => check(
            "entry-source",
            DoctorStatus::Red,
            format!("failed to read {}: {error}", path.display()),
        ),
    }
}

fn lint_check(catalog: &ResolvedPersonaManifest, entry_source: &Option<PathBuf>) -> DoctorCheck {
    let Some(path) = entry_source else {
        return check("lint", DoctorStatus::Red, "entry source unavailable");
    };
    let source = match fs::read_to_string(path) {
        Ok(source) => source,
        Err(error) => {
            return check(
                "lint",
                DoctorStatus::Red,
                format!("failed to read {}: {error}", path.display()),
            )
        }
    };
    let program = match harn_parser::parse_source(&source) {
        Ok(program) => program,
        Err(error) => return check("lint", DoctorStatus::Red, error.to_string()),
    };
    let files = collect_package_harn_files(&catalog.manifest_dir);
    let module_graph = crate::commands::check::build_module_graph(&files);
    let options = harn_lint::LintOptions {
        file_path: Some(path),
        ..Default::default()
    };
    let diagnostics = harn_lint::lint_with_module_graph(
        &program,
        &[],
        Some(&source),
        &HashSet::new(),
        &module_graph,
        path,
        &options,
    );
    if diagnostics.is_empty() {
        return check("lint", DoctorStatus::Green, "no issues found");
    }
    let red = diagnostics.iter().any(|diag| {
        diag.severity == LintSeverity::Error || diag.rule == "persona-body-must-call-steps"
    });
    let status = if red {
        DoctorStatus::Red
    } else {
        DoctorStatus::Yellow
    };
    let summary = diagnostics
        .iter()
        .take(3)
        .map(|diag| format!("{}: {}", diag.rule, diag.message))
        .collect::<Vec<_>>()
        .join("; ");
    check("lint", status, summary)
}

fn prompt_asset_check(
    catalog: &ResolvedPersonaManifest,
    entry_source: &Option<PathBuf>,
) -> DoctorCheck {
    let prompt_dir = catalog.manifest_dir.join("prompts");
    let prompt_files = collect_prompt_files(&prompt_dir);
    if prompt_files.is_empty() {
        return check(
            "prompt-assets",
            DoctorStatus::Yellow,
            "no .harn.prompt assets found",
        );
    }
    for path in &prompt_files {
        let source = match fs::read_to_string(path) {
            Ok(source) => source,
            Err(error) => {
                return check(
                    "prompt-assets",
                    DoctorStatus::Red,
                    format!("failed to read {}: {error}", path.display()),
                )
            }
        };
        if let Err(error) = harn_vm::stdlib::template::validate_template_syntax(&source) {
            return check(
                "prompt-assets",
                DoctorStatus::Red,
                format!("{}: {error}", path.display()),
            );
        }
    }
    let uses_prompt_asset = entry_source
        .as_ref()
        .and_then(|path| fs::read_to_string(path).ok())
        .is_some_and(|source| source.contains("render_prompt(") || source.contains("render("));
    let status = if uses_prompt_asset {
        DoctorStatus::Green
    } else {
        DoctorStatus::Yellow
    };
    check(
        "prompt-assets",
        status,
        format!("{} prompt asset(s) validate", prompt_files.len()),
    )
}

fn step_metadata_check(persona: &PersonaManifestEntry) -> DoctorCheck {
    if persona.steps.is_empty() {
        return check(
            "step-metadata",
            DoctorStatus::Red,
            "entry source did not expose typed @step metadata",
        );
    }
    let missing_receipt = persona
        .steps
        .iter()
        .filter(|step| step.receipt.as_deref().unwrap_or_default().is_empty())
        .count();
    if missing_receipt > 0 {
        return check(
            "step-metadata",
            DoctorStatus::Yellow,
            format!(
                "{} step(s) found, {missing_receipt} without explicit receipt policy",
                persona.steps.len()
            ),
        );
    }
    check(
        "step-metadata",
        DoctorStatus::Green,
        format!("{} typed step(s) found", persona.steps.len()),
    )
}

fn cost_check(persona: &PersonaManifestEntry) -> DoctorCheck {
    let step_token_budget: u64 = persona
        .steps
        .iter()
        .filter_map(|step| step.budget.as_ref()?.max_tokens)
        .sum();
    let Some(max_tokens) = persona.budget.max_tokens else {
        return check(
            "cost-budget",
            DoctorStatus::Yellow,
            "manifest has no max_tokens budget",
        );
    };
    if step_token_budget == 0 {
        return check(
            "cost-budget",
            DoctorStatus::Yellow,
            format!("manifest max_tokens={max_tokens}, no per-step token budgets"),
        );
    }
    if step_token_budget > max_tokens {
        return check(
            "cost-budget",
            DoctorStatus::Red,
            format!("per-step max_tokens sum {step_token_budget} exceeds manifest max_tokens {max_tokens}"),
        );
    }
    check(
        "cost-budget",
        DoctorStatus::Green,
        format!(
            "per-step max_tokens sum {step_token_budget} within manifest max_tokens {max_tokens}"
        ),
    )
}

async fn smoke_check(
    catalog: &ResolvedPersonaManifest,
    persona_name: &str,
    timeout_ms: u64,
) -> DoctorCheck {
    let test_path = catalog
        .manifest_dir
        .join("tests")
        .join(format!("{persona_name}_smoke.harn"));
    if !test_path.exists() {
        return check(
            "smoke-test",
            DoctorStatus::Yellow,
            format!("{} not found", test_path.display()),
        );
    }
    let summary = test_runner::run_tests(&test_path, None, timeout_ms, false, &[]).await;
    if summary.failed > 0 {
        let first_error = summary
            .results
            .iter()
            .find(|result| !result.passed)
            .and_then(|result| result.error.as_deref())
            .unwrap_or("smoke test failed");
        return check(
            "smoke-test",
            DoctorStatus::Red,
            format!("{first_error} ({} failed)", summary.failed),
        );
    }
    if summary.total == 0 {
        return check(
            "smoke-test",
            DoctorStatus::Yellow,
            "no test pipelines found",
        );
    }
    check(
        "smoke-test",
        DoctorStatus::Green,
        format!("{} smoke test(s) passed", summary.passed),
    )
}

fn collect_package_harn_files(dir: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    crate::commands::collect_harn_files(dir, &mut files);
    files
}

fn collect_prompt_files(dir: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    collect_prompt_files_inner(dir, &mut files);
    files.sort();
    files
}

fn collect_prompt_files_inner(dir: &Path, out: &mut Vec<PathBuf>) {
    let Ok(entries) = fs::read_dir(dir) else {
        return;
    };
    for entry in entries.filter_map(Result::ok) {
        let path = entry.path();
        if path.is_dir() {
            collect_prompt_files_inner(&path, out);
        } else if path
            .file_name()
            .and_then(|name| name.to_str())
            .is_some_and(|name| name.ends_with(".harn.prompt"))
        {
            out.push(path);
        }
    }
}
