use crate::core::build_identity::{self, BuildIdentity};
use crate::core::upgrade::{self, InstallMethod};
use semver::Version;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::{Path, PathBuf};
use std::process::Command;

const BREW_FORMULA: &str = "extra-chill/tap/homeboy";

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VersionRelation {
    Current,
    Behind,
    Ahead,
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProbeValue {
    pub available: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HomebrewStatus {
    pub formula: String,
    pub available: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stable_version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub installed_version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SourceCheckoutStatus {
    pub path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub head: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dirty: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SelfStatus {
    pub command: String,
    pub active_binary: String,
    pub active_version: String,
    pub active_build_identity: BuildIdentity,
    pub install_method: InstallMethod,
    pub latest_github_release: ProbeValue,
    pub homebrew: HomebrewStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source_checkout: Option<SourceCheckoutStatus>,
    pub version_relation: VersionRelation,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProbeOutput {
    pub success: bool,
    pub stdout: String,
    pub stderr: String,
}

pub fn collect_status() -> SelfStatus {
    collect_status_with(
        std::env::current_exe().ok(),
        || upgrade::fetch_latest_version(InstallMethod::Homebrew).map_err(|e| e.to_string()),
        run_external,
    )
}

pub fn collect_status_with<F, R>(
    active_binary: Option<PathBuf>,
    fetch_latest_github: F,
    run: R,
) -> SelfStatus
where
    F: Fn() -> Result<String, String>,
    R: Fn(&str, &[&str]) -> Result<ProbeOutput, String>,
{
    let active_binary_string = active_binary
        .as_ref()
        .map(|path| path.to_string_lossy().to_string())
        .unwrap_or_else(|| "unknown".to_string());
    let active_version = upgrade::current_version().to_string();
    let active_build_identity = build_identity::current();
    let install_method = detect_install_method_from_path(active_binary.as_deref());
    let latest_github_release = probe_latest_github(fetch_latest_github);
    let homebrew = probe_homebrew(&run);
    let source_checkout = active_binary
        .as_deref()
        .and_then(|path| probe_source(path, &run));
    let version_relation = relation_to_latest(
        &active_version,
        latest_github_release
            .version
            .as_deref()
            .or(homebrew.stable_version.as_deref()),
    );

    SelfStatus {
        command: "self status".to_string(),
        active_binary: active_binary_string,
        active_version,
        active_build_identity,
        install_method,
        latest_github_release,
        homebrew,
        source_checkout,
        version_relation,
    }
}

fn run_external(command: &str, args: &[&str]) -> Result<ProbeOutput, String> {
    Command::new(command)
        .args(args)
        .output()
        .map(|output| ProbeOutput {
            success: output.status.success(),
            stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
        })
        .map_err(|e| e.to_string())
}

fn detect_install_method_from_path(path: Option<&Path>) -> InstallMethod {
    let Some(path) = path else {
        return InstallMethod::Unknown;
    };
    let raw = path.to_string_lossy();

    if raw.contains("/Homebrew/")
        || raw.contains("/homebrew/")
        || raw.contains("/Cellar/homeboy/")
        || raw.contains("/.linuxbrew/")
    {
        return InstallMethod::Homebrew;
    }
    let secondary_bin = format!(
        "/.{}/bin/",
        crate::core::defaults::secondary_install_method_key()
    );
    if raw.contains(&secondary_bin) {
        return InstallMethod::Secondary;
    }
    if raw.contains("/target/debug/")
        || raw.contains("/target/release/")
        || raw.contains("homeboy@")
    {
        return InstallMethod::Source;
    }

    InstallMethod::Binary
}

fn probe_latest_github<F>(fetch_latest_github: F) -> ProbeValue
where
    F: Fn() -> Result<String, String>,
{
    match fetch_latest_github() {
        Ok(version) => ProbeValue {
            available: true,
            version: Some(version),
            error: None,
        },
        Err(error) => ProbeValue {
            available: false,
            version: None,
            error: Some(error),
        },
    }
}

fn probe_homebrew<R>(run: &R) -> HomebrewStatus
where
    R: Fn(&str, &[&str]) -> Result<ProbeOutput, String>,
{
    match run("brew", &["info", "--json=v2", BREW_FORMULA]) {
        Ok(output) if output.success => match parse_brew_info(&output.stdout) {
            Ok((stable_version, installed_version)) => HomebrewStatus {
                formula: BREW_FORMULA.to_string(),
                available: true,
                stable_version,
                installed_version,
                error: None,
            },
            Err(error) => HomebrewStatus {
                formula: BREW_FORMULA.to_string(),
                available: false,
                stable_version: None,
                installed_version: None,
                error: Some(error),
            },
        },
        Ok(output) => HomebrewStatus {
            formula: BREW_FORMULA.to_string(),
            available: false,
            stable_version: None,
            installed_version: None,
            error: Some(non_empty(output.stderr).unwrap_or_else(|| "brew info failed".to_string())),
        },
        Err(error) => HomebrewStatus {
            formula: BREW_FORMULA.to_string(),
            available: false,
            stable_version: None,
            installed_version: None,
            error: Some(error),
        },
    }
}

fn parse_brew_info(raw: &str) -> Result<(Option<String>, Option<String>), String> {
    let value: Value = serde_json::from_str(raw).map_err(|e| format!("parse brew JSON: {e}"))?;
    let formula = value
        .get("formulae")
        .and_then(Value::as_array)
        .and_then(|items| items.first())
        .ok_or_else(|| "brew info returned no formulae".to_string())?;

    let stable_version = formula
        .pointer("/versions/stable")
        .and_then(Value::as_str)
        .filter(|version| !version.is_empty())
        .map(str::to_string);
    let installed_version = formula
        .get("installed")
        .and_then(Value::as_array)
        .and_then(|items| items.first())
        .and_then(|item| item.get("version"))
        .and_then(Value::as_str)
        .filter(|version| !version.is_empty())
        .map(str::to_string);

    Ok((stable_version, installed_version))
}

fn probe_source<R>(binary: &Path, run: &R) -> Option<SourceCheckoutStatus>
where
    R: Fn(&str, &[&str]) -> Result<ProbeOutput, String>,
{
    let checkout = find_source_checkout(binary)?;
    let checkout_string = checkout.to_string_lossy().to_string();
    let branch = git_probe(&checkout, &["branch", "--show-current"], run);
    let head = git_probe(&checkout, &["rev-parse", "--short", "HEAD"], run);
    let dirty = match git_probe(&checkout, &["status", "--porcelain"], run) {
        Ok(output) => Some(!output.is_empty()),
        Err(_) => None,
    };

    let error = branch
        .as_ref()
        .err()
        .or_else(|| head.as_ref().err())
        .map(|e| e.to_string());

    Some(SourceCheckoutStatus {
        path: checkout_string,
        branch: branch.ok().and_then(non_empty),
        head: head.ok().and_then(non_empty),
        dirty,
        error,
    })
}

fn find_source_checkout(binary: &Path) -> Option<PathBuf> {
    for ancestor in binary.ancestors() {
        if ancestor.file_name().and_then(|name| name.to_str()) == Some("target") {
            return ancestor.parent().map(Path::to_path_buf);
        }
        let package_manifest = ["Car", "go.toml"].concat();
        if ancestor.join(package_manifest).is_file() && ancestor.join("src/main.rs").is_file() {
            return Some(ancestor.to_path_buf());
        }
    }
    None
}

fn git_probe<R>(checkout: &Path, args: &[&str], run: &R) -> Result<String, String>
where
    R: Fn(&str, &[&str]) -> Result<ProbeOutput, String>,
{
    let checkout_arg = checkout.to_string_lossy();
    let mut full_args = vec!["-C", checkout_arg.as_ref()];
    full_args.extend_from_slice(args);
    match run("git", &full_args) {
        Ok(output) if output.success => Ok(output.stdout),
        Ok(output) => {
            Err(non_empty(output.stderr).unwrap_or_else(|| "git command failed".to_string()))
        }
        Err(error) => Err(error),
    }
}

fn relation_to_latest(active: &str, latest: Option<&str>) -> VersionRelation {
    let Some(latest) = latest else {
        return VersionRelation::Unknown;
    };

    let active = normalize_version(active);
    let latest = normalize_version(latest);
    match (Version::parse(&active), Version::parse(&latest)) {
        (Ok(active), Ok(latest)) if active == latest => VersionRelation::Current,
        (Ok(active), Ok(latest)) if active < latest => VersionRelation::Behind,
        (Ok(active), Ok(latest)) if active > latest => VersionRelation::Ahead,
        _ => VersionRelation::Unknown,
    }
}

fn normalize_version(version: &str) -> String {
    version.trim().trim_start_matches('v').to_string()
}

fn non_empty(value: String) -> Option<String> {
    let value = value.trim().to_string();
    if value.is_empty() {
        None
    } else {
        Some(value)
    }
}

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

    fn output(stdout: &str) -> ProbeOutput {
        ProbeOutput {
            success: true,
            stdout: stdout.to_string(),
            stderr: String::new(),
        }
    }

    #[test]
    fn status_collects_active_binary_versions_and_brew_probe() {
        let mut probes = HashMap::new();
        probes.insert(
            "brew info --json=v2 extra-chill/tap/homeboy".to_string(),
            output(r#"{"formulae":[{"versions":{"stable":"0.114.2"},"installed":[{"version":"0.114.1"}]}]}"#),
        );

        let status = collect_status_with(
            Some(PathBuf::from("/opt/homebrew/bin/homeboy")),
            || Ok("0.114.2".to_string()),
            |cmd, args| {
                probes
                    .get(&format!("{} {}", cmd, args.join(" ")))
                    .cloned()
                    .ok_or_else(|| "missing probe".to_string())
            },
        );

        assert_eq!(status.command, "self status");
        assert_eq!(status.active_binary, "/opt/homebrew/bin/homeboy");
        assert_eq!(status.active_version, upgrade::current_version());
        assert_eq!(
            status.active_build_identity.version,
            upgrade::current_version()
        );
        assert!(status.active_build_identity.display.starts_with("homeboy "));
        assert_eq!(status.install_method, InstallMethod::Homebrew);
        assert_eq!(
            status.latest_github_release.version.as_deref(),
            Some("0.114.2")
        );
        assert_eq!(status.homebrew.stable_version.as_deref(), Some("0.114.2"));
        assert_eq!(
            status.homebrew.installed_version.as_deref(),
            Some("0.114.1")
        );
    }

    #[test]
    fn external_probe_failures_do_not_fail_status_collection() {
        let status = collect_status_with(
            Some(PathBuf::from(format!(
                "/Users/test/.{}/bin/homeboy",
                crate::core::defaults::secondary_install_method_key()
            ))),
            || Err("offline".to_string()),
            |_cmd, _args| Err("not installed".to_string()),
        );

        assert_eq!(status.install_method, InstallMethod::Secondary);
        assert!(!status.latest_github_release.available);
        assert_eq!(
            status.latest_github_release.error.as_deref(),
            Some("offline")
        );
        assert!(!status.homebrew.available);
        assert_eq!(status.homebrew.error.as_deref(), Some("not installed"));
        assert_eq!(status.version_relation, VersionRelation::Unknown);
    }

    #[test]
    fn parses_brew_info_json_shape() {
        let (stable, installed) = parse_brew_info(
            r#"{"formulae":[{"versions":{"stable":"0.114.2"},"installed":[{"version":"0.114.1"}]}]}"#,
        )
        .unwrap();

        assert_eq!(stable.as_deref(), Some("0.114.2"));
        assert_eq!(installed.as_deref(), Some("0.114.1"));
    }

    #[test]
    fn compares_versions_with_v_prefixes() {
        assert_eq!(
            relation_to_latest("0.114.1", Some("v0.114.2")),
            VersionRelation::Behind
        );
        assert_eq!(
            relation_to_latest("0.114.2", Some("0.114.2")),
            VersionRelation::Current
        );
        assert_eq!(
            relation_to_latest("0.114.3", Some("0.114.2")),
            VersionRelation::Ahead
        );
        assert_eq!(
            relation_to_latest("0.114.2", None),
            VersionRelation::Unknown
        );
    }

    #[test]
    fn json_shape_keeps_failed_probes_structured() {
        let status = collect_status_with(
            Some(PathBuf::from("/tmp/homeboy")),
            || Err("github unavailable".to_string()),
            |_cmd, _args| Err("brew unavailable".to_string()),
        );
        let json = serde_json::to_value(status).unwrap();

        assert_eq!(json["command"], "self status");
        assert_eq!(json["active_binary"], "/tmp/homeboy");
        assert_eq!(
            json["active_build_identity"]["version"],
            upgrade::current_version()
        );
        assert_eq!(json["latest_github_release"]["available"], false);
        assert_eq!(json["latest_github_release"]["error"], "github unavailable");
        assert_eq!(json["homebrew"]["available"], false);
        assert_eq!(json["homebrew"]["error"], "brew unavailable");
        assert_eq!(json["version_relation"], "unknown");
    }

    #[test]
    fn test_collect_status() {
        let status = collect_status_with(
            None,
            || Ok(upgrade::current_version().to_string()),
            |_cmd, _args| Err("external probe skipped".to_string()),
        );

        assert_eq!(status.active_binary, "unknown");
        assert_eq!(status.active_version, upgrade::current_version());
        assert_eq!(status.install_method, InstallMethod::Unknown);
        assert_eq!(status.version_relation, VersionRelation::Current);
    }
}
