//! Preview mode — fetch GLB via the shared service, stash it locally, then
//! spawn a tiny HTTP server that the Hydrogen webview can load (webviews
//! can't reach the external service directly; they need the user's own
//! container-proxy URL).

use crate::cli::PreviewArgs;
use crate::client;
use crate::ui::{err, hint, ok, CYAN, RESET};
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};

const UI_HTML: &str = include_str!("ui.html");

struct State {
    glb_path: PathBuf,
    source_label: String,
    conversion: serde_json::Value,
}

pub fn run(args: PreviewArgs) {
    let (glb_path, conversion_json, source_label) = match resolve(&args.source) {
        Ok(v) => v,
        Err(e) => {
            err(format!("preview failed: {e}"));
            hint("Run `step2glb health` to verify the service is reachable.");
            std::process::exit(4);
        }
    };

    let port = if args.port == 0 { pick_free_port() } else { args.port };
    let bind = format!("127.0.0.1:{port}");
    let server = match Server::http(&bind) {
        Ok(s) => s,
        Err(e) => { err(format!("bind {bind}: {e}")); std::process::exit(4); }
    };
    let state = Arc::new(Mutex::new(State { glb_path, source_label: source_label.clone(), conversion: conversion_json }));

    let url = proxy_url(port);
    if !args.no_tab {
        let name = format!("step2glb — {}", short_label(&source_label));
        if let Err(e) = crate::tab::open_webview_tab(&name, &url) {
            eprintln!("(warning) could not open Hydrogen tab: {e}");
        }
    }
    ok(format!("preview ready at {CYAN}{url}{RESET}  (Ctrl-C to stop)"));

    for req in server.incoming_requests() {
        handle(req, &state);
    }
}

fn resolve(source: &str) -> Result<(PathBuf, serde_json::Value, String), String> {
    let as_path = PathBuf::from(source);
    if as_path.exists() {
        let out = PathBuf::from(format!("/tmp/step2glb-preview-{}.glb",
            as_path.file_stem().map(|s|s.to_string_lossy().to_string()).unwrap_or_else(||"in".into())));
        let bytes = fs::read(&as_path).map_err(|e| format!("read {}: {e}", as_path.display()))?;
        let r = client::convert_bytes(&bytes, &out)?;
        let label = as_path.file_name().map(|n|n.to_string_lossy().to_string()).unwrap_or_else(||source.to_string());
        Ok((out.clone(), conversion_to_json(&r, &out), label))
    } else {
        let leaf = source.rsplit('/').next().unwrap_or("part");
        let out = PathBuf::from(format!("/tmp/step2glb-preview-{leaf}.glb"));
        let r = client::convert_from_library(source, &out)?;
        Ok((out.clone(), conversion_to_json(&r, &out), source.to_string()))
    }
}

fn conversion_to_json(r: &client::ConvertResult, out: &std::path::Path) -> serde_json::Value {
    json!({
        "output": out.to_string_lossy(),
        "sizeBytes": r.size_bytes,
        "meshes": r.meshes,
        "nodes": r.nodes,
        "materials": r.materials,
        "durationMs": r.duration_ms,
    })
}

fn pick_free_port() -> u16 {
    let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind 0");
    l.local_addr().unwrap().port()
}

fn proxy_url(port: u16) -> String {
    if let Ok(proxy) = std::env::var("VSCODE_PROXY_URI") {
        proxy.replace("{{port}}", &port.to_string())
    } else {
        format!("http://127.0.0.1:{port}/")
    }
}

fn short_label(s: &str) -> String {
    if s.len() > 40 { format!("…{}", &s[s.len() - 37..]) } else { s.to_string() }
}

fn handle(req: Request, state: &Arc<Mutex<State>>) {
    let full = req.url().to_string();
    let path = full.split('?').next().unwrap_or(&full).to_string();
    match (req.method(), path.as_str()) {
        (Method::Get, "/") | (Method::Get, "/index.html") => {
            let resp = Response::from_string(UI_HTML)
                .with_header(Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap());
            let _ = req.respond(resp);
        }
        (Method::Get, "/meta") => {
            let s = state.lock().unwrap();
            let body = json!({
                "source": s.source_label,
                "glbPath": s.glb_path.to_string_lossy(),
                "conversion": s.conversion,
            }).to_string();
            let resp = Response::from_string(body)
                .with_header(Header::from_bytes("Content-Type", "application/json").unwrap());
            let _ = req.respond(resp);
        }
        (Method::Get, "/model.glb") => {
            let path = state.lock().unwrap().glb_path.clone();
            match fs::File::open(&path) {
                Ok(f) => {
                    let len = f.metadata().map(|m| m.len() as usize).unwrap_or(0);
                    let resp = Response::new(
                        StatusCode(200),
                        vec![
                            Header::from_bytes("Content-Type", "model/gltf-binary").unwrap(),
                            Header::from_bytes("Content-Length", len.to_string()).unwrap(),
                        ],
                        f,
                        Some(len),
                        None,
                    );
                    let _ = req.respond(resp);
                }
                Err(e) => {
                    let _ = req.respond(Response::from_string(format!("open {path:?}: {e}")).with_status_code(500));
                }
            }
        }
        (Method::Post, "/console") => {
            let _ = req.respond(Response::empty(204));
        }
        (Method::Post, "/shutdown") => {
            let _ = req.respond(Response::from_string("bye"));
            std::process::exit(0);
        }
        _ => {
            let _ = req.respond(Response::from_string("not found").with_status_code(404));
        }
    }
}