use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JupiterRuntimeMode {
    Binary,
    Simulated,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct JupiterContext {
    pub mode: JupiterRuntimeMode,
    pub orbital_radius_km: f64,
    pub orbital_period_days: f64,
    pub orbital_angle_deg: f64,
    pub rotation_period_h: f64,
    pub rotation_angle_deg: f64,
    pub axial_tilt_deg: f64,
    pub surface_speed_m_s: f64,
}

static PLANET_BINARY_CACHE: OnceLock<Option<PathBuf>> = OnceLock::new();

pub fn ensure_jupiters_binary_or_simulate() -> JupiterContext {
    if find_planet_executable().is_some() {
        return reference_jupiter_context(JupiterRuntimeMode::Binary);
    }

    simulated_jupiter_context()
}

pub fn reference_jupiter_context(mode: JupiterRuntimeMode) -> JupiterContext {
    JupiterContext {
        mode,
        orbital_radius_km: 778_500_000.0,
        orbital_period_days: 4_332.59,
        orbital_angle_deg: 0.0,
        rotation_period_h: 9.925,
        rotation_angle_deg: 0.0,
        axial_tilt_deg: 3.13,
        surface_speed_m_s: 12_600.0,
    }
}

pub fn simulated_jupiter_context() -> JupiterContext {
    reference_jupiter_context(JupiterRuntimeMode::Simulated)
}

pub fn jupiter_sibling_satellites() -> &'static [&'static str] {
    &["callistos", "europas", "ganymedes", "ios"]
}

fn find_planet_executable() -> Option<PathBuf> {
    PLANET_BINARY_CACHE
        .get_or_init(find_planet_executable_uncached)
        .clone()
}

fn find_planet_executable_uncached() -> Option<PathBuf> {
    let names = binary_names();

    names
        .iter()
        .find_map(|binary_name| {
            find_in_path(binary_name)
                .or_else(|| cargo_bin_dir().map(|directory| directory.join(binary_name)))
                .filter(|candidate| candidate.is_file())
        })
        .or_else(|| find_globally(names))
}

fn binary_names() -> &'static [&'static str] {
    if cfg!(windows) {
        &["jupiters.exe", "jupiter.exe", "jupiters", "jupiter"]
    } else {
        &["jupiters", "jupiter"]
    }
}

fn cargo_bin_dir() -> Option<PathBuf> {
    env::var_os("HOME").map(|home_dir| PathBuf::from(home_dir).join(".cargo/bin"))
}

fn find_in_path(binary_name: &str) -> Option<PathBuf> {
    let path_var = env::var_os("PATH")?;

    env::split_paths(&path_var)
        .map(|directory| directory.join(binary_name))
        .find(|candidate| candidate.is_file())
}

fn find_globally(binary_names: &[&str]) -> Option<PathBuf> {
    let mut stack = global_search_roots();

    while let Some(directory) = stack.pop() {
        let entries = match fs::read_dir(&directory) {
            Ok(entries) => entries,
            Err(_) => continue,
        };

        for entry in entries.flatten() {
            let file_type = match entry.file_type() {
                Ok(file_type) => file_type,
                Err(_) => continue,
            };

            let path = entry.path();

            if file_type.is_file() {
                let is_match = path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .map(|name| binary_names.contains(&name))
                    .unwrap_or(false);

                if is_match {
                    return Some(path);
                }
            } else if file_type.is_dir() && !file_type.is_symlink() {
                stack.push(path);
            }
        }
    }

    None
}

fn global_search_roots() -> Vec<PathBuf> {
    if cfg!(windows) {
        let mut roots = Vec::new();

        for drive in b'A'..=b'Z' {
            let candidate = PathBuf::from(format!("{}:\\", drive as char));
            if candidate.is_dir() {
                roots.push(candidate);
            }
        }

        roots
    } else {
        vec![PathBuf::from("/")]
    }
}
