"""Lokální webové rozhraní Babofkátoru (Flask + three.js).

Spuštění:
    python3 -m babofkator.webapp            # http://127.0.0.1:8765
    babofkator-gui
"""
from __future__ import annotations

import os
import io
import json
import shutil
import tempfile
import threading
import traceback
import uuid
import webbrowser

import numpy as np
from flask import (Flask, jsonify, request, send_file, send_from_directory,
                   abort)

from . import __version__, meshing, mold as moldmod
from .pipeline import Params, run as run_pipeline, guess_up_axis, orient_up
from .preview import make_maps
from .stlio import stl_bytes, read_mesh

HERE = os.path.dirname(os.path.abspath(__file__))
STATIC = os.path.join(HERE, "static")

app = Flask(__name__, static_folder=None)
app.config["MAX_CONTENT_LENGTH"] = 512 * 1024 * 1024

ROOT = os.path.join(tempfile.gettempdir(), "babofkator")
os.makedirs(ROOT, exist_ok=True)

JOBS: dict[str, dict] = {}
LOCK = threading.Lock()


# --------------------------------------------------------------------------- #
def _sdir(sid: str) -> str:
    if not sid or not sid.replace("-", "").isalnum():
        abort(400)
    d = os.path.join(ROOT, sid)
    os.makedirs(d, exist_ok=True)
    return d


def _preview_bytes(result, which: str) -> bytes:
    """Odlehčený mesh pro náhled v prohlížeči."""
    g = result.grid
    ext = max(g.nx, g.ny) * g.res
    tol = max(result.params.get("mesh_tol_mm", 0.03), ext / 900.0)
    if which == "model":
        node = result.h > 1e-9
        cm = meshing.cells_from_nodes(node)
        V, F = meshing.heightfields_to_mesh_adaptive(
            g, np.zeros_like(result.h), result.h, cm, tol=tol)
    elif which == "original":
        node = result.h0 > 1e-9
        cm = meshing.cells_from_nodes(node)
        V, F = meshing.heightfields_to_mesh_adaptive(
            g, np.zeros_like(result.h0), result.h0, cm, tol=tol)
    else:
        b, t, cm, _ = moldmod.build_mold(
            g, result.h, wall_mm=result.params["wall_mm"],
            rim_mm=result.params["rim_mm"], back=result.params["back"],
            rim_smooth_mm=result.params.get("rim_smooth_mm", -1.0))
        V, F = meshing.heightfields_to_mesh_adaptive(g, b, t, cm, tol=tol)
    return stl_bytes(V, F, header=f"Babofkator preview {which}")


def _work(job_id: str, sid: str, path: str, params: Params,
          meta: dict | None = None):
    def prog(pct, msg):
        with LOCK:
            JOBS[job_id].update(pct=int(pct), msg=msg)

    try:
        res = run_pipeline(path, params, progress=prog)
        d = _sdir(sid)
        stem = os.path.splitext((meta or {}).get("name")
                                or os.path.basename(path))[0]
        files = {}
        if res.model_mesh is not None:
            p = os.path.join(d, f"{stem}_korigovany.stl")
            open(p, "wb").write(stl_bytes(*res.model_mesh,
                                          header="Babofkator model"))
            files["model"] = os.path.basename(p)
        if res.mold_mesh is not None:
            p = os.path.join(d, f"{stem}_forma.stl")
            open(p, "wb").write(stl_bytes(*res.mold_mesh,
                                          header="Babofkator forma"))
            files["mold"] = os.path.basename(p)
        rp = os.path.join(d, f"{stem}_protokol.json")
        with open(rp, "w", encoding="utf-8") as fh:
            json.dump({"params": res.params, "report": res.report}, fh,
                      ensure_ascii=False, indent=2, default=str)
        files["report"] = os.path.basename(rp)

        with LOCK:
            JOBS[job_id].update(pct=90, msg="Připravuji náhledy…")
        for which in ("original", "model", "mold"):
            try:
                open(os.path.join(d, f"_prev_{which}.stl"), "wb").write(
                    _preview_bytes(res, which))
            except Exception:
                pass

        maps = {}
        try:
            maps = make_maps(res)
        except Exception:
            pass

        counts = {
            "model": int(res.model_mesh[1].shape[0]) if res.model_mesh else 0,
            "mold": int(res.mold_mesh[1].shape[0]) if res.mold_mesh else 0,
        }
        with LOCK:
            JOBS[job_id].update(pct=100, msg="Hotovo.", done=True,
                                result={"report": res.report,
                                        "params": res.params,
                                        "files": files, "maps": maps,
                                        "tri_counts": counts})
    except Exception as exc:                                  # noqa: BLE001
        with LOCK:
            JOBS[job_id].update(done=True, error=str(exc),
                                trace=traceback.format_exc())


# --------------------------------------------------------------------------- #
@app.route("/")
def index():
    return send_from_directory(STATIC, "index.html")


@app.route("/static/<path:fn>")
def static_files(fn):
    return send_from_directory(STATIC, fn)


@app.route("/api/version")
def version():
    return jsonify(version=__version__)


@app.route("/api/upload", methods=["POST"])
def upload():
    f = request.files.get("file")
    if f is None or not f.filename:
        return jsonify(error="Chybí soubor."), 400
    sid = uuid.uuid4().hex
    d = _sdir(sid)
    name = os.path.basename(f.filename).replace(" ", "_")
    ext = os.path.splitext(name)[1].lower()
    if ext not in (".stl", ".obj", ".ply"):
        ext = ".stl"
    path = os.path.join(d, "vstup" + ext)
    f.save(path)
    try:
        V, F = read_mesh(path, name)
    except Exception as exc:                                  # noqa: BLE001
        shutil.rmtree(d, ignore_errors=True)
        return jsonify(error=f"Model se nepodařilo načíst: {exc}"), 400

    axis = guess_up_axis(V)
    Vz = orient_up(V, axis)
    size = (Vz.max(axis=0) - Vz.min(axis=0)).tolist()
    with open(os.path.join(d, "meta.json"), "w", encoding="utf-8") as fh:
        json.dump({"name": name, "file": "vstup" + ext, "axis": axis}, fh)
    # náhled originálu pro prohlížeč vždy jako STL
    with open(os.path.join(d, "_orig.stl"), "wb") as fh:
        fh.write(stl_bytes(Vz, F, header="Babofkator original"))
    return jsonify(sid=sid, name=name, faces=int(F.shape[0]), axis=axis,
                   size_mm=[round(x, 3) for x in size])


@app.route("/api/run", methods=["POST"])
def start():
    data = request.get_json(force=True)
    sid = data.get("sid", "")
    d = _sdir(sid)
    try:
        meta = json.load(open(os.path.join(d, "meta.json"), encoding="utf-8"))
    except Exception:
        return jsonify(error="Nejprve nahraj model."), 400
    path = os.path.join(d, meta["file"])

    fields = {k: v for k, v in data.items() if k in Params.__dataclass_fields__}
    for k in ("draft_deg", "wall_mm", "rim_mm", "resolution_mm",
              "smooth_px", "mesh_tol_mm", "safety", "rim_smooth_mm"):
        if k in fields and fields[k] is not None:
            fields[k] = float(fields[k])
    for k in ("target_cells", "max_cells"):
        if k in fields and fields[k] is not None:
            fields[k] = int(fields[k])
    params = Params(**fields)
    params._name = meta["name"]

    job_id = uuid.uuid4().hex
    with LOCK:
        JOBS[job_id] = {"pct": 0, "msg": "Start…", "done": False,
                        "error": None, "result": None}
    threading.Thread(target=_work, args=(job_id, sid, path, params, meta),
                     daemon=True).start()
    return jsonify(job=job_id)


@app.route("/api/progress/<job>")
def progress(job):
    with LOCK:
        j = JOBS.get(job)
        if j is None:
            return jsonify(error="Neznámá úloha."), 404
        return jsonify(j)


@app.route("/api/file/<sid>/<path:fn>")
def get_file(sid, fn):
    d = _sdir(sid)
    fn = os.path.basename(fn)
    p = os.path.join(d, fn)
    if not os.path.exists(p):
        abort(404)
    return send_file(p, as_attachment=not fn.startswith("_prev"),
                     download_name=fn)


@app.route("/api/original/<sid>")
def original(sid):
    p = os.path.join(_sdir(sid), "_orig.stl")
    if not os.path.exists(p):
        abort(404)
    return send_file(p)


def main():
    import argparse
    ap = argparse.ArgumentParser(description="Babofkátor — webové rozhraní")
    ap.add_argument("--port", type=int, default=8765)
    ap.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--no-browser", action="store_true")
    a = ap.parse_args()
    url = f"http://{a.host}:{a.port}/"
    print(f"Babofkátor {__version__} — {url}")
    if not a.no_browser:
        threading.Timer(1.2, lambda: webbrowser.open(url)).start()
    app.run(host=a.host, port=a.port, debug=False, threaded=True)


if __name__ == "__main__":
    main()
