"""
Three.js renderer service — FastAPI microservice.

Pipeline: STEP → STL (cadquery) → Three.js in headless Chromium → PNG screenshot.

Two render modes
────────────────
  part_colors = None      Single grey metallic mesh (original behaviour).
  part_colors = dict      Connected-component analysis in JavaScript:
                          The STL is loaded as one mesh; disconnected islands are
                          detected entirely in the browser and each gets a distinct
                          palette colour.  No server-side OCC/per-part extraction —
                          just one STL conversion and client-side graph analysis.
"""
import asyncio
import base64
import json
import logging
from pathlib import Path

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

logger = logging.getLogger(__name__)

app = FastAPI(title="Three.js Renderer", version="1.0.0")

# 10-colour palette used for connected-component assignment
PALETTE = [
    "#4C9BE8", "#E85B4C", "#4CBE72", "#E8A84C", "#A04CE8",
    "#4CD4E8", "#E84CA8", "#7EC850", "#E86B30", "#5088C8",
]


class RenderRequest(BaseModel):
    step_path: str
    output_path: str
    width: int = 512
    height: int = 512
    # None  → single grey mesh
    # {}    → auto-colour by connected-component index (palette)
    # {...} → same (named-part colour mapping is handled in JS if names match)
    part_colors: dict[str, str] | None = None
    rotation_x: float = 0.0
    rotation_y: float = 0.0
    rotation_z: float = 0.0


@app.get("/health")
async def health():
    return {"status": "ok", "renderer": "threejs"}


@app.post("/render")
async def render(req: RenderRequest):
    step_path = Path(req.step_path)
    output_path = Path(req.output_path)

    if not step_path.exists():
        raise HTTPException(404, detail=f"STEP file not found: {step_path}")

    output_path.parent.mkdir(parents=True, exist_ok=True)

    # Persistent STL cache — same convention as blender-renderer (quality always "low")
    stl_path = step_path.parent / f"{step_path.stem}_low.stl"
    if not stl_path.exists() or stl_path.stat().st_size == 0:
        try:
            _convert_step_to_stl(step_path, stl_path)
        except Exception as e:
            logger.error(f"STEP→STL conversion failed: {e}")
            raise HTTPException(500, detail=f"STEP conversion failed: {e}")
        logger.info("STL cached: %s (%d KB)", stl_path.name, stl_path.stat().st_size // 1024)
    else:
        logger.info("STL cache hit: %s (%d KB)", stl_path.name, stl_path.stat().st_size // 1024)

    use_colors = req.part_colors is not None
    try:
        await asyncio.to_thread(
            _render_stl_threejs,
            stl_path, output_path, req.width, req.height, use_colors,
            req.rotation_x, req.rotation_y, req.rotation_z,
        )
    except Exception as e:
        logger.error(f"Three.js render failed: {e}")
        raise HTTPException(500, detail=f"Three.js render failed: {e}")

    if not output_path.exists():
        raise HTTPException(500, detail="Render produced no output file")

    return {
        "output_path": str(output_path),
        "status": "ok",
        "renderer": "threejs-colored" if use_colors else "threejs",
    }


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _convert_step_to_stl(step_path: Path, stl_path: Path) -> None:
    """Convert STEP to a single binary STL via cadquery."""
    import cadquery as cq
    shape = cq.importers.importStep(str(step_path))
    cq.exporters.export(shape, str(stl_path))
    if not stl_path.exists() or stl_path.stat().st_size == 0:
        raise RuntimeError("cadquery produced empty STL")


def _render_stl_threejs(
    stl_path: Path,
    output_path: Path,
    width: int,
    height: int,
    use_colors: bool,
    rotation_x: float = 0.0,
    rotation_y: float = 0.0,
    rotation_z: float = 0.0,
) -> None:
    """Render STL via Three.js in headless Chromium."""
    from playwright.sync_api import sync_playwright

    stl_b64 = base64.b64encode(stl_path.read_bytes()).decode()
    filename = stl_path.stem
    palette_json = json.dumps(PALETTE)

    html = _build_html(stl_b64, filename, width, height, palette_json, use_colors,
                       rotation_x, rotation_y, rotation_z)

    with sync_playwright() as p:
        browser = p.chromium.launch(
            args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
        )
        page = browser.new_page(viewport={"width": width, "height": height})
        page.set_content(html, wait_until="domcontentloaded")
        try:
            page.wait_for_function("window.__renderDone === true", timeout=12000)
        except Exception:
            pass   # take screenshot anyway
        page.screenshot(
            path=str(output_path),
            full_page=False,
            clip={"x": 0, "y": 0, "width": width, "height": height},
        )
        browser.close()


def _build_html(
    stl_b64: str,
    filename: str,
    width: int,
    height: int,
    palette_json: str,
    use_colors: bool,
    rotation_x: float = 0.0,
    rotation_y: float = 0.0,
    rotation_z: float = 0.0,
) -> str:
    """
    Build a self-contained HTML page that renders the STL with Three.js.

    When use_colors=True the JavaScript runs a Union-Find connected-component
    analysis on the vertex graph of the STL and paints each disconnected island
    with a distinct colour from the palette.  This requires no server-side part
    extraction — it works directly on the flat triangle soup in the STL.
    """

    # ---------- colour-assignment script (injected only when use_colors=True)
    color_script = ""
    if use_colors:
        color_script = f"""
// ── Connected-component colouring ─────────────────────────────────────────
// Each STL face is a triplet of un-shared vertices.  We weld coincident
// vertices by their rounded position string, then run Union-Find on the
// resulting graph to identify disconnected parts.  Each part gets a colour
// from the palette.
function applyPartColors(geometry, palette) {{
  const pos = geometry.attributes.position;
  const n   = pos.count;

  // Round to 4 d.p. to merge floating-point near-duplicates
  const key = i =>
    Math.round(pos.getX(i)*1e4) + ',' +
    Math.round(pos.getY(i)*1e4) + ',' +
    Math.round(pos.getZ(i)*1e4);

  // Map position string → canonical vertex index
  const posMap = Object.create(null);
  const canon  = new Int32Array(n);
  for (let i = 0; i < n; i++) {{
    const k = key(i);
    if (posMap[k] === undefined) posMap[k] = i;
    canon[i] = posMap[k];
  }}

  // Union-Find with path compression + union by rank
  const parent = new Int32Array(n);
  const rank   = new Uint8Array(n);
  for (let i = 0; i < n; i++) parent[i] = i;
  function find(x) {{
    while (parent[x] !== x) {{ parent[x] = parent[parent[x]]; x = parent[x]; }}
    return x;
  }}
  function unite(a, b) {{
    a = find(a); b = find(b);
    if (a === b) return;
    if (rank[a] < rank[b]) {{ let t = a; a = b; b = t; }}
    parent[b] = a;
    if (rank[a] === rank[b]) rank[a]++;
  }}

  // Connect the three canonical vertices of every triangle
  for (let i = 0; i < n; i += 3) {{
    unite(canon[i], canon[i+1]);
    unite(canon[i+1], canon[i+2]);
  }}

  // Assign a palette index to each component root
  const compIdx = Object.create(null);
  let nextIdx = 0;
  const colors = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) {{
    const root = find(canon[i]);
    if (compIdx[root] === undefined) compIdx[root] = nextIdx++;
    const hex = palette[compIdx[root] % palette.length];
    colors[i*3]   = parseInt(hex.slice(1,3), 16) / 255;
    colors[i*3+1] = parseInt(hex.slice(3,5), 16) / 255;
    colors[i*3+2] = parseInt(hex.slice(5,7), 16) / 255;
  }}

  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  return new THREE.MeshStandardMaterial({{
    vertexColors: true,
    metalness: 0.4,
    roughness: 0.45,
  }});
}}

const palette = {palette_json};
const material = applyPartColors(geometry, palette);
"""
    else:
        color_script = """
const material = new THREE.MeshStandardMaterial({
  color: 0xc0cad8,
  metalness: 0.8,
  roughness: 0.3,
});
"""

    return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
  * {{ margin:0; padding:0; box-sizing:border-box; }}
  body {{ background:#f5f6f8; overflow:hidden; }}
  canvas {{ display:block; }}
</style>
</head>
<body>
<script type="importmap">
{{
  "imports": {{
    "three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/"
  }}
}}
</script>
<script type="module">
import * as THREE from 'three';
import {{ STLLoader }} from 'three/addons/loaders/STLLoader.js';

const W = {width}, H = {height};
const renderer = new THREE.WebGLRenderer({{ antialias: true }});
renderer.setSize(W, H);
renderer.setPixelRatio(1);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf5f6f8);

const camera = new THREE.PerspectiveCamera(45, W / H, 0.001, 10000);
scene.add(camera);

const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 2.5);
dirLight.position.set(1, 2, 1.5);
dirLight.castShadow = true;
scene.add(dirLight);
const fillLight = new THREE.DirectionalLight(0xddeeff, 1.0);
fillLight.position.set(-1, -0.5, -1);
scene.add(fillLight);

// Decode base64 STL
const b64    = "{stl_b64}";
const binary = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const loader = new STLLoader();
const geometry = loader.parse(binary.buffer);
geometry.computeVertexNormals();

// Material (grey or per-part coloured)
{color_script}

const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);

// Centre and frame
geometry.computeBoundingBox();
const box = geometry.boundingBox;
const center = new THREE.Vector3();
box.getCenter(center);
mesh.position.sub(center);
mesh.rotation.set({rotation_x}*Math.PI/180, {rotation_y}*Math.PI/180, {rotation_z}*Math.PI/180);

const size = new THREE.Vector3();
box.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / 180);
let dist = maxDim / (2 * Math.tan(fov / 2)) * 1.15;
dist = Math.max(dist, 0.01);
camera.position.set(dist * 0.8, dist * 0.6, dist * 0.8);
camera.lookAt(0, 0, 0);

// Schaeffler green top bar
const topBar = document.createElement('div');
topBar.style.cssText =
  `position:fixed;top:0;left:0;width:${{W}}px;height:10px;background:#00893d;z-index:10`;
document.body.appendChild(topBar);

// Model name label
const label = document.createElement('div');
label.textContent = "{filename}";
label.style.cssText =
  `position:fixed;bottom:0;left:0;width:${{W}}px;background:rgba(20,35,55,0.85);` +
  `color:#fff;text-align:center;font-size:13px;font-family:monospace;` +
  `padding:6px 4px;box-sizing:border-box;z-index:10`;
document.body.appendChild(label);

renderer.render(scene, camera);
window.__renderDone = true;
</script>
</body>
</html>"""
