use std::collections::HashSet;
use std::path::Path;
use std::process::{Output, Stdio};
use std::sync::{Arc, RwLock};
use std::time::Duration;

use serde::{Deserialize, Serialize};
use tokio::process::Command;

use crate::config::{Config, McpTransport, expand_path};

pub const SUPPORTED_AGENT_BROWSER_VERSION: &str = "0.33.0";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthStatus {
    Pass,
    Warning,
    Fail,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthOverall {
    Healthy,
    Degraded,
    Unhealthy,
}

#[derive(Debug, Clone, Serialize)]
pub struct HealthCheck {
    pub name: String,
    pub category: String,
    pub required: bool,
    pub status: HealthStatus,
    pub detail: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub remediation: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct HealthReport {
    pub version: &'static str,
    pub overall: HealthOverall,
    pub checks: Vec<HealthCheck>,
}

impl HealthReport {
    pub fn configuration_error(error: impl Into<String>) -> Self {
        Self::from_checks(vec![HealthCheck {
            name: "configuration".to_string(),
            category: "core".to_string(),
            required: true,
            status: HealthStatus::Fail,
            detail: error.into(),
            remediation: Some(
                "Fix ~/.picobot/config.json (or ./config.json) and run picobot health again."
                    .to_string(),
            ),
        }])
    }

    fn from_checks(checks: Vec<HealthCheck>) -> Self {
        let overall = if checks
            .iter()
            .any(|check| check.required && check.status == HealthStatus::Fail)
        {
            HealthOverall::Unhealthy
        } else if checks
            .iter()
            .any(|check| check.status != HealthStatus::Pass)
        {
            HealthOverall::Degraded
        } else {
            HealthOverall::Healthy
        };
        Self {
            version: env!("CARGO_PKG_VERSION"),
            overall,
            checks,
        }
    }

    pub fn is_usable(&self) -> bool {
        self.overall != HealthOverall::Unhealthy
    }

    pub fn render_text(&self) -> String {
        let overall = match self.overall {
            HealthOverall::Healthy => "HEALTHY",
            HealthOverall::Degraded => "DEGRADED",
            HealthOverall::Unhealthy => "UNHEALTHY",
        };
        let mut lines = vec![format!("PicoBot {} health: {overall}", self.version)];
        for check in &self.checks {
            let icon = match check.status {
                HealthStatus::Pass => "✓",
                HealthStatus::Warning => "!",
                HealthStatus::Fail => "✗",
            };
            let requirement = if check.required {
                "required"
            } else {
                "optional"
            };
            lines.push(format!(
                "{icon} [{} / {requirement}] {} — {}",
                check.category, check.name, check.detail
            ));
            if let Some(remediation) = &check.remediation {
                lines.push(format!("  Fix: {remediation}"));
            }
        }
        lines.join("\n")
    }
}

#[derive(Clone)]
pub struct HealthService {
    config: Config,
    scheduler_runtime: Arc<RwLock<Option<SchedulerHealthRuntime>>>,
}

#[derive(Clone)]
struct SchedulerHealthRuntime {
    storage: Arc<crate::storage::Storage>,
    channels: HashSet<String>,
    catalog: Option<Arc<crate::agent::AgentCatalog>>,
}

impl HealthService {
    pub fn new(config: Config) -> Self {
        Self {
            config,
            scheduler_runtime: Arc::new(RwLock::new(None)),
        }
    }

    pub fn with_scheduler_runtime(
        self,
        storage: Arc<crate::storage::Storage>,
        channels: Vec<String>,
    ) -> Self {
        *self.scheduler_runtime.write().unwrap() = Some(SchedulerHealthRuntime {
            storage,
            channels: channels.into_iter().collect(),
            catalog: None,
        });
        self
    }

    pub fn bind_agent_catalog(&self, catalog: Arc<crate::agent::AgentCatalog>) {
        if let Some(runtime) = self.scheduler_runtime.write().unwrap().as_mut() {
            runtime.catalog = Some(catalog);
        }
    }

    pub async fn check(&self) -> HealthReport {
        let mut checks = vec![
            check_configuration_recovery(&self.config),
            check_workspace(&self.config),
            check_required_binary(
                "bash",
                "core",
                "Install Bash and make it available on PATH.",
            ),
            check_search_backend("content search", &["rg", "grep"], &["rg"]),
            check_search_backend("file search", &["fd", "fdfind", "find"], &["fd", "fdfind"]),
            check_optional_binary("systemd service management", "systemctl", "service"),
        ];
        checks.extend(self.check_mcp_commands());
        checks.extend(self.check_browser().await);
        checks.extend(self.check_scheduler().await);
        HealthReport::from_checks(checks)
    }

    async fn check_scheduler(&self) -> Vec<HealthCheck> {
        let scheduler_config = self.config.gateway.scheduler.clone().unwrap_or_default();
        if !scheduler_config.enabled {
            return vec![HealthCheck {
                name: "scheduled runs".to_string(),
                category: "configured".to_string(),
                required: false,
                status: HealthStatus::Pass,
                detail: "scheduler disabled; persisted jobs are not executed".to_string(),
                remediation: None,
            }];
        }
        let runtime = self.scheduler_runtime.read().unwrap().clone();
        let Some(runtime) = runtime else {
            return vec![HealthCheck {
                name: "scheduled runs".to_string(),
                category: "runtime".to_string(),
                required: false,
                status: HealthStatus::Pass,
                detail: "runtime scheduler state is checked by the running Gateway".to_string(),
                remediation: None,
            }];
        };
        let jobs = match runtime.storage.list_scheduled_jobs().await {
            Ok(jobs) => jobs,
            Err(error) => {
                return vec![HealthCheck {
                    name: "scheduled run storage".to_string(),
                    category: "runtime".to_string(),
                    required: true,
                    status: HealthStatus::Fail,
                    detail: format!("cannot read scheduled jobs: {error}"),
                    remediation: Some(
                        "Check the configured SQLite database and Gateway logs.".to_string(),
                    ),
                }];
            }
        };
        let now = chrono::Utc::now().timestamp_millis();
        let stale_after = scheduler_config
            .poll_interval_secs
            .max(60)
            .saturating_mul(10_000)
            .min(i64::MAX as u64) as i64;
        let mut bad_references = Vec::new();
        let mut stale_deliveries = Vec::new();
        let mut unhealthy_latest = Vec::new();
        let mut silent_unknown = Vec::new();
        let mut covered_intervals = Vec::new();
        let mut invalid_next = Vec::new();

        match runtime
            .storage
            .list_stale_scheduled_deliveries(now.saturating_sub(stale_after), 100)
            .await
        {
            Ok(runs) => {
                stale_deliveries.extend(
                    runs.into_iter()
                        .map(|run| format!("{}#{}", run.job_id, run.id)),
                );
            }
            Err(error) => bad_references.push(format!("delivery backlog unavailable ({error})")),
        }

        for job in &jobs {
            if !runtime.channels.contains(&job.channel) {
                bad_references.push(format!("{}: channel {}", job.id, job.channel));
            }
            if let Some(agent_id) = job.agent_id.as_deref()
                && runtime
                    .catalog
                    .as_ref()
                    .is_none_or(|catalog| catalog.get(agent_id).is_none())
            {
                bad_references.push(format!("{}: agent {}", job.id, agent_id));
            }
            if job.enabled && crate::scheduler::next_run_for_schedule(&job.schedule, now).is_none()
            {
                invalid_next.push(job.id.clone());
            }

            let runs = match runtime.storage.list_scheduled_job_runs(&job.id, 20).await {
                Ok(runs) => runs,
                Err(error) => {
                    bad_references.push(format!("{}: run history unavailable ({error})", job.id));
                    continue;
                }
            };
            if let Some(latest) = runs.first() {
                if matches!(
                    latest.status,
                    crate::storage::ScheduledRunStatus::Unknown
                        | crate::storage::ScheduledRunStatus::Failed
                        | crate::storage::ScheduledRunStatus::TimedOut
                ) {
                    unhealthy_latest.push(format!("{}#{}", job.id, latest.id));
                }
                if job.delivery_policy == crate::storage::DeliveryPolicy::Never
                    && latest.outcome == Some(crate::storage::ScheduledOutcomeKind::Unknown)
                {
                    silent_unknown.push(format!("{}#{}", job.id, latest.id));
                }
                if let crate::scheduler::Schedule::Every { every_ms } = job.schedule
                    && i64::try_from(every_ms).ok().is_some_and(|interval| {
                        latest
                            .duration_ms
                            .is_some_and(|duration| duration >= interval)
                    })
                {
                    covered_intervals.push(format!("{}#{}", job.id, latest.id));
                }
            }
        }

        vec![
            scheduler_health_check(
                "scheduled references",
                bad_references,
                "all Agent and channel references are available",
                "Update or disable jobs that reference missing Agents or channels.",
            ),
            scheduler_health_check(
                "scheduled delivery backlog",
                stale_deliveries,
                "no stale pending or delivering notifications",
                "Inspect cron_runs and the target channel configuration.",
            ),
            scheduler_health_check(
                "scheduled latest outcomes",
                unhealthy_latest,
                "no latest run is failed, timed out, or unknown",
                "Inspect cron_runs for the diagnostic and assess external side effects before retrying.",
            ),
            scheduler_health_check(
                "silent unknown scheduled runs",
                silent_unknown,
                "no never-delivery job has an unknown latest outcome",
                "Inspect the run manually; delivery_policy=never prevents automatic notification.",
            ),
            scheduler_health_check(
                "scheduled execution intervals",
                covered_intervals,
                "recent execution durations fit their Every intervals",
                "Increase the interval or split long-running jobs.",
            ),
            scheduler_health_check(
                "scheduled next runs",
                invalid_next,
                "all enabled schedules can compute a next run",
                "Correct the schedule expression or disable the job.",
            ),
        ]
    }

    fn check_mcp_commands(&self) -> Vec<HealthCheck> {
        let mut seen = HashSet::new();
        let mut checks = Vec::new();
        for server in &self.config.mcp.servers {
            if !server.enabled {
                continue;
            }
            if !matches!(server.transport, McpTransport::Stdio) {
                continue;
            }
            let Some(command) = server.command.as_deref() else {
                checks.push(HealthCheck {
                    name: format!("MCP server {}", server.name),
                    category: "configured".to_string(),
                    required: true,
                    status: HealthStatus::Fail,
                    detail: "stdio server has no command".to_string(),
                    remediation: Some("Set mcp.servers[].command.".to_string()),
                });
                continue;
            };
            if !seen.insert(command.to_string()) {
                continue;
            }
            let installed = command_exists(command);
            checks.push(HealthCheck {
                name: format!("MCP command {command}"),
                category: "configured".to_string(),
                required: true,
                status: if installed {
                    HealthStatus::Pass
                } else {
                    HealthStatus::Fail
                },
                detail: if installed {
                    "installed".to_string()
                } else {
                    "not found on PATH".to_string()
                },
                remediation: (!installed).then(|| {
                    format!("Install '{command}' or set an absolute mcp.servers[].command path.")
                }),
            });
        }
        if checks.is_empty() {
            checks.push(HealthCheck {
                name: "MCP stdio commands".to_string(),
                category: "configured".to_string(),
                required: false,
                status: HealthStatus::Pass,
                detail: "no stdio MCP servers configured".to_string(),
                remediation: None,
            });
        }
        checks
    }

    async fn check_browser(&self) -> Vec<HealthCheck> {
        let browser = &self.config.browser;
        if !browser.enabled {
            return vec![HealthCheck {
                name: "agent-browser".to_string(),
                category: "configured".to_string(),
                required: false,
                status: HealthStatus::Pass,
                detail: "browser tool disabled; dependency not required".to_string(),
                remediation: None,
            }];
        }

        let mut checks = Vec::new();
        if !browser.allowed_domains.is_empty() {
            checks.push(HealthCheck {
                name: "persistent browser availability".to_string(),
                category: "configured".to_string(),
                required: false,
                status: HealthStatus::Warning,
                detail: "ordinary transient browsing is available, but persistent Chrome profiles are unavailable while allowed_domains is configured".to_string(),
                remediation: Some(
                    "Keep allowed_domains for contained transient browsing, or clear it only if reusable persistent profiles are required; agent-browser 0.33.0 cannot combine both guarantees.".to_string(),
                ),
            });
        }
        if !command_exists(&browser.command) {
            checks.push(HealthCheck {
                name: "agent-browser CLI".to_string(),
                category: "configured".to_string(),
                required: true,
                status: HealthStatus::Fail,
                detail: format!("'{}' was not found", browser.command),
                remediation: Some(format!(
                    "Run `npm install -g agent-browser@{SUPPORTED_AGENT_BROWSER_VERSION}` (or `cargo install agent-browser --version {SUPPORTED_AGENT_BROWSER_VERSION} --locked`), then `agent-browser install`."
                )),
            });
            return checks;
        }

        let version = command_output(
            &browser.command,
            &["--version"],
            &[],
            Duration::from_secs(5),
        )
        .await;
        match version {
            Ok(version_output) => {
                let version_number = extract_version(&version_output);
                let exact = version_number.as_deref() == Some(SUPPORTED_AGENT_BROWSER_VERSION);
                checks.push(HealthCheck {
                    name: "agent-browser CLI".to_string(),
                    category: "configured".to_string(),
                    required: true,
                    status: if exact {
                        HealthStatus::Pass
                    } else {
                        HealthStatus::Warning
                    },
                    detail: format!(
                        "installed version {}; PicoBot is validated with {}",
                        version_number.unwrap_or_else(|| version_output.trim().to_string()),
                        SUPPORTED_AGENT_BROWSER_VERSION
                    ),
                    remediation: (!exact).then(|| {
                        format!(
                            "Install agent-browser@{SUPPORTED_AGENT_BROWSER_VERSION} for the validated CLI contract."
                        )
                    }),
                });
            }
            Err(error) => checks.push(HealthCheck {
                name: "agent-browser CLI".to_string(),
                category: "configured".to_string(),
                required: true,
                status: HealthStatus::Fail,
                detail: error,
                remediation: Some("Reinstall agent-browser and verify it can execute.".to_string()),
            }),
        }

        if let Some(path) = browser.browser_executable_path.as_deref() {
            let path = expand_path(path);
            let path = if path.is_absolute() {
                path
            } else {
                expand_path(&self.config.workspace_dir).join(path)
            };
            let installed = executable_file(&path);
            checks.push(HealthCheck {
                name: "configured browser executable".to_string(),
                category: "configured".to_string(),
                required: true,
                status: if installed {
                    HealthStatus::Pass
                } else {
                    HealthStatus::Fail
                },
                detail: if installed {
                    format!("executable file found at {}", path.display())
                } else {
                    format!("missing or not executable at {}", path.display())
                },
                remediation: (!installed).then(|| {
                    "Fix browser.browser_executable_path and its execute permissions, or run `agent-browser install`.".to_string()
                }),
            });
        }

        let doctor_executable = browser
            .browser_executable_path
            .as_deref()
            .map(expand_path)
            .map(|path| {
                if path.is_absolute() {
                    path
                } else {
                    expand_path(&self.config.workspace_dir).join(path)
                }
            })
            .map(|path| path.to_string_lossy().into_owned());
        let doctor_executable_env = doctor_executable
            .as_deref()
            .map(|path| ("AGENT_BROWSER_EXECUTABLE_PATH", path));
        let doctor_socket_dir = match tempfile::Builder::new()
            .prefix("picobot-health-agent-browser-")
            .tempdir()
        {
            Ok(directory) => directory,
            Err(error) => {
                checks.push(agent_browser_doctor_failure(format!(
                    "failed to create isolated doctor directory: {error}"
                )));
                return checks;
            }
        };
        let socket_dir = doctor_socket_dir.path().to_string_lossy().into_owned();
        let mut doctor_env = vec![("AGENT_BROWSER_SOCKET_DIR", socket_dir.as_str())];
        if let Some(env) = doctor_executable_env {
            doctor_env.push(env);
        }
        let doctor = capture_command(
            &browser.command,
            &[
                "--namespace",
                "picobot-health",
                "doctor",
                "--offline",
                "--json",
            ],
            &doctor_env,
            Duration::from_secs(30),
        )
        .await;
        match doctor {
            Ok(output) => match parse_agent_browser_doctor(&output) {
                Ok(report) => checks.extend(agent_browser_doctor_checks(&report)),
                Err(error) => checks.push(agent_browser_doctor_failure(error)),
            },
            Err(error) => checks.push(agent_browser_doctor_failure(error)),
        }
        checks
    }
}

fn scheduler_health_check(
    name: &str,
    findings: Vec<String>,
    healthy_detail: &str,
    remediation: &str,
) -> HealthCheck {
    if findings.is_empty() {
        HealthCheck {
            name: name.to_string(),
            category: "runtime".to_string(),
            required: false,
            status: HealthStatus::Pass,
            detail: healthy_detail.to_string(),
            remediation: None,
        }
    } else {
        let total = findings.len();
        let mut sample = findings.into_iter().take(5).collect::<Vec<_>>().join(", ");
        if total > 5 {
            sample.push_str(&format!(", and {} more", total - 5));
        }
        HealthCheck {
            name: name.to_string(),
            category: "runtime".to_string(),
            required: false,
            status: HealthStatus::Warning,
            detail: format!("{total} finding(s): {sample}"),
            remediation: Some(remediation.to_string()),
        }
    }
}

fn check_configuration_recovery(config: &Config) -> HealthCheck {
    if config.diagnostics.is_empty() {
        return HealthCheck {
            name: "configuration compatibility".to_string(),
            category: "core".to_string(),
            required: false,
            status: HealthStatus::Pass,
            detail: "no recoverable configuration problems detected".to_string(),
            remediation: None,
        };
    }
    let paths = config
        .diagnostics
        .iter()
        .take(5)
        .map(|diagnostic| diagnostic.path.as_str())
        .collect::<Vec<_>>()
        .join(", ");
    HealthCheck {
        name: "configuration compatibility".to_string(),
        category: "core".to_string(),
        required: false,
        status: HealthStatus::Warning,
        detail: format!(
            "ignored {} recoverable configuration item(s): {paths}",
            config.diagnostics.len()
        ),
        remediation: Some(
            "Review and clean invalid entries in WebUI Settings → config.json.".to_string(),
        ),
    }
}

fn check_workspace(config: &Config) -> HealthCheck {
    let workspace = expand_path(&config.workspace_dir);
    let exists = workspace.is_dir();
    HealthCheck {
        name: "workspace".to_string(),
        category: "core".to_string(),
        required: true,
        status: if exists {
            HealthStatus::Pass
        } else {
            HealthStatus::Fail
        },
        detail: if exists {
            format!("{} is available", workspace.display())
        } else {
            format!("{} does not exist", workspace.display())
        },
        remediation: (!exists)
            .then(|| "Create the configured workspace directory or fix workspace_dir.".to_string()),
    }
}

fn check_required_binary(name: &str, category: &str, remediation: &str) -> HealthCheck {
    let installed = command_exists(name);
    HealthCheck {
        name: name.to_string(),
        category: category.to_string(),
        required: true,
        status: if installed {
            HealthStatus::Pass
        } else {
            HealthStatus::Fail
        },
        detail: if installed {
            "installed".to_string()
        } else {
            "not found on PATH".to_string()
        },
        remediation: (!installed).then(|| remediation.to_string()),
    }
}

fn check_search_backend(name: &str, candidates: &[&str], preferred: &[&str]) -> HealthCheck {
    let found = candidates.iter().copied().find(|name| command_exists(name));
    let (status, detail, remediation) = search_backend_result(found, candidates, preferred);
    HealthCheck {
        name: name.to_string(),
        category: "core".to_string(),
        required: true,
        status,
        detail,
        remediation,
    }
}

fn search_backend_result(
    found: Option<&str>,
    candidates: &[&str],
    preferred: &[&str],
) -> (HealthStatus, String, Option<String>) {
    match found {
        Some(found) if preferred.contains(&found) => (
            HealthStatus::Pass,
            format!("using preferred backend {found}"),
            None,
        ),
        Some(found) => (
            HealthStatus::Warning,
            format!("using fallback backend {found}"),
            Some(format!(
                "Install {} for faster searches.",
                preferred.join(" or ")
            )),
        ),
        None => (
            HealthStatus::Fail,
            "no supported backend found".to_string(),
            Some(format!("Install one of: {}.", candidates.join(", "))),
        ),
    }
}

fn check_optional_binary(name: &str, binary: &str, category: &str) -> HealthCheck {
    let installed = command_exists(binary);
    HealthCheck {
        name: name.to_string(),
        category: category.to_string(),
        required: false,
        status: HealthStatus::Pass,
        detail: if installed {
            format!("{binary} installed")
        } else {
            format!("{binary} not installed; feature remains unavailable")
        },
        remediation: None,
    }
}

fn command_exists(command: &str) -> bool {
    if command.contains(std::path::MAIN_SEPARATOR) {
        executable_file(Path::new(command))
    } else {
        which::which(command).is_ok()
    }
}

fn executable_file(path: &Path) -> bool {
    let Ok(metadata) = path.metadata() else {
        return false;
    };
    if !metadata.is_file() {
        return false;
    }
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        metadata.permissions().mode() & 0o111 != 0
    }
    #[cfg(not(unix))]
    {
        true
    }
}

async fn capture_command(
    command: &str,
    args: &[&str],
    env: &[(&str, &str)],
    timeout: Duration,
) -> Result<Output, String> {
    let mut process = Command::new(command);
    process
        .args(args)
        .envs(env.iter().copied())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .kill_on_drop(true);
    tokio::time::timeout(timeout, process.output())
        .await
        .map_err(|_| format!("command timed out after {} seconds", timeout.as_secs()))?
        .map_err(|error| format!("failed to start: {error}"))
}

async fn command_output(
    command: &str,
    args: &[&str],
    env: &[(&str, &str)],
    timeout: Duration,
) -> Result<String, String> {
    let output = capture_command(command, args, env, timeout).await?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    if !output.status.success() {
        let detail = if stderr.trim().is_empty() {
            stdout.trim()
        } else {
            stderr.trim()
        };
        return Err(format!(
            "exited with {}: {}",
            output.status,
            truncate(detail, 1_000)
        ));
    }
    let combined = if stdout.trim().is_empty() {
        stderr.trim()
    } else {
        stdout.trim()
    };
    Ok(truncate(combined, 4_000))
}

#[derive(Debug, Deserialize)]
struct AgentBrowserDoctorReport {
    #[serde(default)]
    checks: Vec<AgentBrowserDoctorCheck>,
    #[serde(default)]
    success: bool,
}

#[derive(Debug, Deserialize)]
struct AgentBrowserDoctorCheck {
    id: String,
    message: String,
    status: String,
}

fn parse_agent_browser_doctor(output: &Output) -> Result<AgentBrowserDoctorReport, String> {
    const MAX_DOCTOR_OUTPUT_BYTES: usize = 64 * 1024;
    if output.stdout.len() > MAX_DOCTOR_OUTPUT_BYTES {
        return Err(format!(
            "agent-browser doctor returned more than {MAX_DOCTOR_OUTPUT_BYTES} bytes"
        ));
    }
    let stdout = String::from_utf8_lossy(&output.stdout);
    match serde_json::from_str::<AgentBrowserDoctorReport>(stdout.trim()) {
        Ok(report) => Ok(report),
        Err(error) => {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let detail = if stderr.trim().is_empty() {
                stdout.trim()
            } else {
                stderr.trim()
            };
            Err(format!(
                "agent-browser doctor returned invalid JSON ({error}): {}",
                truncate(detail, 1_000)
            ))
        }
    }
}

fn agent_browser_doctor_checks(report: &AgentBrowserDoctorReport) -> Vec<HealthCheck> {
    let mut installation = doctor_named_check(
        report,
        "browser installation",
        |check| check.id == "chrome.installed",
        "Run `agent-browser install`, or configure browser.browser_executable_path.",
    );
    let launch = doctor_named_check(
        report,
        "browser headless launch",
        |check| check.id.starts_with("launch."),
        "Run `agent-browser doctor --debug`, then `agent-browser install --with-deps` on Linux or `agent-browser install` on other platforms.",
    );
    if installation.status == HealthStatus::Fail && launch.status == HealthStatus::Pass {
        installation.status = HealthStatus::Pass;
        installation.detail =
            "headless launch confirmed an available configured or system browser".to_string();
        installation.remediation = None;
    }

    let other_issues = report
        .checks
        .iter()
        .filter(|check| {
            check.id != "chrome.installed"
                && !check.id.starts_with("launch.")
                && matches!(check.status.as_str(), "warn" | "warning" | "fail")
        })
        .collect::<Vec<_>>();
    let unexplained_failure = !report.success
        && !report
            .checks
            .iter()
            .any(|check| check.status.as_str() == "fail");
    let runtime_status = if other_issues
        .iter()
        .any(|check| check.status.as_str() == "fail")
        || unexplained_failure
    {
        HealthStatus::Fail
    } else if other_issues.is_empty() {
        HealthStatus::Pass
    } else {
        HealthStatus::Warning
    };
    let runtime_detail = if other_issues.is_empty() {
        if runtime_status == HealthStatus::Pass {
            "isolated offline doctor completed without environment warnings".to_string()
        } else {
            "doctor reported failure without a structured failing check".to_string()
        }
    } else {
        let details = other_issues
            .iter()
            .take(3)
            .map(|check| format!("{}: {}", check.id, check.message))
            .collect::<Vec<_>>()
            .join("; ");
        truncate(
            &format!("{} environment issue(s): {details}", other_issues.len()),
            1_000,
        )
    };
    let runtime = HealthCheck {
        name: "agent-browser environment".to_string(),
        category: "configured".to_string(),
        required: true,
        status: runtime_status,
        detail: runtime_detail,
        remediation: (runtime_status != HealthStatus::Pass).then(|| {
            "Run `agent-browser doctor --debug` to inspect the reported environment checks."
                .to_string()
        }),
    };

    vec![installation, launch, runtime]
}

fn doctor_named_check(
    report: &AgentBrowserDoctorReport,
    name: &str,
    predicate: impl Fn(&AgentBrowserDoctorCheck) -> bool,
    remediation: &str,
) -> HealthCheck {
    let found = report.checks.iter().find(|check| predicate(check));
    let (status, detail) = match found {
        Some(check) => (doctor_status(&check.status), check.message.clone()),
        None => (
            HealthStatus::Fail,
            format!("agent-browser doctor did not report {name}"),
        ),
    };
    HealthCheck {
        name: name.to_string(),
        category: "configured".to_string(),
        required: true,
        status,
        detail,
        remediation: (status != HealthStatus::Pass).then(|| remediation.to_string()),
    }
}

fn doctor_status(status: &str) -> HealthStatus {
    match status {
        "pass" | "info" => HealthStatus::Pass,
        "warn" | "warning" => HealthStatus::Warning,
        "fail" => HealthStatus::Fail,
        _ => HealthStatus::Warning,
    }
}

fn agent_browser_doctor_failure(detail: String) -> HealthCheck {
    HealthCheck {
        name: "agent-browser runtime".to_string(),
        category: "configured".to_string(),
        required: true,
        status: HealthStatus::Fail,
        detail,
        remediation: Some(
            "Run `agent-browser doctor --debug`, then `agent-browser install --with-deps` on Linux or `agent-browser install` on other platforms."
                .to_string(),
        ),
    }
}

fn extract_version(output: &str) -> Option<String> {
    output
        .split_whitespace()
        .map(|token| {
            token
                .trim_start_matches('v')
                .trim_matches(|c: char| c == ',' || c == ';')
        })
        .find(|token| {
            let mut parts = token.split('.');
            parts.clone().count() >= 3 && parts.all(|part| part.chars().all(|c| c.is_ascii_digit()))
        })
        .map(str::to_string)
}

fn truncate(value: &str, max: usize) -> String {
    if value.len() <= max {
        value.to_string()
    } else {
        format!("{}…", &value[..value.floor_char_boundary(max)])
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn required_failure_makes_report_unhealthy() {
        let report = HealthReport::from_checks(vec![HealthCheck {
            name: "x".into(),
            category: "core".into(),
            required: true,
            status: HealthStatus::Fail,
            detail: "missing".into(),
            remediation: None,
        }]);
        assert_eq!(report.overall, HealthOverall::Unhealthy);
        assert!(!report.is_usable());
    }

    #[test]
    fn report_serializes_the_management_api_contract() {
        let report = HealthReport::from_checks(vec![HealthCheck {
            name: "content search".into(),
            category: "core".into(),
            required: true,
            status: HealthStatus::Warning,
            detail: "using fallback backend".into(),
            remediation: Some("Install rg.".into()),
        }]);

        let value = serde_json::to_value(report).unwrap();
        assert_eq!(value["version"], env!("CARGO_PKG_VERSION"));
        assert_eq!(value["overall"], "degraded");
        assert_eq!(value["checks"][0]["status"], "warning");
        assert_eq!(value["checks"][0]["required"], true);
        assert_eq!(value["checks"][0]["remediation"], "Install rg.");
    }

    #[test]
    fn fdfind_is_a_preferred_file_search_backend() {
        let (status, detail, remediation) =
            search_backend_result(Some("fdfind"), &["fd", "fdfind", "find"], &["fd", "fdfind"]);

        assert_eq!(status, HealthStatus::Pass);
        assert_eq!(detail, "using preferred backend fdfind");
        assert_eq!(remediation, None);

        let (status, _, remediation) =
            search_backend_result(Some("find"), &["fd", "fdfind", "find"], &["fd", "fdfind"]);
        assert_eq!(status, HealthStatus::Warning);
        assert_eq!(
            remediation.as_deref(),
            Some("Install fd or fdfind for faster searches.")
        );
    }

    #[test]
    fn doctor_report_exposes_install_launch_and_environment_checks() {
        let report: AgentBrowserDoctorReport = serde_json::from_value(serde_json::json!({
            "success": true,
            "checks": [
                {"id": "env.version", "message": "CLI version 0.33.0", "status": "pass"},
                {"id": "chrome.installed", "message": "Chromium found", "status": "pass"},
                {"id": "launch.elapsed", "message": "Headless launch in 1.2s", "status": "pass"}
            ]
        }))
        .unwrap();

        let checks = agent_browser_doctor_checks(&report);
        assert_eq!(checks.len(), 3);
        assert_eq!(checks[0].name, "browser installation");
        assert_eq!(checks[0].status, HealthStatus::Pass);
        assert_eq!(checks[1].name, "browser headless launch");
        assert_eq!(checks[1].status, HealthStatus::Pass);
        assert_eq!(checks[2].name, "agent-browser environment");
        assert_eq!(checks[2].status, HealthStatus::Pass);
    }

    #[test]
    fn doctor_launch_failure_is_required_and_actionable() {
        let report: AgentBrowserDoctorReport = serde_json::from_value(serde_json::json!({
            "success": false,
            "checks": [
                {"id": "env.disk_free", "message": "low disk", "status": "warn"},
                {"id": "chrome.installed", "message": "Chromium found", "status": "pass"},
                {"id": "launch.daemon", "message": "shared library missing", "status": "fail"}
            ]
        }))
        .unwrap();

        let checks = agent_browser_doctor_checks(&report);
        assert_eq!(checks[1].status, HealthStatus::Fail);
        assert!(checks[1].required);
        assert!(checks[1].detail.contains("shared library missing"));
        assert!(checks[1].remediation.is_some());
        assert_eq!(checks[2].status, HealthStatus::Warning);
    }

    #[test]
    fn successful_launch_accepts_a_configured_browser_without_bundled_chrome() {
        let report: AgentBrowserDoctorReport = serde_json::from_value(serde_json::json!({
            "success": false,
            "checks": [
                {"id": "chrome.installed", "message": "Chrome for Testing missing", "status": "fail"},
                {"id": "launch.elapsed", "message": "Headless launch in 0.8s", "status": "pass"}
            ]
        }))
        .unwrap();

        let checks = agent_browser_doctor_checks(&report);
        assert_eq!(checks[0].status, HealthStatus::Pass);
        assert!(checks[0].detail.contains("launch confirmed"));
        assert_eq!(checks[1].status, HealthStatus::Pass);
        assert_eq!(checks[2].status, HealthStatus::Pass);
    }

    #[test]
    fn extracts_agent_browser_version() {
        assert_eq!(
            extract_version("agent-browser 0.33.0"),
            Some("0.33.0".to_string())
        );
    }
}
