use serde_json::json;

use crate::util::{
    api_request, check_auth_configured, client, exit_error, raw_api_request, resolve_token,
};

fn parse_version_triplet(raw: &str) -> Option<(u64, u64, u64)> {
    let mut parts = raw.trim().split('.');
    let major = parts.next()?.parse().ok()?;
    let minor = parts.next()?.parse().ok()?;
    let patch = parts.next()?.parse().ok()?;
    Some((major, minor, patch))
}

fn is_cli_version_compatible(current: &str, minimum: &str) -> Option<bool> {
    let current = parse_version_triplet(current)?;
    let minimum = parse_version_triplet(minimum)?;
    Some(current >= minimum)
}

pub async fn config(api_url: &str, token: Option<&str>) -> i32 {
    api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/system/config",
        token,
        None,
        &[],
        &[],
        false,
        false,
    )
    .await
}

pub async fn snapshot(api_url: &str, token: Option<&str>) -> i32 {
    api_request(
        api_url,
        reqwest::Method::GET,
        "/v1/projections",
        token,
        None,
        &[],
        &[],
        false,
        false,
    )
    .await
}

pub async fn discover(api_url: &str, endpoints_only: bool) -> i32 {
    let resp = match client()
        .get(format!("{api_url}/api-doc/openapi.json"))
        .send()
        .await
    {
        Ok(r) => r,
        Err(e) => {
            let err = json!({
                "error": "connection_error",
                "message": format!("{e}"),
                "docs_hint": "Is the API server running? Check KURA_API_URL."
            });
            eprintln!("{}", serde_json::to_string_pretty(&err).unwrap());
            return 3;
        }
    };

    if !resp.status().is_success() {
        let status = resp.status().as_u16();
        let body: serde_json::Value = resp.json().await.unwrap_or(json!({"error": "unknown"}));
        eprintln!("{}", serde_json::to_string_pretty(&body).unwrap());
        return if (400..500).contains(&status) { 1 } else { 2 };
    }

    let spec: serde_json::Value = match resp.json().await {
        Ok(v) => v,
        Err(e) => exit_error(&format!("Failed to parse OpenAPI spec: {e}"), None),
    };

    if !endpoints_only {
        println!("{}", serde_json::to_string_pretty(&spec).unwrap());
        return 0;
    }

    // Extract compact endpoint list from OpenAPI paths
    let mut endpoints = Vec::new();

    if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
        for (path, methods) in paths {
            if let Some(methods_obj) = methods.as_object() {
                for (method, details) in methods_obj {
                    // Skip OpenAPI metadata keys
                    if !["get", "post", "put", "delete", "patch", "head", "options"]
                        .contains(&method.as_str())
                    {
                        continue;
                    }

                    let summary = details
                        .get("summary")
                        .and_then(|s| s.as_str())
                        .unwrap_or("");

                    let requires_auth = details
                        .get("security")
                        .map(|s| !s.as_array().map(|a| a.is_empty()).unwrap_or(true))
                        .unwrap_or(false);

                    endpoints.push(json!({
                        "method": method.to_uppercase(),
                        "path": path,
                        "summary": summary,
                        "auth": requires_auth
                    }));
                }
            }
        }
    }

    // Sort by path, then method
    endpoints.sort_by(|a, b| {
        let pa = a["path"].as_str().unwrap_or("");
        let pb = b["path"].as_str().unwrap_or("");
        pa.cmp(pb).then_with(|| {
            let ma = a["method"].as_str().unwrap_or("");
            let mb = b["method"].as_str().unwrap_or("");
            ma.cmp(mb)
        })
    });

    let output = json!({
        "endpoints": endpoints,
        "total": endpoints.len(),
        "base_url": api_url
    });

    println!("{}", serde_json::to_string_pretty(&output).unwrap());
    0
}

pub async fn doctor(api_url: &str) -> i32 {
    let mut checks: Vec<serde_json::Value> = Vec::new();
    let mut overall = "ok";

    // 1. API reachable
    match raw_api_request(api_url, reqwest::Method::GET, "/health", None).await {
        Ok((status, body)) if status == 200 => {
            let version = body
                .get("version")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            checks.push(json!({
                "name": "api_reachable",
                "status": "ok",
                "detail": format!("{api_url} (v{version})")
            }));
        }
        Ok((status, _)) => {
            overall = "error";
            checks.push(json!({
                "name": "api_reachable",
                "status": "error",
                "detail": format!("{api_url} returned HTTP {status}")
            }));
        }
        Err(e) => {
            overall = "error";
            checks.push(json!({
                "name": "api_reachable",
                "status": "error",
                "detail": format!("{api_url}: {e}")
            }));
            // No point continuing if API is unreachable
            let output = json!({ "checks": checks, "overall": overall });
            eprintln!("{}", serde_json::to_string_pretty(&output).unwrap());
            return 1;
        }
    }

    // 2. Auth configured
    match check_auth_configured() {
        Some((method, detail)) => {
            checks.push(json!({
                "name": "auth_configured",
                "status": "ok",
                "detail": format!("{method}: {detail}")
            }));
        }
        None => {
            if overall == "ok" {
                overall = "warn";
            }
            checks.push(json!({
                "name": "auth_configured",
                "status": "warn",
                "detail": "No credentials found. Run `kura login` or set KURA_API_KEY."
            }));
        }
    }

    // 3. Auth valid (only if configured)
    let token = match resolve_token(api_url).await {
        Ok(t) => {
            // Try an authenticated request
            match raw_api_request(
                api_url,
                reqwest::Method::GET,
                "/v1/projections/user_profile/me",
                Some(&t),
            )
            .await
            {
                Ok((status, body)) if (200..=404).contains(&status) => {
                    // 200 = has profile, 404 = new user (both mean auth works)
                    let user_hint = if status == 200 {
                        body.get("user_id")
                            .and_then(|u| u.as_str())
                            .map(|u| format!("user_id: {u}"))
                            .unwrap_or_else(|| "authenticated".to_string())
                    } else {
                        "authenticated (new user, no profile yet)".to_string()
                    };
                    checks.push(json!({
                        "name": "auth_valid",
                        "status": "ok",
                        "detail": user_hint
                    }));
                }
                Ok((401, _)) => {
                    if overall == "ok" {
                        overall = "warn";
                    }
                    checks.push(json!({
                        "name": "auth_valid",
                        "status": "error",
                        "detail": "Token rejected (HTTP 401). Run `kura login` again."
                    }));
                }
                Ok((status, _)) => {
                    if overall == "ok" {
                        overall = "warn";
                    }
                    checks.push(json!({
                        "name": "auth_valid",
                        "status": "warn",
                        "detail": format!("Unexpected HTTP {status} on auth check")
                    }));
                }
                Err(e) => {
                    if overall == "ok" {
                        overall = "warn";
                    }
                    checks.push(json!({
                        "name": "auth_valid",
                        "status": "error",
                        "detail": format!("Auth check request failed: {e}")
                    }));
                }
            }
            Some(t)
        }
        Err(e) => {
            if overall == "ok" {
                overall = "warn";
            }
            checks.push(json!({
                "name": "auth_valid",
                "status": "warn",
                "detail": format!("{e}")
            }));
            None
        }
    };

    // 4. System config (workers write this at startup — empty = worker never ran)
    if let Some(ref t) = token {
        match raw_api_request(
            api_url,
            reqwest::Method::GET,
            "/v1/agent/capabilities",
            Some(t),
        )
        .await
        {
            Ok((200, body)) => {
                let preferred_read = body
                    .get("preferred_read_endpoint")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown");
                let preferred_write = body
                    .get("preferred_write_endpoint")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown");
                let protocol_version = body
                    .get("protocol_version")
                    .and_then(|v| v.as_str())
                    .unwrap_or("unknown");
                let min_cli = body
                    .get("min_cli_version")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                let current_cli = env!("CARGO_PKG_VERSION");

                let compat_status = match is_cli_version_compatible(current_cli, min_cli) {
                    Some(true) => "ok",
                    Some(false) => {
                        if overall == "ok" {
                            overall = "warn";
                        }
                        "warn"
                    }
                    None => "ok",
                };
                let compat_hint = match is_cli_version_compatible(current_cli, min_cli) {
                    Some(true) => "compatible".to_string(),
                    Some(false) => format!("upgrade cli (current={current_cli}, min={min_cli})"),
                    None => "version comparison unavailable".to_string(),
                };
                checks.push(json!({
                    "name": "agent_capabilities",
                    "status": compat_status,
                    "detail": format!("protocol={protocol_version}, read={preferred_read}, write={preferred_write}, cli={compat_hint}")
                }));
            }
            Ok((404, _)) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "agent_capabilities",
                    "status": "warn",
                    "detail": "Not found (HTTP 404). API is older than capability-manifest contract."
                }));
            }
            Ok((status, _)) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "agent_capabilities",
                    "status": "warn",
                    "detail": format!("HTTP {status} while checking /v1/agent/capabilities")
                }));
            }
            Err(e) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "agent_capabilities",
                    "status": "error",
                    "detail": format!("{e}")
                }));
            }
        }
    }

    // 5. System config (workers write this at startup — empty = worker never ran)
    if let Some(ref t) = token {
        match raw_api_request(api_url, reqwest::Method::GET, "/v1/system/config", Some(t)).await {
            Ok((200, body)) => {
                let data = body.get("data").unwrap_or(&body);
                let dimensions = data
                    .get("dimensions")
                    .and_then(|d| d.as_object())
                    .map(|d| d.len())
                    .unwrap_or(0);
                let conventions = data
                    .get("event_conventions")
                    .and_then(|c| c.as_object())
                    .map(|c| c.len())
                    .unwrap_or(0);

                if dimensions > 0 {
                    checks.push(json!({
                        "name": "system_config",
                        "status": "ok",
                        "detail": format!("{dimensions} dimensions, {conventions} event conventions")
                    }));
                } else {
                    if overall == "ok" {
                        overall = "warn";
                    }
                    checks.push(json!({
                        "name": "system_config",
                        "status": "warn",
                        "detail": "Config exists but has no dimensions. Worker may not have started."
                    }));
                }
            }
            Ok((404, _)) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "system_config",
                    "status": "warn",
                    "detail": "Not found. Worker has not started yet (worker writes system_config at startup)."
                }));
            }
            Ok((status, _)) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "system_config",
                    "status": "warn",
                    "detail": format!("HTTP {status}")
                }));
            }
            Err(e) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "system_config",
                    "status": "error",
                    "detail": format!("{e}")
                }));
            }
        }
    }

    // 6. Worker active — check if projections exist (workers create them)
    if let Some(ref t) = token {
        match raw_api_request(api_url, reqwest::Method::GET, "/v1/projections", Some(t)).await {
            Ok((200, body)) => {
                let count = body.as_array().map(|a| a.len()).unwrap_or(0);
                if count > 0 {
                    // Check freshness: find most recent updated_at
                    let newest = body
                        .as_array()
                        .and_then(|arr| {
                            arr.iter()
                                .filter_map(|p| p.get("updated_at").and_then(|u| u.as_str()))
                                .max()
                        })
                        .unwrap_or("unknown");

                    checks.push(json!({
                        "name": "worker_active",
                        "status": "ok",
                        "detail": format!("{count} projections, newest: {newest}")
                    }));
                } else {
                    if overall == "ok" {
                        overall = "warn";
                    }
                    checks.push(json!({
                        "name": "worker_active",
                        "status": "warn",
                        "detail": "No projections found. Worker may not be running or no events logged yet."
                    }));
                }
            }
            Ok((status, _)) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "worker_active",
                    "status": "warn",
                    "detail": format!("HTTP {status} on snapshot check")
                }));
            }
            Err(e) => {
                if overall == "ok" {
                    overall = "warn";
                }
                checks.push(json!({
                    "name": "worker_active",
                    "status": "error",
                    "detail": format!("{e}")
                }));
            }
        }
    }

    let output = json!({
        "checks": checks,
        "overall": overall
    });

    if overall == "ok" {
        println!("{}", serde_json::to_string_pretty(&output).unwrap());
        0
    } else if overall == "warn" {
        println!("{}", serde_json::to_string_pretty(&output).unwrap());
        0
    } else {
        eprintln!("{}", serde_json::to_string_pretty(&output).unwrap());
        1
    }
}

#[cfg(test)]
mod tests {
    use super::{is_cli_version_compatible, parse_version_triplet};

    #[test]
    fn parse_version_triplet_accepts_semver_triplets() {
        assert_eq!(parse_version_triplet("0.1.0"), Some((0, 1, 0)));
        assert_eq!(parse_version_triplet("12.34.56"), Some((12, 34, 56)));
    }

    #[test]
    fn parse_version_triplet_rejects_invalid_values() {
        assert_eq!(parse_version_triplet("0.1"), None);
        assert_eq!(parse_version_triplet("x.y.z"), None);
    }

    #[test]
    fn cli_version_compatibility_compares_triplets() {
        assert_eq!(is_cli_version_compatible("0.2.0", "0.1.9"), Some(true));
        assert_eq!(is_cli_version_compatible("0.1.0", "0.1.0"), Some(true));
        assert_eq!(is_cli_version_compatible("0.1.0", "0.2.0"), Some(false));
    }
}
