//! Descarga remota para `.swm`: `patch_url` (source_patch) y `content_url` (file_drop).
//! Ver `docs/06-swm-format.md` §2. La red sólo se toca aquí y en `fetch` — nunca dentro del
//! sandbox de build.
//!
//! Usa `curl` (ya presente en el rootfs del lab). Acepta cualquier esquema que curl entienda;
//! en tests usamos `file://` para no depender de la red. La integridad la garantiza el
//! llamador: el `file_drop` verifica BLAKE3 (`content_hash`) tras descargar; el `patch_url`
//! se cubre indirectamente por el build reproducible + `expected_hash` del artefacto.

use std::path::Path;
use std::process::Command;

/// Descarga `url` y devuelve su contenido en memoria. Falla con el stderr de curl si el
/// código de salida no es 0 (incluye errores HTTP gracias a `-f`).
pub fn fetch_url_bytes(url: &str) -> hammer_core::Result<Vec<u8>> {
    let out = Command::new("curl")
        .args([
            "-fsSL",
            "--retry",
            "2",
            "--connect-timeout",
            "30",
            "--max-time",
            "300",
            url,
        ])
        .output()
        .map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?;
    if !out.status.success() {
        return Err(hammer_core::Error::Other(anyhow::anyhow!(
            "curl {url} falló (exit {}): {}",
            out.status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into()),
            String::from_utf8_lossy(&out.stderr).trim()
        )));
    }
    Ok(out.stdout)
}

/// Descarga `url` y lo escribe en `dst` (creando los directorios padre).
pub fn fetch_url_to_file(url: &str, dst: &Path) -> hammer_core::Result<()> {
    let bytes = fetch_url_bytes(url)?;
    if let Some(parent) = dst.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(dst, &bytes)?;
    Ok(())
}

/// Verifica que el sha256 de `path` coincide con `expected` (hex, case-insensitive). Lo usan
/// los consumidores que ingieren una fuente fijada (p. ej. el toolchain semilla del bootstrap,
/// [SDD 11](../../docs/11-bootstrap.md)) y necesitan comprobar integridad **antes** de sellar.
pub fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> {
    use sha2::{Digest, Sha256};
    let mut f = std::fs::File::open(path)?;
    let mut hasher = Sha256::new();
    std::io::copy(&mut f, &mut hasher)?;
    let got = hex::encode(hasher.finalize());
    if !got.eq_ignore_ascii_case(expected) {
        return Err(hammer_core::Error::Other(anyhow::anyhow!(
            "sha256 mismatch en {}: esperado {expected}, obtenido {got}",
            path.display()
        )));
    }
    Ok(())
}

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

    #[test]
    fn fetch_file_url_roundtrip() {
        let d = tempfile::tempdir().unwrap();
        let src = d.path().join("payload.txt");
        std::fs::write(&src, b"contenido remoto\n").unwrap();
        let url = format!("file://{}", src.display());
        let got = fetch_url_bytes(&url).unwrap();
        assert_eq!(got, b"contenido remoto\n");
    }

    #[test]
    fn fetch_to_file_writes_and_creates_parents() {
        let d = tempfile::tempdir().unwrap();
        let src = d.path().join("p.txt");
        std::fs::write(&src, b"abc").unwrap();
        let dst = d.path().join("nested/dir/out.txt");
        fetch_url_to_file(&format!("file://{}", src.display()), &dst).unwrap();
        assert_eq!(std::fs::read(&dst).unwrap(), b"abc");
    }

    #[test]
    fn missing_url_is_error() {
        let err = fetch_url_bytes("file:///no/existe/aqui.bin").unwrap_err().to_string();
        assert!(err.contains("curl"), "{err}");
    }
}
