//! Implements `limb doctor`. Diagnoses setup and emits actionable fixes.

use std::path::Path;
use std::time::SystemTime;

use anyhow::Result;
use serde::Serialize;

use crate::config::{Global, Hooks, Repo};
use crate::context::Context;
use crate::discover;
use crate::refresh;
use crate::style;

/// Suppresses the human-readable diagnostic line when the report is in
/// JSON mode. Text-mode `limb doctor` writes a tree of headers to stdout;
/// `--json` mode writes only the structured summary at the end.
macro_rules! sayln {
    ($report:expr, $($arg:tt)*) => {
        if !$report.json {
            println!($($arg)*);
        }
    };
}

/// Runs `limb doctor`.
///
/// Walks through the current repo, global config, per-repo config, and
/// `projects.roots` discovery; reports each check as `ok` / `missing` and
/// emits an aggregated list of problems at the end with concrete
/// fix suggestions.
///
/// # Errors
///
/// Returns an error. With exit code 1. If any problem was found. The
/// message itself is `{N} problem(s) found`; the details are printed to
/// stderr before the error returns.
pub fn run(ctx: &Context) -> Result<()> {
    let mut report = Report::new(ctx.json);

    let global = ctx.global().unwrap_or_else(|e| {
        report.problem(format!("global config: {e:#}"));
        Global::defaults()
    });

    let repo = ctx.repo().ok();
    let repo_config = ctx
        .repo_config()
        .map_err(|e| report.problem(format!("repo config: {e:#}")))
        .ok()
        .flatten();

    section_repo(&report, repo.as_deref());
    section_global(&global, &mut report);
    section_repo_config(repo_config.as_ref(), &mut report);
    if !global.projects_roots.is_empty() {
        section_discovered(&global, &mut report);
        section_freshness(&global, &mut report);
    }

    report.emit()
}

fn section_repo(report: &Report, repo: Option<&Path>) {
    sayln!(report, "repo:");
    match repo {
        Some(r) => sayln!(report, "  path: {}", r.display()),
        None => sayln!(report, "  (not in a git repository)"),
    }
}

fn section_global(global: &Global, report: &mut Report) {
    sayln!(report, "global config:");
    let Some(source) = &global.source else {
        sayln!(report, "  (none. Using built-in defaults)");
        return;
    };
    sayln!(report, "  source: {}", source.display());
    sayln!(report, "  theme: {}", global.ui_theme);
    sayln!(report, "  shell prefix: {}", global.shell_prefix);
    sayln!(
        report,
        "  git default remote: {}",
        global.git_default_remote
    );
    if let Some(base) = &global.git_default_base {
        sayln!(report, "  git default base: {base}");
    }
    if global.projects_roots.is_empty() {
        sayln!(report, "  projects.roots: (none)");
        return;
    }
    sayln!(report, "  projects.roots: {}", global.projects_roots.len());
    for root in &global.projects_roots {
        let ok = root.is_dir();
        let tag = if ok { "ok" } else { "missing" };
        sayln!(report, "    - {} [{tag}]", root.display());
        if !ok {
            report.problem(format!(
                "projects root does not exist: {}; \
                 try: remove it from ~/.config/limb/config.toml, or `mkdir -p {}`",
                root.display(),
                root.display()
            ));
        }
    }
}

fn section_repo_config(repo_config: Option<&Repo>, report: &mut Report) {
    sayln!(report, "repo config:");
    let Some(r) = repo_config else {
        sayln!(report, "  (no .limb.toml in this repo or any ancestor)");
        return;
    };
    sayln!(report, "  source: {}", r.source.display());
    sayln!(report, "  root: {}", r.root.display());
    sayln!(report, "  base_dir: {}", r.worktrees_base_dir.display());
    section_shared(r, report);
    section_templates(r, report);
    check_hooks("repo hooks", &r.hooks, &r.root, report);
}

fn section_shared(r: &Repo, report: &mut Report) {
    if r.worktrees_shared.is_empty() {
        sayln!(report, "  shared files: (none)");
        return;
    }
    let source = r.resolved_shared_source();
    sayln!(
        report,
        "  shared files: {} from {}",
        r.worktrees_shared.len(),
        source.display()
    );
    if !source.is_dir() {
        report.problem(format!(
            "shared source dir does not exist: {}; \
             try: `mkdir -p {}` and add the files listed under worktrees.shared",
            source.display(),
            source.display()
        ));
    }
    for f in &r.worktrees_shared {
        let full = source.join(f);
        let tag = if full.exists() { "ok" } else { "missing" };
        sayln!(report, "    - {} [{tag}]", f.display());
        if !full.exists() {
            report.note(format!(
                "shared file missing: {}; `limb setup` will skip it",
                full.display()
            ));
        }
    }
}

fn section_templates(r: &Repo, report: &Report) {
    if r.templates.is_empty() {
        sayln!(report, "  templates: (none)");
        return;
    }
    sayln!(report, "  templates: {}", r.templates.len());
    for name in r.templates.keys() {
        sayln!(report, "    - {name}");
    }
}

fn section_discovered(global: &Global, report: &mut Report) {
    sayln!(report, "discovered repos:");
    let mut total = 0_usize;
    for root in &global.projects_roots {
        if !root.is_dir() {
            continue;
        }
        match discover::repos_under(root) {
            Ok(repos) => {
                sayln!(report, "  {}: {}", root.display(), repos.len());
                total += repos.len();
            }
            Err(e) => report.problem(format!("cannot scan {}: {e:#}", root.display())),
        }
    }
    sayln!(report, "  total: {total}");
}

fn section_freshness(global: &Global, report: &mut Report) {
    sayln!(report, "freshness:");
    let now = SystemTime::now();
    let warn = global.refresh_doctor_warn;
    let error = global.refresh_doctor_error;

    for root in &global.projects_roots {
        if !root.is_dir() {
            continue;
        }
        let Ok(repos) = discover::repos_under(root) else {
            continue;
        };
        for repo in &repos {
            let Some(anchor) = discover::anchor_for(repo) else {
                continue;
            };
            let name = discover::repo_name(repo);
            match refresh::fetched_at(&anchor) {
                None => {
                    sayln!(report, "  {name}: never fetched");
                    report.note(format!(
                        "{} has no FETCH_HEAD; \
                         try: `limb update --fetch-only` (or `--all`)",
                        repo.display()
                    ));
                }
                Some(t) => {
                    let age = now.duration_since(t).unwrap_or_default();
                    let short = refresh::render_age(now, Some(t));
                    sayln!(report, "  {name}: {short}");
                    if age > error {
                        report.problem(format!(
                            "{} not fetched in {}; \
                             try: `limb update --fetch-only --all`",
                            repo.display(),
                            refresh::render_age_long(age)
                        ));
                    } else if age > warn {
                        report.note(format!(
                            "{} fetched {} ago",
                            repo.display(),
                            refresh::render_age_long(age)
                        ));
                    }
                }
            }
        }
    }
}

fn check_hooks(label: &str, hooks: &Hooks, root: &Path, report: &mut Report) {
    let entries = [
        ("pre_add", hooks.pre_add.as_deref()),
        ("post_add", hooks.post_add.as_deref()),
        ("pre_remove", hooks.pre_remove.as_deref()),
        ("post_remove", hooks.post_remove.as_deref()),
    ];
    if !entries.iter().any(|(_, p)| p.is_some()) {
        sayln!(report, "  {label}: (none)");
        return;
    }
    sayln!(report, "  {label}:");
    for (name, maybe) in entries {
        let Some(path) = maybe else { continue };
        let full = root.join(path);
        let tag = if full.is_file() { "ok" } else { "missing" };
        sayln!(report, "    - {name}: {} [{tag}]", path.display());
        if !full.is_file() {
            report.problem(format!(
                "hook script missing: {} ({name}); \
                 try: create the script and `chmod +x {}`",
                full.display(),
                full.display()
            ));
        }
    }
}

/// Severity of an aggregated diagnostic finding.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
enum Severity {
    Note,
    Problem,
}

/// One diagnostic finding emitted by a section.
#[derive(Debug, Serialize)]
struct Item {
    severity: Severity,
    message: String,
}

struct Report {
    items: Vec<Item>,
    json: bool,
}

impl Report {
    fn new(json: bool) -> Self {
        Self {
            items: Vec::new(),
            json,
        }
    }

    fn problem(&mut self, msg: String) {
        self.items.push(Item {
            severity: Severity::Problem,
            message: msg,
        });
    }

    fn note(&mut self, msg: String) {
        self.items.push(Item {
            severity: Severity::Note,
            message: msg,
        });
    }

    fn count(&self, severity: Severity) -> usize {
        self.items.iter().filter(|i| i.severity == severity).count()
    }

    fn emit(self) -> Result<()> {
        let problems = self.count(Severity::Problem);
        let notes = self.count(Severity::Note);

        if self.json {
            let out = serde_json::json!({
                "ok": problems == 0,
                "summary": { "problems": problems, "notes": notes },
                "items": self.items,
            });
            println!("{}", serde_json::to_string_pretty(&out)?);
            if problems > 0 {
                anyhow::bail!("{problems} problem(s) found");
            }
            return Ok(());
        }

        if notes > 0 {
            let s = style::DIM;
            anstream::println!();
            anstream::println!("{s}notes:{s:#}");
            for item in self.items.iter().filter(|i| i.severity == Severity::Note) {
                anstream::println!("  {s}- {}{s:#}", item.message);
            }
        }
        if problems == 0 {
            let ok = style::OK;
            anstream::println!();
            anstream::println!("{ok}✓ all checks passed{ok:#}");
            Ok(())
        } else {
            let e = style::ERROR;
            anstream::eprintln!();
            anstream::eprintln!("{e}problems:{e:#}");
            for item in self
                .items
                .iter()
                .filter(|i| i.severity == Severity::Problem)
            {
                anstream::eprintln!("  {e}✗{e:#} {}", item.message);
            }
            anyhow::bail!("{problems} problem(s) found")
        }
    }
}
