pub mod conflict;
pub mod executor;
pub mod gpu_allocation;
pub mod info;
pub mod job;
pub mod macros;
pub mod migrations;
pub mod reservation;
pub mod scheduler;

use std::{collections::HashMap, env, path::PathBuf};
pub type UUID = String;

const VERSION_MESSAGE: &str = concat!(
    env!("CARGO_PKG_VERSION"),
    " (",
    env!("VERGEN_BUILD_TIMESTAMP"),
    ")\n",
    "Branch: ",
    env!("VERGEN_GIT_BRANCH"),
    "\nCommit: ",
    env!("VERGEN_GIT_SHA"),
);

pub fn version() -> &'static str {
    use std::sync::OnceLock;
    static VERSION: OnceLock<String> = OnceLock::new();

    VERSION.get_or_init(|| {
        let author = clap::crate_authors!();
        format!(
            "\
{VERSION_MESSAGE}
Authors: {author}"
        )
    })
}

#[derive(Debug, Clone)]
pub struct GPUSlot {
    pub index: u32,
    pub available: bool,
    /// Total GPU memory in MB, if known from NVML.
    pub total_memory_mb: Option<u64>,
    /// Reason why GPU is unavailable (e.g., occupied by non-gflow process)
    pub reason: Option<String>,
}

use nvml_wrapper::Nvml;

pub trait GPU {
    fn get_gpus(nvml: &Nvml) -> HashMap<UUID, GPUSlot>;
}

pub fn get_config_dir() -> anyhow::Result<PathBuf> {
    dirs::config_dir()
        .ok_or_else(|| anyhow::anyhow!("Failed to get config directory"))
        .map(|p| p.join("gflow"))
}

pub fn get_data_dir() -> anyhow::Result<PathBuf> {
    dirs::data_dir()
        .ok_or_else(|| anyhow::anyhow!("Failed to get data directory"))
        .map(|p| p.join("gflow"))
}

pub fn get_runtime_dir() -> anyhow::Result<PathBuf> {
    dirs::runtime_dir()
        .or_else(dirs::cache_dir)
        .ok_or_else(|| anyhow::anyhow!("Failed to get runtime or cache directory"))
        .map(|p| p.join("gflow"))
}

/// Returns the log file path for a job without any side effects.
pub fn get_log_file_path(job_id: u32) -> anyhow::Result<PathBuf> {
    let log_dir = get_data_dir()?.join("logs");
    if !log_dir.exists() {
        std::fs::create_dir_all(&log_dir)?;
    }
    Ok(log_dir.join(format!("{job_id}.log")))
}

/// Returns the log file path for a job, archiving any existing log first.
/// Only call this when starting a new job execution to avoid losing active logs.
pub fn prepare_log_file_path(job_id: u32) -> anyhow::Result<PathBuf> {
    let log_path = get_log_file_path(job_id)?;

    // If log file already exists, archive it to prevent confusion when job IDs are reused
    if log_path.exists() {
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();
        let archived_name = format!("{job_id}.log.old.{timestamp}");
        let archived_path = log_path.parent().unwrap().join(&archived_name);

        if let Err(e) = std::fs::rename(&log_path, &archived_path) {
            tracing::warn!(
                "Failed to archive existing log {:?} to {:?}: {}",
                log_path,
                archived_path,
                e
            );
        } else {
            tracing::info!(
                "Archived existing log for job {} to {}",
                job_id,
                archived_name
            );
        }
    }

    Ok(log_path)
}

pub fn get_daemon_log_file_path() -> anyhow::Result<PathBuf> {
    let log_dir = get_data_dir()?.join("logs");
    if !log_dir.exists() {
        std::fs::create_dir_all(&log_dir)?;
    }
    Ok(log_dir.join("daemon.log"))
}

pub fn get_current_username() -> String {
    env::var("USER")
        .or_else(|_| env::var("USERNAME"))
        .unwrap_or_else(|_| "unknown".to_string())
}
