use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::time::SystemTime;

const LOG_NAME: &str = "meatshell-debug.log";

pub fn init() -> Option<PathBuf> {
    let (file, path) = match open_log_file() {
        Some(pair) => pair,
        None => {
            install_panic_hook(None);
            init_stderr_tracing();
            tracing::warn!("failed to create {LOG_NAME}; logging to stderr only");
            return None;
        }
    };

    install_panic_hook(Some(path.clone()));

    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,meatshell=debug,winit=error"));
    let log_file = file;
    if tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_ansi(false)
        .with_thread_ids(true)
        .with_writer(move || log_file.try_clone().expect("clone debug log file"))
        .try_init()
        .is_err()
    {
        return Some(path);
    }

    tracing::info!(
        version = env!("CARGO_PKG_VERSION"),
        os = std::env::consts::OS,
        arch = std::env::consts::ARCH,
        "debug logging initialized"
    );

    if log_paths_enabled() {
        tracing::debug!(log = %path.display(), "debug log path");
        if let Ok(cwd) = std::env::current_dir() {
            tracing::debug!(cwd = %cwd.display(), "current working directory");
        }
        if let Ok(exe) = std::env::current_exe() {
            tracing::debug!(exe = %exe.display(), "current executable");
        }
    }

    Some(path)
}

fn init_stderr_tracing() {
    let filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,meatshell=debug,winit=error"));
    let _ = tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_ansi(false)
        .with_thread_ids(true)
        .try_init();
}

fn open_log_file() -> Option<(File, PathBuf)> {
    let mut dirs = Vec::new();
    if let Ok(exe) = std::env::current_exe() {
        if let Some(dir) = exe.parent() {
            dirs.push(dir.to_path_buf());
        }
    }
    if let Ok(cwd) = std::env::current_dir() {
        if !dirs.iter().any(|dir| dir == &cwd) {
            dirs.push(cwd);
        }
    }

    for dir in dirs {
        let path = dir.join(LOG_NAME);
        if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) {
            return Some((file, path));
        }
    }

    None
}

fn install_panic_hook(path: Option<PathBuf>) {
    let previous = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let payload = if let Some(message) = info.payload().downcast_ref::<&str>() {
            *message
        } else if let Some(message) = info.payload().downcast_ref::<String>() {
            message.as_str()
        } else {
            "<non-string panic payload>"
        };
        let location = info
            .location()
            .map(|loc| format!("{}:{}:{}", short_file(loc.file()), loc.line(), loc.column()))
            .unwrap_or_else(|| "<unknown>".to_string());
        let backtrace = if env_flag("MEATSHELL_DEBUG_BACKTRACE") {
            Some(std::backtrace::Backtrace::force_capture().to_string())
        } else {
            None
        };

        if let Some(path) = &path {
            if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
                let _ = writeln!(
                    file,
                    "\n===== panic {:?} =====\nlocation: {}\nmessage: {}",
                    SystemTime::now(),
                    location,
                    payload
                );
                if let Some(backtrace) = &backtrace {
                    let _ = writeln!(file, "backtrace:\n{}", backtrace);
                }
                let _ = writeln!(file);
            }
        }

        if let Some(backtrace) = &backtrace {
            tracing::error!(
                location = %location,
                message = %payload,
                backtrace = %backtrace,
                "panic captured"
            );
        } else {
            tracing::error!(
                location = %location,
                message = %payload,
                "panic captured"
            );
        }
        previous(info);
    }));
}

pub(crate) fn log_paths_enabled() -> bool {
    env_flag("MEATSHELL_DEBUG_PATHS")
}

fn env_flag(name: &str) -> bool {
    matches!(
        std::env::var(name).as_deref(),
        Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES") | Ok("on") | Ok("ON")
    )
}

fn short_file(path: &str) -> &str {
    path.rsplit(['/', '\\']).next().unwrap_or(path)
}
