"""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 subprocess
import sys
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)

# Kam se ukládají hotové výstupy. Stahování odkazem funguje v prohlížeči,
# ale nativní okno aplikace (WKWebView) stahování nepodporuje — proto se
# soubory ukládají přímo z aplikace na disk.
OUTPUT_ROOT = os.path.expanduser("~/Documents/Bábofkátor")
SAVED: set[str] = set()      # složky, do kterých jsme sami uložili

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/save", methods=["POST"])
def save_outputs():
    """Uloží hotové STL a protokol do složky na disku."""
    data = request.get_json(force=True)
    d = _sdir(data.get("sid", ""))
    try:
        meta = json.load(open(os.path.join(d, "meta.json"), encoding="utf-8"))
        stem = os.path.splitext(meta["name"])[0]
    except Exception:
        stem = "babofkator"

    folder = (data.get("folder") or "").strip()
    if folder:
        target = os.path.join(os.path.expanduser(folder), stem)
    else:
        target = os.path.join(OUTPUT_ROOT, stem)
    base, k = target, 2
    while os.path.isdir(target) and os.listdir(target):
        target = f"{base} ({k})"
        k += 1

    try:
        os.makedirs(target, exist_ok=True)
        copied = []
        for fn in sorted(os.listdir(d)):
            if fn.startswith(("_", "vstup")) or fn == "meta.json":
                continue
            if fn.lower().endswith((".stl", ".json")):
                shutil.copy2(os.path.join(d, fn), os.path.join(target, fn))
                copied.append(fn)
    except Exception as exc:                                  # noqa: BLE001
        return jsonify(error=f"Uložení se nezdařilo: {exc}"), 500
    if not copied:
        return jsonify(error="Není co ukládat — nejdřív spusť výpočet."), 400
    SAVED.add(os.path.realpath(target))
    return jsonify(folder=target, files=copied)


@app.route("/api/reveal", methods=["POST"])
def reveal():
    """Otevře složku s výsledky ve Finderu."""
    folder = (request.get_json(force=True).get("folder") or "").strip()
    real = os.path.realpath(os.path.expanduser(folder))
    # otevřít smíme jen to, kam jsme sami ukládali
    if real not in SAVED and not real.startswith(os.path.realpath(OUTPUT_ROOT)):
        return jsonify(error="Neznámá složka."), 403
    if not os.path.isdir(real):
        return jsonify(error="Složka neexistuje."), 400
    try:
        if sys.platform == "darwin":
            subprocess.Popen(["open", real])
        elif os.name == "nt":
            os.startfile(real)                                # noqa: S606
        else:
            subprocess.Popen(["xdg-open", real])
    except Exception:                                         # noqa: BLE001
        return jsonify(error="Složku se nepodařilo otevřít."), 500
    return jsonify(ok=True)


@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()
