//! Thin client to service-step2glb. Converts STEP -> GLB, caches by SHA-256.
//!
//! Mirrors the HTTP shape of step2glb's client.rs (Material structure,
//! X-Materials-Json header, X-Meshes / X-Nodes / X-Duration-Ms headers)
//! but inlines it here so adom-step has zero dependency on the step2glb crate.

use crate::cli::step2glb_service_url;
use crate::server::cache;
use anyhow::{anyhow, Result};
use std::path::PathBuf;
use std::time::Duration;

pub struct ConvertResult {
    pub glb_path: PathBuf,
    pub size_bytes: u64,
    pub meshes: u32,
    pub nodes: u32,
    pub materials: serde_json::Value,
    pub duration_ms: u64,
    pub cache_hit: bool,
}

/// Convert raw STEP bytes via the service. Cache key = SHA-256 of bytes.
/// On hit, returns instantly without hitting the network.
pub fn convert_step_bytes(step: &[u8]) -> Result<ConvertResult> {
    let dir = cache::ensure_dir()?;
    let hash = cache::sha256_hex(step);
    let glb = cache::glb_path(&hash);
    let mat_path = cache::materials_path(&hash);

    // Cache hit
    if glb.exists() {
        let size = std::fs::metadata(&glb).map(|m| m.len()).unwrap_or(0);
        let materials = cache::read_materials(&mat_path);
        return Ok(ConvertResult {
            glb_path: glb,
            size_bytes: size,
            meshes: 0, // not re-introspected on cache hit
            nodes: 0,
            materials,
            duration_ms: 0,
            cache_hit: true,
        });
    }

    // Cache miss — POST to service-step2glb /convert.
    let url = format!("{}/convert", step2glb_service_url());
    let resp = ureq::post(&url)
        .set("Content-Type", "application/step")
        .timeout(Duration::from_secs(180))
        .send_bytes(step)
        .map_err(|e| anyhow!("POST {url}: {e}"))?;

    if resp.status() != 200 {
        let body = resp.into_string().unwrap_or_default();
        return Err(anyhow!("service-step2glb returned HTTP non-200: {body}"));
    }

    let meshes = header_u32(&resp, "X-Meshes");
    let nodes = header_u32(&resp, "X-Nodes");
    let duration_ms = u64::from(header_u32(&resp, "X-Duration-Ms"));
    let mats_json_str = resp.header("X-Materials-Json").unwrap_or("[]").to_string();
    let materials: serde_json::Value =
        serde_json::from_str(&mats_json_str).unwrap_or(serde_json::json!([]));

    // Stream body to disk in cache.
    let mut tmp = dir.join(format!("{hash}.glb.tmp"));
    let mut file = std::fs::File::create(&tmp)
        .map_err(|e| anyhow!("create {}: {e}", tmp.display()))?;
    let mut reader = resp.into_reader();
    let size_bytes = std::io::copy(&mut reader, &mut file)
        .map_err(|e| anyhow!("write GLB: {e}"))?;
    drop(file);
    std::fs::rename(&tmp, &glb).map_err(|e| anyhow!("rename {}: {e}", tmp.display()))?;
    tmp.clear();

    // Write materials sidecar.
    let _ = std::fs::write(&mat_path, mats_json_str);

    Ok(ConvertResult {
        glb_path: glb,
        size_bytes,
        meshes,
        nodes,
        materials,
        duration_ms,
        cache_hit: false,
    })
}

/// Convert from a service-kicad library: GET /convert?library=L&name=N.
pub fn convert_from_library(lib_slash_name: &str) -> Result<ConvertResult> {
    let (lib, name) = lib_slash_name
        .rsplit_once('/')
        .ok_or_else(|| anyhow!("bad path {lib_slash_name:?}; expected <Library>/<Name>"))?;
    let url = format!(
        "{}/convert?library={}&name={}",
        step2glb_service_url(),
        urlencode(lib),
        urlencode(name),
    );
    // Cache key for library entries: hash of the URL path itself.
    let dir = cache::ensure_dir()?;
    let hash = cache::sha256_hex(format!("lib:{lib}/{name}").as_bytes());
    let glb = cache::glb_path(&hash);
    let mat_path = cache::materials_path(&hash);

    if glb.exists() {
        let size = std::fs::metadata(&glb).map(|m| m.len()).unwrap_or(0);
        let materials = cache::read_materials(&mat_path);
        return Ok(ConvertResult {
            glb_path: glb,
            size_bytes: size,
            meshes: 0,
            nodes: 0,
            materials,
            duration_ms: 0,
            cache_hit: true,
        });
    }

    let resp = ureq::get(&url)
        .timeout(Duration::from_secs(180))
        .call()
        .map_err(|e| anyhow!("GET {url}: {e}"))?;

    if resp.status() != 200 {
        let body = resp.into_string().unwrap_or_default();
        return Err(anyhow!(
            "service-step2glb returned HTTP non-200 for library {lib}/{name}: {body}"
        ));
    }

    let meshes = header_u32(&resp, "X-Meshes");
    let nodes = header_u32(&resp, "X-Nodes");
    let duration_ms = u64::from(header_u32(&resp, "X-Duration-Ms"));
    let mats_json_str = resp.header("X-Materials-Json").unwrap_or("[]").to_string();
    let materials: serde_json::Value =
        serde_json::from_str(&mats_json_str).unwrap_or(serde_json::json!([]));

    let tmp = dir.join(format!("{hash}.glb.tmp"));
    let mut file = std::fs::File::create(&tmp)
        .map_err(|e| anyhow!("create {}: {e}", tmp.display()))?;
    let mut reader = resp.into_reader();
    let size_bytes = std::io::copy(&mut reader, &mut file)
        .map_err(|e| anyhow!("write GLB: {e}"))?;
    drop(file);
    std::fs::rename(&tmp, &glb).map_err(|e| anyhow!("rename: {e}"))?;
    let _ = std::fs::write(&mat_path, mats_json_str);

    Ok(ConvertResult {
        glb_path: glb,
        size_bytes,
        meshes,
        nodes,
        materials,
        duration_ms,
        cache_hit: false,
    })
}

fn header_u32(r: &ureq::Response, name: &str) -> u32 {
    r.header(name).and_then(|s| s.parse().ok()).unwrap_or(0)
}

fn urlencode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.bytes() {
        if matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~') {
            out.push(b as char);
        } else {
            out.push_str(&format!("%{:02X}", b));
        }
    }
    out
}