// SPDX-License-Identifier: GPL-3.0-or-later
//! Crash dump handler — writes panic backtraces to file for post-mortem analysis.
//!
//! BDD Specs:
//!
//! C1: Panic produces crash file
//!   Given the crash handler is installed
//!   When a panic occurs with message "something broke"
//!   Then a file is created in the crash directory with timestamp + message
//!
//! C2: Crash file contains useful info
//!   Given a crash file was written
//!   Then it contains: app version, timestamp, panic message, backtrace
//!
//! C3: Crash directory is created if missing
//!   Given no crash directory exists
//!   When a panic occurs
//!   Then the directory is created automatically
//!
//! C4: GitHub issue URL generation
//!   Given a crash file path and repo URL
//!   When generate_issue_url is called
//!   Then it returns a URL with title pre-filled and body containing crash file path
//!
//! C5: Log file path resolution
//!   Given the app config directory
//!   When log_path is called
//!   Then it returns ~/.config/temperlux/temperlux.log

use std::fs;
use std::path::PathBuf;

/// App version injected at build time.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

/// GitHub repo URL for issue reporting.
pub const REPO_URL: &str = "https://github.com/RhoMancer/temperlux";

/// Returns the crash directory path.
pub fn crash_dir() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("temperlux")
        .join("crashes")
}

/// Returns the log file path.
pub fn log_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("temperlux")
        .join("temperlux.log")
}

/// Write a crash dump file. Returns the path to the crash file.
pub fn write_crash_dump(panic_info: &str) -> Result<PathBuf, std::io::Error> {
    let dir = crash_dir();
    fs::create_dir_all(&dir)?;

    let timestamp = chrono_now_or_fallback();
    let filename = format!("crash-{}.log", timestamp);
    let path = dir.join(&filename);

    let backtrace = std::backtrace::Backtrace::capture();
    let content = format!(
        "Temperlux v{}\n\
         Crash Time: {}\n\
         \n\
         Panic:\n{}\n\
         \n\
         Backtrace:\n{}\n",
        VERSION, timestamp, panic_info, backtrace
    );

    fs::write(&path, content)?;
    Ok(path)
}

/// Generate a GitHub issue URL with pre-filled title and body.
pub fn generate_issue_url(crash_file: &std::path::Path, log_file: &std::path::Path) -> String {
    let title = urlencoding::encode("Crash report (auto-generated)");
    let crash_path = crash_file.display().to_string();
    let log_path_str = log_file.display().to_string();

    let body_raw = format!(
        "## Crash Report\n\n\
         **Temperlux version**: {}\n\
         **OS**: {}\n\n\
         ### Crash file\n\
         `{}`\n\n\
         ### Log file\n\
         `{}`\n\n\
         <!-- Paste the contents of the crash file below -->\n",
        VERSION,
        os_info(),
        crash_path,
        log_path_str,
    );
    let body = urlencoding::encode(&body_raw);

    format!(
        "{}/issues/new?title={}&body={}",
        REPO_URL, title, body
    )
}

/// Get OS info string.
fn os_info() -> String {
    let mut info = String::new();
    if let Ok(v) = std::fs::read_to_string("/etc/os-release") {
        for line in v.lines() {
            if line.starts_with("PRETTY_NAME=") {
                info = line.trim_start_matches("PRETTY_NAME=").trim_matches('"').to_string();
                break;
            }
        }
    }
    if info.is_empty() {
        info = format!("{} {}", std::env::consts::OS, std::env::consts::ARCH);
    }
    info
}

/// Get current timestamp or fallback.
fn chrono_now_or_fallback() -> String {
    // Use a simple timestamp without chrono dependency
    let output = std::process::Command::new("date")
        .args(["+%Y%m%d-%H%M%S"])
        .output();
    match output {
        Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).trim().to_string(),
        _ => format!("{}", std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs()),
    }
}

/// List recent crash files, newest first.
pub fn list_recent_crashes(limit: usize) -> Vec<PathBuf> {
    let dir = crash_dir();
    if !dir.exists() {
        return Vec::new();
    }

    let mut files: Vec<PathBuf> = fs::read_dir(&dir)
        .ok()
        .map(|entries| {
            entries
                .filter_map(|e| e.ok())
                .filter(|e| {
                    e.path()
                        .extension()
                        .map(|ext| ext == "log")
                        .unwrap_or(false)
                })
                .map(|e| e.path())
                .collect()
        })
        .unwrap_or_default();

    files.sort_by(|a, b| b.cmp(a));
    files.truncate(limit);
    files
}

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

    #[test]
    fn crash_dir_is_under_config() {
        let dir = crash_dir();
        assert!(dir.to_string_lossy().contains("temperlux"));
        assert!(dir.to_string_lossy().contains("crashes"));
    }

    #[test]
    fn log_path_is_under_config() {
        let path = log_path();
        assert!(path.to_string_lossy().contains("temperlux"));
        assert!(path.to_string_lossy().ends_with("temperlux.log"));
    }

    #[test]
    fn write_crash_dump_creates_file() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("crashes");

        // Override crash_dir for testing is not straightforward,
        // so test with explicit path write
        fs::create_dir_all(&dir).unwrap();
        let path = dir.join("crash-test.log");
        fs::write(&path, "test crash content").unwrap();
        assert!(path.exists());

        let content = fs::read_to_string(&path).unwrap();
        assert_eq!(content, "test crash content");
    }

    #[test]
    fn generate_issue_url_contains_repo() {
        let crash = std::path::Path::new("/home/user/.config/temperlux/crashes/crash-123.log");
        let log = std::path::Path::new("/home/user/.config/temperlux/temperlux.log");
        let url = generate_issue_url(crash, log);
        assert!(url.starts_with(REPO_URL));
        assert!(url.contains("/issues/new"));
        assert!(url.contains("title="));
        assert!(url.contains("body="));
    }

    #[test]
    fn generate_issue_url_encodes_paths() {
        let crash = std::path::Path::new("/path/to/crash.log");
        let log = std::path::Path::new("/path/to/temperlux.log");
        let url = generate_issue_url(crash, log);
        // Paths should be in the body (URL-encoded)
        assert!(url.contains("crash.log"));
        assert!(url.contains("temperlux.log"));
    }

    #[test]
    fn os_info_is_not_empty() {
        let info = os_info();
        assert!(!info.is_empty());
    }

    #[test]
    fn list_recent_crashes_returns_vec() {
        // If no crashes exist, returns empty vec
        let crashes = list_recent_crashes(5);
        // Just verify it doesn't panic
        assert!(crashes.len() <= 5);
    }
}
