//! Configuración del lab: paths del rootfs base, del compilador inyectado y del work dir.
//!
//! La idea es que `BuildConfig` se construye una vez (en la CLI desde env vars + defaults) y
//! se pasa explícitamente a `build`, de modo que los tests pueden montar configuraciones
//! aisladas sin tocar variables de entorno globales.

use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct BuildConfig {
    /// Rootfs base read-only del sandbox (Alpine para la fase actual del roadmap).
    pub rootfs: PathBuf,
    /// Directorio con el binario `zig` y su `lib/`.
    pub zig_dir: PathBuf,
    /// Raíz para directorios de trabajo (repos clonados, copias del source para
    /// patch+compile, y `out` por build). Debe estar en el mismo filesystem que el store
    /// para que el `seal` final sea un rename atómico.
    pub work_root: PathBuf,
    /// Directorio host persistente que se bindea como `/cache` en el sandbox; alberga la
    /// caché global de `zig` (objetos de musl, libc, etc.) entre builds. `None` desactiva.
    pub cache_root: Option<PathBuf>,
}

/// Nombre del directorio del lab, buscado hacia arriba como hace git con `.git`.
const LAB_DIR: &str = ".dev-fs";

/// Busca el lab (`.dev-fs`) desde `start` hacia arriba. Pura y testeable: no mira env ni CWD.
fn find_lab_upward(start: &Path) -> Option<PathBuf> {
    start.ancestors().map(|d| d.join(LAB_DIR)).find(|c| c.is_dir())
}

/// Resuelve dónde vive el lab SIN atarlo a dónde vive el store.
///
/// Orden, de más explícito a más adivinado:
///   1. `HAMMER_LAB` — el operador manda.
///   2. Hermano del store (`<store>/../.dev-fs`) **si existe**. Preserva exactamente el
///      comportamiento histórico en la topología normal (lab y store en la raíz del proyecto).
///   3. Hacia arriba desde el CWD. Ésta es la que desacopla: cubre el caso en que el store fue
///      movido a otro filesystem (volumen persistente) y el lab se quedó en el proyecto.
///   4. `None` ⇒ el llamador cae al default histórico, para que los mensajes de error sigan
///      apuntando a un lugar previsible en vez de a la nada.
fn resolve_lab_dir(store_root: &Path) -> Option<PathBuf> {
    if let Some(v) = std::env::var_os("HAMMER_LAB") {
        return Some(PathBuf::from(v));
    }
    if let Some(parent) = store_root.parent() {
        let sibling = parent.join(LAB_DIR);
        if sibling.is_dir() {
            return Some(sibling);
        }
    }
    find_lab_upward(&std::env::current_dir().ok()?)
}

impl BuildConfig {
    /// Defaults relativos al store: `.dev-fs` y `work/` como hermanos del store.
    /// `store_root = /…/store` → rootfs = `/…/.dev-fs/alpine`, zig = `/…/.dev-fs/tools/zig`,
    /// work = `/…/work`, cache = `/…/.dev-fs/cache`.
    ///
    /// PURA a propósito (no mira env ni CWD): los tests montan configs herméticas con esto. Quien
    /// quiera la resolución real del entorno usa [`Self::from_env_or_defaults`].
    pub fn defaults_for_store(store_root: &Path) -> Self {
        let project_root = store_root
            .parent()
            .unwrap_or_else(|| Path::new("."))
            .to_path_buf();
        Self::defaults_for_store_with_lab(store_root, &project_root.join(LAB_DIR))
    }

    /// Igual que [`Self::defaults_for_store`] pero con el lab (`.dev-fs`) puesto a mano.
    ///
    /// POR QUÉ EXISTE (2026-07-22): atar el lab al PADRE DEL STORE resultó ser una suposición
    /// oculta y cara. Al anclar el store de un worker a un volumen persistente (symlink a
    /// `/mnt/cosecha/store`), el lab pasó a buscarse en `/mnt/cosecha/.dev-fs` — que no existe — y
    /// TODA receta con `zig_version` murió con "no encuentro el ejecutable zig en …", un mensaje
    /// que apunta al lugar equivocado (el zig estaba, y estaba bien). Dos campañas leyeron eso como
    /// deuda de la cascada GUI. Son dos decisiones independientes: dónde se GUARDA lo sellado
    /// (almacenamiento) y dónde vive el toolchain de desarrollo (entorno).
    ///
    /// `work_root` SÍ sigue al store, y es deliberado: el `seal` final es un rename, que sólo es
    /// atómico dentro del mismo filesystem. Ése es un acoplamiento real, no una suposición.
    pub fn defaults_for_store_with_lab(store_root: &Path, lab: &Path) -> Self {
        let store_parent = store_root
            .parent()
            .unwrap_or_else(|| Path::new("."))
            .to_path_buf();
        Self {
            rootfs: lab.join("alpine"),
            zig_dir: lab.join("tools/zig"),
            work_root: store_parent.join("work"),
            cache_root: Some(lab.join("cache")),
        }
    }

    /// Lee overrides desde entorno (`HAMMER_LAB`, `HAMMER_ROOTFS`, `HAMMER_ZIG`, `HAMMER_WORK`,
    /// `HAMMER_CACHE`). `HAMMER_CACHE=""` (vacío) desactiva la caché.
    ///
    /// Es el constructor que HUELE EL ENTORNO (env + CWD); `defaults_for_store` se mantiene pura.
    /// `HAMMER_LAB` mueve el lab entero de una; los `HAMMER_ROOTFS`/`HAMMER_ZIG`/`HAMMER_CACHE`
    /// siguen pisando pieza por pieza y ganan sobre él.
    pub fn from_env_or_defaults(store_root: &Path) -> Self {
        let mut cfg = match resolve_lab_dir(store_root) {
            Some(lab) => Self::defaults_for_store_with_lab(store_root, &lab),
            None => Self::defaults_for_store(store_root),
        };
        if let Ok(v) = std::env::var("HAMMER_ROOTFS") {
            cfg.rootfs = PathBuf::from(v);
        }
        if let Ok(v) = std::env::var("HAMMER_ZIG") {
            cfg.zig_dir = PathBuf::from(v);
        }
        if let Ok(v) = std::env::var("HAMMER_WORK") {
            cfg.work_root = PathBuf::from(v);
        }
        if let Ok(v) = std::env::var("HAMMER_CACHE") {
            cfg.cache_root = if v.is_empty() { None } else { Some(PathBuf::from(v)) };
        }
        cfg
    }
}

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

    /// La topología normal (lab y store hermanos en la raíz) no se mueve.
    #[test]
    fn lab_hermano_del_store_sigue_igual() {
        let cfg = BuildConfig::defaults_for_store(Path::new("/proj/store"));
        assert_eq!(cfg.rootfs, Path::new("/proj/.dev-fs/alpine"));
        assert_eq!(cfg.zig_dir, Path::new("/proj/.dev-fs/tools/zig"));
        assert_eq!(cfg.cache_root.unwrap(), Path::new("/proj/.dev-fs/cache"));
        assert_eq!(cfg.work_root, Path::new("/proj/work"));
    }

    /// LA REGRESIÓN: store mudado a un volumen, lab quieto en el proyecto. El lab debe seguir al
    /// proyecto y NO al store; `work` debe seguir al STORE (mismo filesystem ⇒ seal atómico).
    #[test]
    fn lab_y_store_pueden_vivir_en_filesystems_distintos() {
        let cfg = BuildConfig::defaults_for_store_with_lab(
            Path::new("/mnt/cosecha/store"),
            Path::new("/opt/hammer/.dev-fs"),
        );
        assert_eq!(cfg.zig_dir, Path::new("/opt/hammer/.dev-fs/tools/zig"));
        assert_eq!(cfg.rootfs, Path::new("/opt/hammer/.dev-fs/alpine"));
        assert_eq!(cfg.cache_root.unwrap(), Path::new("/opt/hammer/.dev-fs/cache"));
        assert_eq!(
            cfg.work_root,
            Path::new("/mnt/cosecha/work"),
            "work DEBE seguir al store: el seal es un rename y sólo es atómico en el mismo FS"
        );
    }

    #[test]
    fn find_lab_upward_encuentra_desde_un_subdirectorio() {
        let d = tempfile::tempdir().unwrap();
        let raiz = d.path().join("proj");
        std::fs::create_dir_all(raiz.join(LAB_DIR)).unwrap();
        let hondo = raiz.join("a/b/c");
        std::fs::create_dir_all(&hondo).unwrap();
        assert_eq!(find_lab_upward(&hondo), Some(raiz.join(LAB_DIR)));
    }

    #[test]
    fn find_lab_upward_no_inventa_lo_que_no_existe() {
        let d = tempfile::tempdir().unwrap();
        let hondo = d.path().join("sin/lab/alguno");
        std::fs::create_dir_all(&hondo).unwrap();
        assert_eq!(find_lab_upward(&hondo), None);
    }

    /// Un fichero llamado `.dev-fs` no es un lab: la búsqueda exige directorio.
    #[test]
    fn find_lab_upward_ignora_un_fichero_homonimo() {
        let d = tempfile::tempdir().unwrap();
        let raiz = d.path().join("proj");
        std::fs::create_dir_all(&raiz).unwrap();
        std::fs::write(raiz.join(LAB_DIR), b"no soy un lab").unwrap();
        assert_eq!(find_lab_upward(&raiz), None);
    }
}
