//! 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-chipsmith 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)
}

/// POST original STEP bytes + chipsmith bake-meta JSON to service-step2glb
/// /bake. Returns the baked STEP file bytes on success, or None when the
/// service hasn't deployed /bake yet (for v0.5.0 and earlier — chipsmith
/// then falls back to writing only the JSON sidecar).
pub fn bake_step(step: &[u8], bake_meta: &serde_json::Value) -> Result<Option<Vec<u8>>> {
    use serde_json::json;
    let url = format!("{}/bake", step2glb_service_url());
    let body = json!({
        "step_b64": base64_encode(step),
        "bake_meta": bake_meta,
    });
    let body_bytes = serde_json::to_vec(&body).map_err(|e| anyhow!("encode: {e}"))?;
    let resp = match ureq::post(&url)
        .set("Content-Type", "application/json")
        .timeout(Duration::from_secs(180))
        .send_bytes(&body_bytes) {
        Ok(r) => r,
        Err(ureq::Error::Status(404, _)) => return Ok(None),
        Err(e) => return Err(anyhow!("POST {url}: {e}")),
    };
    if resp.status() == 404 { return Ok(None); }
    if resp.status() != 200 {
        let s = resp.into_string().unwrap_or_default();
        return Err(anyhow!("/bake non-200: {s}"));
    }
    let mut out = Vec::new();
    use std::io::Read;
    resp.into_reader().take(100 * 1024 * 1024).read_to_end(&mut out)
        .map_err(|e| anyhow!("read body: {e}"))?;
    Ok(Some(out))
}

fn base64_encode(b: &[u8]) -> String {
    const TBL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut out = String::with_capacity((b.len() + 2) / 3 * 4);
    let mut i = 0;
    while i + 3 <= b.len() {
        let n = ((b[i] as u32) << 16) | ((b[i+1] as u32) << 8) | (b[i+2] as u32);
        out.push(TBL[((n >> 18) & 0x3f) as usize] as char);
        out.push(TBL[((n >> 12) & 0x3f) as usize] as char);
        out.push(TBL[((n >> 6) & 0x3f) as usize] as char);
        out.push(TBL[(n & 0x3f) as usize] as char);
        i += 3;
    }
    let rem = b.len() - i;
    if rem == 1 {
        let n = (b[i] as u32) << 16;
        out.push(TBL[((n >> 18) & 0x3f) as usize] as char);
        out.push(TBL[((n >> 12) & 0x3f) as usize] as char);
        out.push_str("==");
    } else if rem == 2 {
        let n = ((b[i] as u32) << 16) | ((b[i+1] as u32) << 8);
        out.push(TBL[((n >> 18) & 0x3f) as usize] as char);
        out.push(TBL[((n >> 12) & 0x3f) as usize] as char);
        out.push(TBL[((n >> 6) & 0x3f) as usize] as char);
        out.push('=');
    }
    out
}

/// POST raw STEP bytes to service-step2glb /step-meta. Returns the parsed
/// JSON tree (PRODUCTs + faces + surface types) on success, or None when
/// the service is on an older version that doesn't have /step-meta yet.
/// Cached at /tmp/adom-chipsmith-cache/<sha>.step-meta.json next to the GLB
/// so re-loads of the same chip skip the network call.
pub fn fetch_step_meta(step: &[u8]) -> Result<Option<serde_json::Value>> {
    let _ = cache::ensure_dir()?;
    let hash = cache::sha256_hex(step);
    let cache_path = cache::cache_dir().join(format!("{hash}.step-meta.json"));
    if cache_path.exists() {
        if let Ok(s) = std::fs::read_to_string(&cache_path) {
            if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
                return Ok(Some(v));
            }
        }
    }
    let url = format!("{}/step-meta", step2glb_service_url());
    let resp = match ureq::post(&url)
        .set("Content-Type", "application/step")
        .timeout(Duration::from_secs(180))
        .send_bytes(step) {
        Ok(r) => r,
        Err(ureq::Error::Status(404, _)) => return Ok(None), // service hasn't redeployed yet
        Err(e) => return Err(anyhow!("POST {url}: {e}")),
    };
    if resp.status() == 404 {
        return Ok(None);
    }
    if resp.status() != 200 {
        let body = resp.into_string().unwrap_or_default();
        return Err(anyhow!("service-step2glb /step-meta non-200: {body}"));
    }
    let body = resp.into_string().map_err(|e| anyhow!("read body: {e}"))?;
    let _ = std::fs::write(&cache_path, &body);
    let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| anyhow!("parse: {e}"))?;
    Ok(Some(v))
}

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
}