//! 3D thumbnail: call service-step2glb's POST /thumbnail directly (OCCT
//! V3d offscreen). We talk to the service over HTTP rather than shelling
//! to the `step2glb` CLI, because the deployed CLI's thumbnail path does
//! NOT poll the service's async (202 + job) response — it bails with
//! "service returned HTTP 202". Calling the service directly lets us poll
//! the job to completion, which is what actually works.
//!
//! Renders the as-is + 5 other face-up orientations at the medium size the
//! dashboard displays. Manufacturer chip STEPs ship in inconsistent up-axis
//! conventions; chip-thumbnailer is intentionally neutral and renders every
//! face-up variant — downstream tools pick the right one per chip.

use crate::library::{ChipPaths, ThreeDOrientation, ThreeDSize};
use anyhow::{anyhow, Context, Result};
use std::path::Path;
use std::time::Duration;

const SERVICE_URL_DEFAULT: &str = "https://step2glb-gmdoncpxdwx0.adom.cloud";

fn service_url() -> String {
    std::env::var("STEP2GLB_SERVICE_API")
        .unwrap_or_else(|_| SERVICE_URL_DEFAULT.to_string())
        .trim_end_matches('/')
        .to_string()
}

/// Render every face-up orientation at the displayed (Md) size. This covers
/// the dashboard's Z↑/Y↑ tiles plus the 6-orientation popover. (The sm/lg
/// size variants are rendered on demand via `render_orient_size`.)
pub fn render(chip: &ChipPaths) -> Result<()> {
    let step = chip
        .step
        .as_ref()
        .ok_or_else(|| anyhow!("no .step in {}", chip.dir.display()))?
        .clone();

    let mut first_err: Option<anyhow::Error> = None;
    for orient in ThreeDOrientation::all() {
        let out = chip.three_d_thumb_path(orient, ThreeDSize::Md);
        let (w, h) = ThreeDSize::Md.dims();
        if let Err(e) = service_thumbnail(&step, &out, orient.step2glb_arg(), w, h, &chip.mpn) {
            // Keep rendering the other orientations; report the first failure.
            if first_err.is_none() {
                first_err = Some(anyhow!(
                    "step2glb service render failed for {} ({}): {e}",
                    chip.mpn,
                    orient.json_key()
                ));
            }
        }
    }

    // Lazily populate <MPN>-features.json (manifest pulls bbox from it).
    ensure_features(&step, chip);

    match first_err {
        Some(e) => Err(e),
        None => Ok(()),
    }
}

/// Back-compat single-size render at the AsIs orientation.
pub fn render_size(chip: &ChipPaths, size: ThreeDSize) -> Result<()> {
    render_orient_size(chip, ThreeDOrientation::AsIs, size)
}

/// Render a single (orientation, size) to disk via the service.
pub fn render_orient_size(
    chip: &ChipPaths,
    orient: ThreeDOrientation,
    size: ThreeDSize,
) -> Result<()> {
    let step = chip
        .step
        .as_ref()
        .ok_or_else(|| anyhow!("no .step in {}", chip.dir.display()))?
        .clone();
    let out = chip.three_d_thumb_path(orient, size);
    let (w, h) = size.dims();
    service_thumbnail(&step, &out, orient.step2glb_arg(), w, h, &chip.mpn)
        .with_context(|| format!("3D render {} ({}/{})", chip.mpn, orient.json_key(), size.label()))?;
    ensure_features(&step, chip);
    Ok(())
}

fn ensure_features(step: &Path, chip: &ChipPaths) {
    let features_path = chip.dir.join(format!("{}-features.json", chip.mpn));
    if features_path.is_file() {
        return;
    }
    // features still goes through the CLI. Its render path can return the
    // service's async 202 (which the CLI doesn't poll) — that's a non-fatal
    // metadata miss, so capture output rather than letting a misleading
    // "feature extraction failed" line leak to the user.
    let _ = std::process::Command::new("step2glb")
        .arg("features")
        .arg(step)
        .arg("--out")
        .arg(&features_path)
        .output();
}

/// POST a STEP to service-step2glb's /thumbnail, handling both the legacy
/// synchronous (200 → PNG) and the current async (202 → job → poll) shapes,
/// then write the PNG to `out`.
fn service_thumbnail(
    step: &Path,
    out: &Path,
    up_axis: &str,
    w: u32,
    h: u32,
    mpn: &str,
) -> Result<()> {
    let svc = service_url();
    let body = std::fs::read(step).with_context(|| format!("read {}", step.display()))?;
    let job_name = format!("thumbnail-iso-{up_axis}-{mpn}-for-chip-thumbnailer");
    let url = format!("{svc}/thumbnail?pose=iso&kind=step&upAxis={up_axis}&width={w}&height={h}");

    let resp = ureq::post(&url)
        .set("Content-Type", "application/step")
        .set("X-Client", "chip-thumbnailer/chip-fetcher")
        .set("X-Job-Name", &job_name)
        .timeout(Duration::from_secs(180))
        .send_bytes(&body)
        .map_err(|e| anyhow!("POST {url}: {e}"))?;

    match resp.status() {
        200 => write_reader(resp, out),
        202 => {
            let txt = resp.into_string().context("read 202 job body")?;
            let v: serde_json::Value =
                serde_json::from_str(&txt).context("parse 202 job json")?;
            let job_id = v
                .get("job_id")
                .and_then(|s| s.as_str())
                .ok_or_else(|| anyhow!("202 response had no job_id"))?
                .to_string();
            poll_and_write(&svc, &job_id, out)
        }
        code => Err(anyhow!("service returned HTTP {code}")),
    }
}

fn poll_and_write(svc: &str, job_id: &str, out: &Path) -> Result<()> {
    for _ in 0..30 {
        if let Ok(r) = ureq::get(&format!("{svc}/jobs/{job_id}"))
            .set("X-Client", "chip-thumbnailer/chip-fetcher")
            .set("X-Job-Name", "poll")
            .timeout(Duration::from_secs(15))
            .call()
        {
            let v: serde_json::Value = r
                .into_string()
                .ok()
                .and_then(|t| serde_json::from_str(&t).ok())
                .unwrap_or(serde_json::Value::Null);
            match v.get("status").and_then(|s| s.as_str()) {
                Some("complete") => {
                    let res = ureq::get(&format!("{svc}/jobs/{job_id}/result"))
                        .set("X-Client", "chip-thumbnailer/chip-fetcher")
                        .set("X-Job-Name", "result")
                        .timeout(Duration::from_secs(30))
                        .call()
                        .map_err(|e| anyhow!("GET job result: {e}"))?;
                    return write_reader(res, out);
                }
                Some("failed") => {
                    let msg = v.get("error").and_then(|s| s.as_str()).unwrap_or("unknown");
                    return Err(anyhow!("service job failed: {msg}"));
                }
                _ => {}
            }
        }
        std::thread::sleep(Duration::from_secs(2));
    }
    Err(anyhow!("service job {job_id} timed out"))
}

fn write_reader(resp: ureq::Response, out: &Path) -> Result<()> {
    let mut reader = resp.into_reader();
    let mut f = std::fs::File::create(out).with_context(|| format!("create {}", out.display()))?;
    std::io::copy(&mut reader, &mut f).with_context(|| format!("write {}", out.display()))?;
    Ok(())
}