"""
PrintVault — STL-Bibliothek für ZimaOS
FastAPI Backend + Static Frontend
"""

import json
import os
import uuid
import shutil
import time
from pathlib import Path
from datetime import datetime
from typing import Optional

from fastapi import FastAPI, File, UploadFile, HTTPException, Query, Request
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import trimesh
import numpy as np

# ── Konfiguration ──────────────────────────────────────────

BASE_DIR = Path(os.environ.get("DATA_DIR", "/data"))
# /data wird vom Docker Volume-Mount gemounted
STL_DIR = BASE_DIR / "stls"
THUMB_DIR = BASE_DIR / "thumbs"
META_FILE = BASE_DIR / "metadata.json"

BASE_DIR.mkdir(parents=True, exist_ok=True)
STL_DIR.mkdir(parents=True, exist_ok=True)
THUMB_DIR.mkdir(parents=True, exist_ok=True)

app = FastAPI(title="PrintVault", version="1.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# ── Metadata Helpers ───────────────────────────────────────

def load_meta() -> dict:
    if META_FILE.exists():
        return json.loads(META_FILE.read_text())
    return {}

def save_meta(meta: dict):
    META_FILE.write_text(json.dumps(meta, indent=2, ensure_ascii=False))

@app.middleware("http")
async def add_frame_options(request: Request, call_next):
    """Allow iframe embedding for CasaOS dashboard."""
    response = await call_next(request)
    response.headers["X-Frame-Options"] = "ALLOWALL"
    response.headers["Content-Security-Policy"] = "frame-ancestors 'self' http://*:* https://*:*;"
    return response

def analyze_stl(path: Path) -> dict:
    """Trimesh-Analyse einer STL-Datei."""
    try:
        mesh = trimesh.load(str(path))

        # Bounding box
        extent = mesh.bounding_box.extents
        x, y, z = float(extent[0]), float(extent[1]), float(extent[2])

        # Sort for display: largest dimension first
        dims = sorted([x, y, z], reverse=True)

        # Volume (mm³ → cm³)
        volume_mm3 = float(mesh.volume) if hasattr(mesh, 'volume') and mesh.volume else 0
        volume_cm3 = volume_mm3 / 1000.0

        # Filament weight (PLA density: 1.24 g/cm³)
        filament_grams = round(volume_cm3 * 1.24, 1)

        # Faces
        faces = len(mesh.faces) if hasattr(mesh, 'faces') and mesh.faces is not None else 0

        # Watertight
        watertight = bool(mesh.is_watertight) if hasattr(mesh, 'is_watertight') else False

        # Manifold
        is_manifold = True
        try:
            from manifold3d import Manifold
            mesh_manifold = Manifold(trimesh_to_manifold(mesh))
            is_manifold = True  # kein Fehler = manifold
        except Exception:
            is_manifold = False

        return {
            "width_mm": round(dims[0], 1),
            "depth_mm": round(dims[1], 1),
            "height_mm": round(dims[2], 1),
            "volume_cm3": round(volume_cm3, 2),
            "filament_grams": filament_grams,
            "faces": faces,
            "watertight": watertight,
            "manifold": is_manifold,
        }
    except Exception as e:
        return {
            "width_mm": 0,
            "depth_mm": 0,
            "height_mm": 0,
            "volume_cm3": 0,
            "faces": 0,
            "watertight": False,
            "manifold": False,
            "error": str(e),
        }

def trimesh_to_manifold(mesh) -> tuple:
    """Trimesh Mesh → manifold3d Manifold inputs."""
    verts = np.array(mesh.vertices, dtype=np.float64)
    faces = np.array(mesh.faces, dtype=np.int32)
    return verts, faces

def generate_thumbnail(stl_path: Path, thumb_path: Path):
    """3D-PNG-Thumbnail aus STL rendern (matplotlib, headless-safe)."""
    try:
        import matplotlib
        matplotlib.use('Agg')
        import matplotlib.pyplot as plt
        from mpl_toolkits.mplot3d.art3d import Poly3DCollection

        mesh = trimesh.load(str(stl_path))

        fig = plt.figure(figsize=(5, 5), dpi=100)
        ax = fig.add_subplot(111, projection='3d')
        ax.set_facecolor('#f2f2f7')
        fig.patch.set_facecolor('#f2f2f7')

        verts = np.array(mesh.vertices)
        faces = np.array(mesh.faces)
        poly = [[verts[v] for v in f] for f in faces]
        coll = Poly3DCollection(
            poly,
            edgecolor='#007AFF',
            facecolor='#007AFF',
            alpha=0.8,
            linewidth=0.3,
        )
        ax.add_collection3d(coll)

        all_pts = np.array([p for tri in poly for p in tri])
        center = (all_pts.min(axis=0) + all_pts.max(axis=0)) / 2
        scale = np.ptp(all_pts).max() * 0.8
        ax.set_xlim(center[0] - scale, center[0] + scale)
        ax.set_ylim(center[1] - scale, center[1] + scale)
        ax.set_zlim(center[2] - scale, center[2] + scale)
        ax.axis('off')
        ax.view_init(elev=25, azim=-60)

        import io
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight',
                     pad_inches=0, transparent=False)
        buf.seek(0)
        thumb_path.write_bytes(buf.read())
        plt.close(fig)
        return True
    except Exception as e:
        print(f"Thumbnail error: {e}")
        return False

# ── API Routes ─────────────────────────────────────────────

@app.get("/api/files")
def list_files(
    sort: str = Query("date", regex="^(name|date|size|volume)$"),
    search: str = Query(""),
):
    """Alle STL-Dateien mit Analyse-Daten."""
    meta = load_meta()
    results = []

    for stl_file in sorted(STL_DIR.glob("*.stl"), key=lambda p: p.stat().st_mtime, reverse=True):
        fid = stl_file.stem
        entry = meta.get(fid, {})
        file_stat = stl_file.stat()

        results.append({
            "id": fid,
            "name": entry.get("name", stl_file.name.replace(".stl", "").replace("_", " ")),
            "original_name": stl_file.name,
            "size_kb": round(file_stat.st_size / 1024, 1),
            "uploaded_at": datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
            "tags": entry.get("tags", []),
            "note": entry.get("note", ""),
            "has_thumb": (THUMB_DIR / f"{fid}.png").exists(),
            "analysis": entry.get("analysis", {}),
        })

    # Search filter
    if search:
        q = search.lower()
        results = [
            r for r in results
            if q in r["name"].lower()
            or any(q in t.lower() for t in r.get("tags", []))
        ]

    # Sort
    if sort == "name":
        results.sort(key=lambda r: r["name"].lower())
    elif sort == "size":
        results.sort(key=lambda r: r["size_kb"], reverse=True)
    elif sort == "volume":
        results.sort(key=lambda r: r.get("analysis", {}).get("volume_cm3", 0), reverse=True)
    # default = date (already sorted by mtime)

    return results


@app.get("/api/files/{file_id}")
def get_file_detail(file_id: str):
    """Detail-Ansicht einer STL."""
    meta = load_meta()
    stl_path = STL_DIR / f"{file_id}.stl"

    if not stl_path.exists():
        raise HTTPException(404, "Datei nicht gefunden")

    entry = meta.get(file_id, {})
    if not entry.get("analysis"):
        analysis = analyze_stl(stl_path)
        entry["analysis"] = analysis
        meta[file_id] = entry
        save_meta(meta)

    return {
        "id": file_id,
        "name": entry.get("name", stl_path.name.replace(".stl", "").replace("_", " ")),
        "original_name": stl_path.name,
        "size_kb": round(stl_path.stat().st_size / 1024, 1),
        "uploaded_at": datetime.fromtimestamp(stl_path.stat().st_mtime).isoformat(),
        "tags": entry.get("tags", []),
        "note": entry.get("note", ""),
        "has_thumb": (THUMB_DIR / f"{file_id}.png").exists(),
        "analysis": entry.get("analysis", {}),
    }


@app.post("/api/upload")
async def upload_stl(file: UploadFile = File(...)):
    """STL-Datei hochladen und analysieren."""
    if not file.filename.lower().endswith(".stl"):
        raise HTTPException(400, "Nur .stl-Dateien erlaubt")

    fid = str(uuid.uuid4())[:8]
    stl_path = STL_DIR / f"{fid}.stl"
    content = await file.read()
    stl_path.write_bytes(content)

    if stl_path.stat().st_size == 0:
        stl_path.unlink()
        raise HTTPException(400, "Leere Datei")

    # Analyse
    analysis = analyze_stl(stl_path)

    # Thumbnail
    thumb_path = THUMB_DIR / f"{fid}.png"
    generate_thumbnail(stl_path, thumb_path)

    # Metadata
    meta = load_meta()
    meta[fid] = {
        "name": file.filename.replace(".stl", "").replace("_", " "),
        "tags": [],
        "note": "",
        "analysis": analysis,
    }
    save_meta(meta)

    return {
        "id": fid,
        "name": meta[fid]["name"],
        "size_kb": round(stl_path.stat().st_size / 1024, 1),
        "analysis": analysis,
        "has_thumb": thumb_path.exists(),
    }


@app.patch("/api/files/{file_id}")
async def update_file(file_id: str, body: dict):
    """Name, Tags, Notiz updaten."""
    meta = load_meta()
    if file_id not in meta:
        raise HTTPException(404, "Datei nicht gefunden")

    if "name" in body:
        meta[file_id]["name"] = body["name"]
    if "tags" in body:
        meta[file_id]["tags"] = body["tags"]
    if "note" in body:
        meta[file_id]["note"] = body["note"]

    save_meta(meta)
    return {"ok": True}


@app.delete("/api/files/{file_id}")
def delete_file(file_id: str):
    """STL + Thumbnail löschen."""
    meta = load_meta()
    stl_path = STL_DIR / f"{file_id}.stl"
    thumb_path = THUMB_DIR / f"{file_id}.png"

    if stl_path.exists():
        stl_path.unlink()
    if thumb_path.exists():
        thumb_path.unlink()
    if file_id in meta:
        del meta[file_id]
        save_meta(meta)

    return {"ok": True}


@app.get("/api/thumbs/{file_id}.png")
def get_thumbnail(file_id: str):
    """Thumbnail-PNG liefern."""
    thumb_path = THUMB_DIR / f"{file_id}.png"
    if not thumb_path.exists():
        # Fallback: generiere jetzt
        stl_path = STL_DIR / f"{file_id}.stl"
        if not stl_path.exists():
            raise HTTPException(404, "Kein Thumbnail")
        generate_thumbnail(stl_path, thumb_path)

    return FileResponse(thumb_path, media_type="image/png")


@app.get("/api/stats")
def get_stats():
    """Schnelle Statistiken."""
    stl_files = list(STL_DIR.glob("*.stl"))
    total_size = sum(f.stat().st_size for f in stl_files)
    meta = load_meta()

    watertight_count = sum(
        1 for e in meta.values()
        if e.get("analysis", {}).get("watertight", False)
    )

    return {
        "total_files": len(stl_files),
        "total_size_mb": round(total_size / 1024 / 1024, 1),
        "watertight": watertight_count,
        "not_watertight": len(stl_files) - watertight_count,
    }


@app.get("/api/download/{file_id}")
def download_stl(file_id: str):
    """STL-Datei zum Download bereitstellen."""
    stl_path = STL_DIR / f"{file_id}.stl"
    if not stl_path.exists():
        raise HTTPException(404, "Datei nicht gefunden")

    meta = load_meta()
    entry = meta.get(file_id, {})
    filename = entry.get("name", file_id).replace(" ", "_") + ".stl"

    return FileResponse(
        stl_path,
        media_type="application/octet-stream",
        filename=filename,
    )


# ── Static Frontend ────────────────────────────────────────

FRONTEND_DIR = Path(__file__).parent / "frontend"
FRONTEND_DIR.mkdir(exist_ok=True)

@app.get("/")
def serve_frontend():
    return FileResponse(FRONTEND_DIR / "index.html")

# Static assets
if (FRONTEND_DIR / "assets").exists():
    app.mount("/assets", StaticFiles(directory=FRONTEND_DIR / "assets"), name="assets")

@app.get("/icon.svg")
def serve_icon():
    return FileResponse(FRONTEND_DIR / "icon.svg", media_type="image/svg+xml")

# ── Startup ────────────────────────────────────────────────

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=3001)
