#!/usr/bin/env python3
"""Serve this workspace with UTF-8 text response headers.

Python's default ``python3 -m http.server`` can serve Markdown as text without a
charset, which lets browsers guess the wrong encoding for Korean text.  This
keeps the same simple static-server behavior while making text-like files
explicitly UTF-8.
"""
from __future__ import annotations

import argparse
import base64
import json
import re
import time
import uuid
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import quote, unquote, urlparse


UTF8_EXTENSIONS = {
    ".css",
    ".csv",
    ".htm",
    ".html",
    ".js",
    ".json",
    ".map",
    ".markdown",
    ".md",
    ".mjs",
    ".svg",
    ".txt",
    ".xml",
}

ROOT = Path(__file__).resolve().parents[1]
HUD_STATE_PATH = ROOT / "data" / "hud_trace_state.json"
HUD_UPLOAD_DIR = ROOT / "data" / "hud_uploads"
HUD_MAX_UPLOAD_BYTES = 16 * 1024 * 1024
HUD_MAX_JSON_BYTES = 24 * 1024 * 1024
HUD_UPLOAD_TYPES = {
    "image/png": ".png",
    "image/jpeg": ".jpg",
    "image/webp": ".webp",
    "image/gif": ".gif",
}
HUD_UI_CNS_PRIORITY = [
    "frame.cns",
    "window.cns",
    "status.cns",
    "num.cns",
    "icon.cns",
    "item.cns",
    "btl_etc.cns",
    "face_01.cns",
    "01234567.cns",
    "cara_fnt.cns",
    "title.cns",
    "logo_00.cns",
    "compile.cns",
    "aaa.cns",
]


def default_hud_state() -> dict:
    return {
        "version": 1,
        "kind": "hwanse-hud-trace-workbench",
        "activeSheetId": "normal-ui",
        "updatedAt": None,
        "sheets": {
            "normal-ui": {
                "id": "normal-ui",
                "name": "평상시 UI",
                "createdAt": None,
                "updatedAt": None,
                "screenshot": None,
                "placements": [],
                "annotations": [],
            }
        },
    }


def atomic_write_json(path: Path, payload: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    tmp.replace(path)


class Utf8StaticHandler(SimpleHTTPRequestHandler):
    def guess_type(self, path: str) -> str:
        content_type = super().guess_type(path)
        suffix = Path(path).suffix.lower()
        if suffix in UTF8_EXTENSIONS and "charset=" not in content_type.lower():
            return f"{content_type}; charset=utf-8"
        return content_type

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        if parsed.path == "/web/hud/api/state":
            self._send_hud_state()
            return
        if parsed.path == "/web/hud/api/cns-list":
            self._send_hud_cns_list()
            return
        if parsed.path.startswith("/web/hud/api/uploads/"):
            self._send_hud_upload(parsed.path.rsplit("/", 1)[-1])
            return
        super().do_GET()

    def do_POST(self) -> None:
        parsed = urlparse(self.path)
        if parsed.path == "/web/hud/api/state":
            self._save_hud_state()
            return
        if parsed.path == "/web/hud/api/upload":
            self._save_hud_upload()
            return
        self.send_error(404, "unknown HUD API endpoint")

    def _read_json(self, max_bytes: int = HUD_MAX_JSON_BYTES) -> dict:
        try:
            length = int(self.headers.get("Content-Length", "0"))
        except ValueError as exc:
            raise ValueError("invalid Content-Length") from exc
        if length <= 0:
            raise ValueError("empty request body")
        if length > max_bytes:
            raise ValueError(f"request body too large: {length} bytes")
        raw = self.rfile.read(length)
        return json.loads(raw.decode("utf-8"))

    def _send_json(self, payload: dict, status: int = 200) -> None:
        body = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _send_hud_state(self) -> None:
        if HUD_STATE_PATH.exists():
            try:
                payload = json.loads(HUD_STATE_PATH.read_text(encoding="utf-8"))
            except json.JSONDecodeError:
                payload = default_hud_state()
                payload["loadWarning"] = "stored HUD trace state was invalid JSON; default state returned"
        else:
            payload = default_hud_state()
        self._send_json(payload)

    def _save_hud_state(self) -> None:
        try:
            payload = self._read_json()
            if not isinstance(payload, dict):
                raise ValueError("state must be a JSON object")
            if payload.get("kind") != "hwanse-hud-trace-workbench":
                raise ValueError("state kind must be hwanse-hud-trace-workbench")
            if not isinstance(payload.get("sheets"), dict):
                raise ValueError("state.sheets must be an object")
            payload["updatedAt"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
            atomic_write_json(HUD_STATE_PATH, payload)
        except (ValueError, json.JSONDecodeError) as exc:
            self._send_json({"ok": False, "error": str(exc)}, status=400)
            return
        self._send_json({"ok": True, "path": str(HUD_STATE_PATH.relative_to(ROOT))})

    def _save_hud_upload(self) -> None:
        try:
            payload = self._read_json()
            data_url = str(payload.get("dataUrl") or "")
            match = re.fullmatch(r"data:(image/(?:png|jpeg|webp|gif));base64,(.+)", data_url, re.DOTALL)
            if not match:
                raise ValueError("upload must be a base64 data URL for png/jpeg/webp/gif")
            mime = match.group(1)
            raw = base64.b64decode(match.group(2), validate=True)
            if len(raw) > HUD_MAX_UPLOAD_BYTES:
                raise ValueError(f"upload too large: {len(raw)} bytes; limit {HUD_MAX_UPLOAD_BYTES}")
            original = Path(str(payload.get("name") or "capture")).stem
            safe = re.sub(r"[^A-Za-z0-9._-]+", "-", original).strip("-._") or "capture"
            filename = f"{time.strftime('%Y%m%d-%H%M%S', time.gmtime())}-{uuid.uuid4().hex[:8]}-{safe}{HUD_UPLOAD_TYPES[mime]}"
            HUD_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
            (HUD_UPLOAD_DIR / filename).write_bytes(raw)
        except (ValueError, json.JSONDecodeError) as exc:
            self._send_json({"ok": False, "error": str(exc)}, status=400)
            return
        self._send_json({
            "ok": True,
            "name": filename,
            "url": f"/web/hud/api/uploads/{quote(filename)}",
            "mime": mime,
            "size": len(raw),
        })

    def _send_hud_upload(self, encoded_name: str) -> None:
        name = unquote(encoded_name)
        if "/" in name or "\\" in name or name.startswith("."):
            self.send_error(404, "upload not found")
            return
        path = HUD_UPLOAD_DIR / name
        if not path.exists() or not path.is_file():
            self.send_error(404, "upload not found")
            return
        suffix = path.suffix.lower()
        content_type = {
            ".png": "image/png",
            ".jpg": "image/jpeg",
            ".jpeg": "image/jpeg",
            ".webp": "image/webp",
            ".gif": "image/gif",
        }.get(suffix, "application/octet-stream")
        body = path.read_bytes()
        self.send_response(200)
        self.send_header("Content-Type", content_type)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _send_hud_cns_list(self) -> None:
        extract_dir = ROOT / "extract_fld"
        priority = {name: index for index, name in enumerate(HUD_UI_CNS_PRIORITY)}
        items = []
        if extract_dir.exists():
            for path in extract_dir.glob("*.cns"):
                name = path.name
                lower = name.lower()
                recommended = lower in priority or lower.startswith(("ui", "window", "frame", "status", "num", "icon", "item", "btl_", "face", "title", "logo"))
                items.append({
                    "name": name,
                    "stem": path.stem,
                    "url": f"/extract_fld/{quote(name)}",
                    "size": path.stat().st_size,
                    "uiRecommended": recommended,
                    "sortRank": priority.get(lower, 1000),
                })
        items.sort(key=lambda row: (0 if row["uiRecommended"] else 1, row["sortRank"], row["name"]))
        self._send_json({"ok": True, "count": len(items), "items": items})


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("port", nargs="?", type=int, default=8013)
    parser.add_argument("--bind", default="0.0.0.0")
    parser.add_argument("--directory", default=".")
    args = parser.parse_args()

    handler = partial(Utf8StaticHandler, directory=args.directory)
    server = ThreadingHTTPServer((args.bind, args.port), handler)
    print(f"serving {Path(args.directory).resolve()} on http://{args.bind}:{args.port}/ with UTF-8 text headers")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


if __name__ == "__main__":
    main()
