//! Deep Tool Diagnosis Command (PRD-027)
//!
//! Provides comprehensive diagnosis for specific tools including:
//! - Installation status and location
//! - Binary analysis (type, permissions, symlinks)
//! - Dependency verification
//! - Configuration examination
//! - Network connectivity tests
//! - Health checks
//! - Automated fix suggestions
//!
//! ## Usage
//!
//! ```bash
//! jarvy diagnose docker          # Diagnose docker installation
//! jarvy diagnose node --fix      # Diagnose and attempt fixes
//! jarvy diagnose git --export    # Export diagnostic bundle
//! ```

use crate::observability::Sanitizer;
use crate::tools::registry::get_tool;
use crate::tools::spec::{ToolSpec, get_tool_spec};
use serde::Serialize;
use std::path::PathBuf;
use std::process::Command;

/// Diagnostic report for a tool
#[derive(Debug, Serialize)]
pub struct DiagnosticReport {
    /// Tool name
    pub tool: String,
    /// Installation status
    pub installation: InstallationStatus,
    /// Binary analysis
    pub binary: Option<BinaryAnalysis>,
    /// Dependencies
    pub dependencies: Vec<DependencyStatus>,
    /// Configuration files found
    pub config_files: Vec<ConfigFile>,
    /// Health check results
    pub health_checks: Vec<HealthCheck>,
    /// Issues found
    pub issues: Vec<Issue>,
    /// Suggested fixes
    pub fixes: Vec<Fix>,
}

/// Installation status
#[derive(Debug, Serialize)]
pub struct InstallationStatus {
    /// Whether the tool is installed
    pub installed: bool,
    /// Version if installed
    pub version: Option<String>,
    /// Install location
    pub location: Option<String>,
    /// Install method (homebrew, apt, manual, etc.)
    pub method: Option<String>,
}

/// Binary analysis
#[derive(Debug, Serialize)]
pub struct BinaryAnalysis {
    /// File type (e.g., "Mach-O 64-bit executable arm64")
    pub file_type: String,
    /// Permissions (e.g., "-rwxr-xr-x")
    pub permissions: String,
    /// Owner
    pub owner: String,
    /// Symlink target if applicable
    pub symlink_target: Option<String>,
    /// Size in bytes
    pub size: u64,
}

/// Dependency status
#[derive(Debug, Serialize)]
pub struct DependencyStatus {
    /// Dependency name
    pub name: String,
    /// Whether it's available
    pub available: bool,
    /// Details
    pub details: Option<String>,
}

/// Configuration file
#[derive(Debug, Serialize)]
pub struct ConfigFile {
    /// File path
    pub path: String,
    /// Whether it exists
    pub exists: bool,
    /// File size if exists
    pub size: Option<u64>,
}

/// Health check result
#[derive(Debug, Serialize)]
pub struct HealthCheck {
    /// Check name
    pub name: String,
    /// Whether it passed
    pub passed: bool,
    /// Details or error message
    pub details: Option<String>,
}

/// Issue found during diagnosis
#[derive(Debug, Serialize)]
pub struct Issue {
    /// Issue severity
    pub severity: IssueSeverity,
    /// Issue description
    pub description: String,
    /// Suggested fix ID
    pub fix_id: Option<String>,
}

/// Issue severity levels
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum IssueSeverity {
    Error,
    Warning,
    #[allow(dead_code)] // Reserved for informational diagnostics
    Info,
}

/// Suggested fix
#[derive(Debug, Serialize)]
pub struct Fix {
    /// Fix identifier
    pub id: String,
    /// Description of the fix
    pub description: String,
    /// Command to run
    pub command: Option<String>,
    /// Whether it can be auto-applied
    pub auto_applicable: bool,
}

/// Run the diagnose command
pub fn run_diagnose(tool: &str, fix: bool, export: bool, _scope: &str, output_format: &str) -> i32 {
    // Check if tool exists in registry - spec is required for diagnosis
    let tool_spec = match get_tool_spec(tool) {
        Some(spec) => spec,
        None => {
            // Tool not found in spec registry
            if get_tool(tool).is_some() {
                eprintln!("Tool '{}' is registered but has no diagnostic spec.", tool);
                eprintln!("Only tools with full spec definitions can be diagnosed.");
            } else {
                eprintln!(
                    "Unknown tool: '{}'. Run 'jarvy tools' to see available tools.",
                    tool
                );
            }
            return 1;
        }
    };

    println!("Diagnosing: {}", tool);
    println!("{}", "=".repeat(50));
    println!();

    // Generate diagnostic report
    let report = diagnose_tool(tool, tool_spec);

    // Output report
    if output_format == "json" {
        match serde_json::to_string_pretty(&report) {
            Ok(json) => println!("{}", json),
            Err(e) => eprintln!("Failed to serialize report: {}", e),
        }
    } else {
        print_diagnostic_report(&report);
    }

    // Handle export
    if export {
        let filename = format!("jarvy-diagnose-{}-{}.json", tool, chrono_timestamp());
        let sanitizer = Sanitizer::new();
        let json = serde_json::to_string_pretty(&report).unwrap_or_default();
        let sanitized = sanitizer.sanitize(&json);

        match std::fs::write(&filename, sanitized) {
            Ok(_) => println!("\nDiagnostic export saved to: {}", filename),
            Err(e) => eprintln!("\nFailed to export: {}", e),
        }
    }

    // Handle fix
    if fix && !report.fixes.is_empty() {
        println!("\nApplying fixes...");
        let mut had_failure = false;
        for fix_item in &report.fixes {
            if fix_item.auto_applicable {
                if let Some(ref cmd) = fix_item.command {
                    println!("  Running: {}", cmd);
                    match execute_fix_command(cmd) {
                        Ok(()) => {
                            println!("    ok");
                        }
                        Err(e) => {
                            had_failure = true;
                            eprintln!("    failed: {e}");
                        }
                    }
                }
            } else {
                println!("  Manual fix required: {}", fix_item.description);
            }
        }
        if had_failure {
            return 1;
        }
    }

    0
}

/// Execute a suggested-fix shell command via `sh -c` and surface the
/// exit status. Commands come from `Fix::command` which is built from
/// the tool's own `ToolSpec` — Jarvy's first-party data — NOT from
/// any remote / user-supplied source. That trust posture is what
/// makes this safe to execute without an additional gate; the same
/// commands would be printed verbatim today for the user to copy.
fn execute_fix_command(cmd: &str) -> Result<(), String> {
    let status = std::process::Command::new("sh")
        .arg("-c")
        .arg(cmd)
        .status()
        .map_err(|e| e.to_string())?;
    if !status.success() {
        return Err(format!("exit {}", status.code().unwrap_or(-1)));
    }
    Ok(())
}

/// Generate diagnostic report for a tool
fn diagnose_tool(tool_name: &str, spec: &ToolSpec) -> DiagnosticReport {
    let mut issues = Vec::new();
    let mut fixes = Vec::new();

    // Check installation status
    let installation = check_installation(tool_name, spec);

    // Binary analysis (if installed)
    let binary = if installation.installed {
        installation
            .location
            .as_ref()
            .and_then(|loc| analyze_binary(loc).ok())
    } else {
        issues.push(Issue {
            severity: IssueSeverity::Error,
            description: format!("{} is not installed", tool_name),
            fix_id: Some("install".to_string()),
        });
        fixes.push(Fix {
            id: "install".to_string(),
            description: format!("Install {} using Jarvy", tool_name),
            command: Some(format!("jarvy setup --only {}", tool_name)),
            auto_applicable: false,
        });
        None
    };

    // Check dependencies
    let dependencies = check_dependencies(tool_name, spec);

    // Find configuration files
    let config_files = find_config_files(tool_name);

    // Run health checks
    let health_checks = run_health_checks(tool_name, spec, &installation);

    // Check for PATH issues
    if installation.installed {
        if let Some(ref loc) = installation.location {
            let path_issues = check_path_issues(loc);
            issues.extend(path_issues);
        }
    }

    // Add fixes for health check failures
    for check in &health_checks {
        if !check.passed {
            issues.push(Issue {
                severity: IssueSeverity::Warning,
                description: format!(
                    "Health check '{}' failed: {}",
                    check.name,
                    check.details.as_deref().unwrap_or("unknown")
                ),
                fix_id: None,
            });
        }
    }

    DiagnosticReport {
        tool: tool_name.to_string(),
        installation,
        binary,
        dependencies,
        config_files,
        health_checks,
        issues,
        fixes,
    }
}

/// Check installation status
fn check_installation(_tool_name: &str, spec: &ToolSpec) -> InstallationStatus {
    let command = spec.command;

    // Try to find the binary
    let which_output = Command::new("which").arg(command).output();

    let location = which_output.ok().and_then(|o| {
        if o.status.success() {
            String::from_utf8(o.stdout)
                .ok()
                .map(|s| s.trim().to_string())
        } else {
            None
        }
    });

    let installed = location.is_some();

    // Get version (use standard --version flag)
    let version = if installed {
        get_tool_version(command, "--version")
    } else {
        None
    };

    // Detect install method
    let method = if installed {
        detect_install_method(location.as_deref())
    } else {
        None
    };

    InstallationStatus {
        installed,
        version,
        location,
        method,
    }
}

/// Get tool version
fn get_tool_version(command: &str, version_arg: &str) -> Option<String> {
    let output = Command::new(command).arg(version_arg).output().ok()?;

    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        let combined = format!("{}{}", stdout, stderr);

        // Extract version number using regex
        let re = regex::Regex::new(r"(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)").ok()?;
        re.captures(&combined)
            .and_then(|c| c.get(1))
            .map(|m| m.as_str().to_string())
    } else {
        None
    }
}

/// Detect how a tool was installed. Delegates to the canonical
/// classifier in `tools::install_method` (round-2 maint F1).
///
/// Two label remappings preserve diagnose's user-facing wire format:
/// `Brew` → `"homebrew"` (diagnose has historically used the longer
/// name) and `Unknown` → `"manual"`.
fn detect_install_method(location: Option<&str>) -> Option<String> {
    use crate::tools::install_method::{InstallMethod, detect_install_method_from_path};
    let loc = location?;
    let method = detect_install_method_from_path(std::path::Path::new(loc));
    Some(match method {
        InstallMethod::Brew => "homebrew".to_string(),
        InstallMethod::Unknown => "manual".to_string(),
        other => other.to_string(),
    })
}

/// Analyze a binary file (Unix). Reads POSIX mode/uid/gid from filesystem
/// metadata, which is unavailable on Windows.
#[cfg(unix)]
fn analyze_binary(path: &str) -> Result<BinaryAnalysis, std::io::Error> {
    use std::os::unix::fs::MetadataExt;

    let metadata = std::fs::metadata(path)?;
    let symlink_meta = std::fs::symlink_metadata(path)?;

    // Get file type using `file` command
    let file_type = Command::new("file")
        .arg("-b")
        .arg(path)
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    // Get permissions
    let mode = metadata.mode();
    let permissions = format_permissions(mode);

    // Get owner
    let owner = format!("{}:{}", metadata.uid(), metadata.gid());

    // Check if symlink
    let symlink_target = if symlink_meta.file_type().is_symlink() {
        std::fs::read_link(path)
            .ok()
            .map(|p| p.to_string_lossy().to_string())
    } else {
        None
    };

    Ok(BinaryAnalysis {
        file_type,
        permissions,
        owner,
        symlink_target,
        size: metadata.len(),
    })
}

// Windows stub: POSIX mode/uid/gid don't exist on Windows. The single
// caller in this file uses `.and_then(|loc| analyze_binary(loc).ok())`,
// so an Unsupported error degrades gracefully to `None` on Windows.
#[cfg(not(unix))]
fn analyze_binary(_path: &str) -> Result<BinaryAnalysis, std::io::Error> {
    Err(std::io::Error::new(
        std::io::ErrorKind::Unsupported,
        "binary analysis is unix-only",
    ))
}

/// Format Unix permissions
fn format_permissions(mode: u32) -> String {
    let user = (mode >> 6) & 0o7;
    let group = (mode >> 3) & 0o7;
    let other = mode & 0o7;

    let format_triplet = |bits: u32| -> String {
        format!(
            "{}{}{}",
            if bits & 4 != 0 { 'r' } else { '-' },
            if bits & 2 != 0 { 'w' } else { '-' },
            if bits & 1 != 0 { 'x' } else { '-' }
        )
    };

    format!(
        "-{}{}{}",
        format_triplet(user),
        format_triplet(group),
        format_triplet(other)
    )
}

/// Check dependencies for a tool
fn check_dependencies(tool_name: &str, _spec: &ToolSpec) -> Vec<DependencyStatus> {
    let mut deps = Vec::new();

    // Tool-specific dependency checks
    match tool_name {
        "docker" => {
            // Check Docker daemon
            let daemon_running = Command::new("docker")
                .arg("info")
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false);

            deps.push(DependencyStatus {
                name: "Docker daemon".to_string(),
                available: daemon_running,
                details: if daemon_running {
                    Some("Running".to_string())
                } else {
                    Some("Not running or not accessible".to_string())
                },
            });

            // Check Docker socket
            let socket_exists = std::path::Path::new("/var/run/docker.sock").exists();
            deps.push(DependencyStatus {
                name: "Docker socket".to_string(),
                available: socket_exists,
                details: Some("/var/run/docker.sock".to_string()),
            });
        }
        "node" | "npm" => {
            // Check npm
            let npm_available = Command::new("npm")
                .arg("--version")
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false);

            deps.push(DependencyStatus {
                name: "npm".to_string(),
                available: npm_available,
                details: None,
            });
        }
        "rust" | "cargo" => {
            // Check rustup
            let rustup_available = Command::new("rustup")
                .arg("--version")
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false);

            deps.push(DependencyStatus {
                name: "rustup".to_string(),
                available: rustup_available,
                details: None,
            });
        }
        _ => {}
    }

    deps
}

/// Find configuration files for a tool
fn find_config_files(tool_name: &str) -> Vec<ConfigFile> {
    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"));
    let mut configs = Vec::new();

    // Tool-specific config files
    let paths: Vec<PathBuf> = match tool_name {
        "docker" => vec![
            home.join(".docker/config.json"),
            home.join(".docker/daemon.json"),
        ],
        "git" => vec![home.join(".gitconfig"), home.join(".gitignore_global")],
        "node" | "npm" => vec![home.join(".npmrc"), home.join(".nvmrc")],
        "rust" | "cargo" => vec![
            home.join(".cargo/config.toml"),
            home.join(".cargo/config"),
            home.join(".rustup/settings.toml"),
        ],
        "kubectl" | "kubernetes" => vec![home.join(".kube/config")],
        _ => vec![],
    };

    for path in paths {
        let exists = path.exists();
        let size = if exists {
            std::fs::metadata(&path).ok().map(|m| m.len())
        } else {
            None
        };

        configs.push(ConfigFile {
            path: path.to_string_lossy().to_string(),
            exists,
            size,
        });
    }

    configs
}

/// Run health checks for a tool
fn run_health_checks(
    tool_name: &str,
    spec: &ToolSpec,
    installation: &InstallationStatus,
) -> Vec<HealthCheck> {
    let mut checks = Vec::new();

    if !installation.installed {
        return checks;
    }

    // Basic version check
    checks.push(HealthCheck {
        name: format!("{} --version", spec.command),
        passed: installation.version.is_some(),
        details: installation.version.clone(),
    });

    // Tool-specific health checks
    match tool_name {
        "docker" => {
            // Check docker ps
            let ps_ok = Command::new("docker")
                .args(["ps", "--format", "{{.ID}}"])
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false);

            checks.push(HealthCheck {
                name: "docker ps".to_string(),
                passed: ps_ok,
                details: if ps_ok {
                    None
                } else {
                    Some("Cannot list containers".to_string())
                },
            });
        }
        "git" => {
            // Check git config
            let config_ok = Command::new("git")
                .args(["config", "--get", "user.name"])
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false);

            checks.push(HealthCheck {
                name: "git config user.name".to_string(),
                passed: config_ok,
                details: if config_ok {
                    None
                } else {
                    Some("User name not configured".to_string())
                },
            });
        }
        "node" => {
            // Check node can execute
            let exec_ok = Command::new("node")
                .args(["-e", "console.log('ok')"])
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false);

            checks.push(HealthCheck {
                name: "node execution".to_string(),
                passed: exec_ok,
                details: if exec_ok {
                    None
                } else {
                    Some("Cannot execute Node.js".to_string())
                },
            });
        }
        _ => {}
    }

    checks
}

/// Check for PATH issues
fn check_path_issues(binary_location: &str) -> Vec<Issue> {
    let mut issues = Vec::new();

    // Get directory of binary
    let binary_dir = std::path::Path::new(binary_location)
        .parent()
        .map(|p| p.to_string_lossy().to_string());

    if let Some(dir) = binary_dir {
        // Check if directory is in PATH
        if let Ok(path) = std::env::var("PATH") {
            if !path.split(':').any(|p| p == dir) {
                issues.push(Issue {
                    severity: IssueSeverity::Warning,
                    description: format!("Binary directory '{}' may not be in PATH", dir),
                    fix_id: Some("add-to-path".to_string()),
                });
            }
        }
    }

    issues
}

/// Print diagnostic report in pretty format
fn print_diagnostic_report(report: &DiagnosticReport) {
    // Installation Status
    println!("Installation Status");
    println!("{}", "-".repeat(40));
    println!(
        "Installed: {}",
        if report.installation.installed {
            "Yes"
        } else {
            "No"
        }
    );
    if let Some(ref version) = report.installation.version {
        println!("Version:   {}", version);
    }
    if let Some(ref location) = report.installation.location {
        println!("Location:  {}", location);
    }
    if let Some(ref method) = report.installation.method {
        println!("Method:    {}", method);
    }
    println!();

    // Binary Analysis
    if let Some(ref binary) = report.binary {
        println!("Binary Analysis");
        println!("{}", "-".repeat(40));
        println!("File type:   {}", binary.file_type);
        println!("Permissions: {}", binary.permissions);
        println!("Owner:       {}", binary.owner);
        if let Some(ref target) = binary.symlink_target {
            println!("Symlink:     -> {}", target);
        }
        println!("Size:        {} bytes", binary.size);
        println!();
    }

    // Dependencies
    if !report.dependencies.is_empty() {
        println!("Dependencies");
        println!("{}", "-".repeat(40));
        for dep in &report.dependencies {
            let status = if dep.available {
                "\x1b[32m[OK]\x1b[0m"
            } else {
                "\x1b[31m[MISSING]\x1b[0m"
            };
            print!("{} {}", status, dep.name);
            if let Some(ref details) = dep.details {
                print!(" ({})", details);
            }
            println!();
        }
        println!();
    }

    // Configuration Files
    if !report.config_files.is_empty() {
        println!("Configuration");
        println!("{}", "-".repeat(40));
        for config in &report.config_files {
            let status = if config.exists {
                "\x1b[32m[EXISTS]\x1b[0m"
            } else {
                "\x1b[33m[MISSING]\x1b[0m"
            };
            print!("{} {}", status, config.path);
            if let Some(size) = config.size {
                print!(" ({} bytes)", size);
            }
            println!();
        }
        println!();
    }

    // Health Checks
    if !report.health_checks.is_empty() {
        println!("Health Checks");
        println!("{}", "-".repeat(40));
        for check in &report.health_checks {
            let status = if check.passed {
                "\x1b[32m[PASS]\x1b[0m"
            } else {
                "\x1b[31m[FAIL]\x1b[0m"
            };
            print!("{} {}", status, check.name);
            if let Some(ref details) = check.details {
                print!(" - {}", details);
            }
            println!();
        }
        println!();
    }

    // Issues
    if !report.issues.is_empty() {
        println!("Issues Found");
        println!("{}", "-".repeat(40));
        for issue in &report.issues {
            let icon = match issue.severity {
                IssueSeverity::Error => "\x1b[31m[ERROR]\x1b[0m",
                IssueSeverity::Warning => "\x1b[33m[WARN]\x1b[0m",
                IssueSeverity::Info => "\x1b[34m[INFO]\x1b[0m",
            };
            println!("{} {}", icon, issue.description);
        }
        println!();
    }

    // Fixes
    if !report.fixes.is_empty() {
        println!("Suggested Fixes");
        println!("{}", "-".repeat(40));
        for (i, fix) in report.fixes.iter().enumerate() {
            println!("{}. {}", i + 1, fix.description);
            if let Some(ref cmd) = fix.command {
                println!("   Command: {}", cmd);
            }
        }
        println!();
    }

    // Summary
    let error_count = report
        .issues
        .iter()
        .filter(|i| i.severity == IssueSeverity::Error)
        .count();
    let warning_count = report
        .issues
        .iter()
        .filter(|i| i.severity == IssueSeverity::Warning)
        .count();

    if error_count == 0 && warning_count == 0 {
        println!(
            "\x1b[32mNo issues detected. {} is healthy.\x1b[0m",
            report.tool
        );
    } else {
        println!(
            "Summary: {} error(s), {} warning(s)",
            error_count, warning_count
        );
    }
}

/// Generate a timestamp for filenames
fn chrono_timestamp() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};

    let duration = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();

    let secs = duration.as_secs();
    format!("{}", secs)
}

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

    #[test]
    fn test_format_permissions() {
        assert_eq!(format_permissions(0o755), "-rwxr-xr-x");
        assert_eq!(format_permissions(0o644), "-rw-r--r--");
        assert_eq!(format_permissions(0o700), "-rwx------");
    }

    #[test]
    fn test_detect_install_method() {
        assert_eq!(
            detect_install_method(Some("/opt/homebrew/bin/git")),
            Some("homebrew".to_string())
        );
        assert_eq!(
            detect_install_method(Some("/Users/test/.cargo/bin/rustc")),
            Some("cargo".to_string())
        );
        assert_eq!(
            detect_install_method(Some("/usr/bin/ls")),
            Some("system".to_string())
        );
    }

    #[test]
    fn test_issue_severity_serialization() {
        let issue = Issue {
            severity: IssueSeverity::Error,
            description: "Test".to_string(),
            fix_id: None,
        };
        let json = serde_json::to_string(&issue).unwrap();
        assert!(json.contains("\"severity\":\"error\""));
    }
}
