#!/usr/bin/env python3
"""Local Executive Desk / Last Click — static files + submit + desk reply loop."""
from __future__ import annotations

import json
import mimetypes
import re
import ssl
import threading
import uuid
from datetime import datetime, timezone
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse
from zoneinfo import ZoneInfo

ROOT = Path(__file__).resolve().parent
STATE = ROOT / "state"
CERT_DIR = ROOT / "certs"
PORT = 8765
HTTPS_PORT = 8443
CT = ZoneInfo("America/Chicago")


def now_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def now_ct() -> str:
    return datetime.now(CT).strftime("%Y-%m-%dT%H:%M:%S%z")


def read_json(path: Path, default):
    if not path.exists():
        return default
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default


def write_json(path: Path, data) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2), encoding="utf-8")


def parse_desk_md(path: Path) -> dict:
    """Parse # submit / # reply markdown blocks."""
    if not path.exists():
        return {}
    text = path.read_text(encoding="utf-8")
    out: dict = {"raw": text, "kind": "", "time": "", "from": "", "text": ""}
    m = re.search(r"^#\s*(submit|reply)\s*$", text, re.M | re.I)
    if m:
        out["kind"] = m.group(1).lower()
    tm = re.search(r"^time:\s*(.+)$", text, re.M)
    if tm:
        out["time"] = tm.group(1).strip()
    fr = re.search(r"^from:\s*(.+)$", text, re.M)
    if fr:
        out["from"] = fr.group(1).strip()
    tx = re.search(r"^text:\s*\|\s*\n((?:[ \t].*\n?)*)", text, re.M)
    if tx:
        body = tx.group(1)
        lines = []
        for line in body.splitlines():
            if line.startswith("  "):
                lines.append(line[2:])
            elif line.startswith("\t"):
                lines.append(line[1:])
            else:
                lines.append(line)
        out["text"] = "\n".join(lines).rstrip()
    elif "text:" in text:
        # single-line fallback
        sm = re.search(r"^text:\s*(.+)$", text, re.M)
        if sm and not sm.group(1).strip().startswith("|"):
            out["text"] = sm.group(1).strip()
    return out


def write_submit_md(text: str, from_who: str = "phone") -> str:
    ts = now_ct()
    # YAML-ish block with indented body
    body = "\n".join(("  " + ln if ln else "  ") for ln in (text or "").splitlines()) or "  "
    md = f"""# submit
time: {ts}
from: {from_who}
text: |
{body}
"""
    STATE.mkdir(parents=True, exist_ok=True)
    (STATE / "latest_submit.md").write_text(md, encoding="utf-8")
    with (STATE / "chat_log.md").open("a", encoding="utf-8") as f:
        f.write(f"{ts} | {from_who} | {text.replace(chr(10), ' ')[:500]}\n")
    (STATE / "CHAT_READY").write_text(ts + "\n", encoding="utf-8")
    return ts


def write_reply_md(text: str, from_who: str = "eve-local") -> str:
    ts = now_ct()
    body = "\n".join(("  " + ln if ln else "  ") for ln in (text or "").splitlines()) or "  "
    md = f"""# reply
time: {ts}
from: {from_who}
text: |
{body}
"""
    STATE.mkdir(parents=True, exist_ok=True)
    (STATE / "latest_reply.md").write_text(md, encoding="utf-8")
    with (STATE / "chat_log.md").open("a", encoding="utf-8") as f:
        f.write(f"{ts} | {from_who} | {text.replace(chr(10), ' ')[:500]}\n")
    return ts


class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=str(ROOT), **kwargs)

    def end_headers(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.send_header("Cache-Control", "no-store")
        super().end_headers()

    def do_OPTIONS(self):
        self.send_response(204)
        self.end_headers()

    def _json_response(self, code: int, obj) -> None:
        body = json.dumps(obj).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _read_body(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length) if length else b"{}"
        try:
            return json.loads(raw.decode("utf-8") or "{}")
        except json.JSONDecodeError:
            return None

    def do_GET(self):
        path = urlparse(self.path).path.rstrip("/") or "/"
        if path == "/api/workers":
            return self._json_response(200, read_json(STATE / "workers.json", {"workers": []}))
        if path == "/api/chat/thread":
            return self._json_response(200, read_json(STATE / "chat_thread.json", {"messages": []}))
        if path == "/api/queue":
            return self._json_response(200, read_json(STATE / "queue.json", {"items": []}))
        if path == "/api/reply":
            parsed = parse_desk_md(STATE / "latest_reply.md")
            audio_path = STATE / "latest_reply.mp3"
            has_audio = audio_path.exists() and audio_path.stat().st_size > 200
            return self._json_response(
                200,
                {
                    "ok": True,
                    "time": parsed.get("time") or "",
                    "from": parsed.get("from") or "",
                    "text": parsed.get("text") or "",
                    "exists": (STATE / "latest_reply.md").exists(),
                    "audio": has_audio,
                    "audio_url": "/api/reply/audio" if has_audio else "",
                },
            )
        if path == "/api/reply/audio":
            audio_path = STATE / "latest_reply.mp3"
            if not audio_path.exists():
                return self.send_error(404, "No Eve voice clip yet")
            data = audio_path.read_bytes()
            self.send_response(200)
            self.send_header("Content-Type", "audio/mpeg")
            self.send_header("Content-Length", str(len(data)))
            self.send_header("Cache-Control", "no-store")
            self.send_header("Access-Control-Allow-Origin", "*")
            self.end_headers()
            self.wfile.write(data)
            return
        if path == "/api/submit/latest":
            parsed = parse_desk_md(STATE / "latest_submit.md")
            return self._json_response(
                200,
                {
                    "ok": True,
                    "time": parsed.get("time") or "",
                    "from": parsed.get("from") or "",
                    "text": parsed.get("text") or "",
                    "exists": (STATE / "latest_submit.md").exists(),
                },
            )
        if path == "/api/metrics":
            metrics_path = STATE / "metrics.json"
            # Refresh if missing or older than 5 minutes
            try:
                import time as _time
                from subprocess import Popen
                stale = True
                if metrics_path.exists():
                    age = _time.time() - metrics_path.stat().st_mtime
                    stale = age > 300
                if stale:
                    Popen(
                        ["python3", str(ROOT / "analytics" / "fetch_plausible.py")],
                        cwd=str(ROOT),
                        stdout=open("/tmp/plausible_fetch.log", "a"),
                        stderr=open("/tmp/plausible_fetch.log", "a"),
                    )
            except Exception:
                pass
            if not metrics_path.exists():
                return self._json_response(
                    200,
                    {
                        "ok": False,
                        "error": "no_metrics_yet",
                        "hint": "Run: python3 analytics/fetch_plausible.py (needs PLAUSIBLE_API_KEY in analytics/.env)",
                        "realtime": None,
                        "today": {},
                        "days30": {},
                    },
                )
            return self._json_response(200, read_json(metrics_path, {"ok": False}))
        return super().do_GET()

    def do_POST(self):
        path = urlparse(self.path).path.rstrip("/")
        payload = self._read_body()
        if payload is None:
            return self._json_response(400, {"ok": False, "error": "Invalid JSON"})

        if path == "/api/submit":
            return self._submit_pack(payload)
        if path == "/api/chat":
            return self._chat(payload)
        if path == "/api/reply":
            return self._reply(payload)
        if path == "/api/workers/status":
            return self._worker_status(payload)
        self.send_error(404, "Not found")

    def _submit_pack(self, payload: dict) -> None:
        STATE.mkdir(parents=True, exist_ok=True)
        ts = now_iso()
        payload["_submitted_at"] = ts
        text = payload.get("pack") or json.dumps(payload, indent=2)
        write_json(STATE / "latest_submit.json", payload)
        write_json(STATE / f"submit_{ts.replace(':', '')}.json", payload)
        (STATE / "latest_submit.md").write_text(
            text if str(text).startswith("LAST CLICK") or str(text).startswith("# submit") else f"LAST CLICK submit — {ts}\n\n{text}\n",
            encoding="utf-8",
        )
        (STATE / "READY_FOR_EVE").write_text(ts + "\n", encoding="utf-8")
        self._json_response(200, {"ok": True, "at": ts, "path": str(STATE / "latest_submit.md")})

    def _chat(self, payload: dict) -> None:
        """Phone/desk send → latest_submit.md. No fake Eve reply. Phone polls /api/reply."""
        STATE.mkdir(parents=True, exist_ok=True)
        text = (payload.get("text") or "").strip()
        files = payload.get("files") or []
        from_who = (payload.get("from") or "phone").strip() or "phone"
        if not text and not files:
            return self._json_response(400, {"ok": False, "error": "Empty message"})
        if files and not text:
            text = "(attachment) " + ", ".join(
                (f.get("name") if isinstance(f, dict) else str(f)) for f in files
            )
        ts = write_submit_md(text, from_who=from_who)

        thread_path = STATE / "chat_thread.json"
        thread = read_json(thread_path, {"messages": []})
        user_msg = {
            "id": str(uuid.uuid4()),
            "role": "user",
            "text": text,
            "files": files,
            "at": ts,
        }
        thread.setdefault("messages", []).append(user_msg)
        write_json(thread_path, thread)

        inbox = read_json(STATE / "chat_inbox.json", {"pending": []})
        inbox.setdefault("pending", []).append(user_msg)
        write_json(STATE / "chat_inbox.json", inbox)

        workers = read_json(STATE / "workers.json", {"workers": []})
        for w in workers.get("workers", []):
            if w.get("id") == "eve":
                w["status"] = "waiting"
                w["detail"] = "Desk message waiting on local Eve"
        workers["updated"] = now_iso()
        write_json(STATE / "workers.json", workers)

        self._json_response(
            200,
            {
                "ok": True,
                "saved": True,
                "status": "waiting_on_local_eve",
                "time": ts,
                "user": user_msg,
            },
        )

    def _reply(self, payload: dict) -> None:
        """TUI / desk_reply.py writes Eve's answer for the phone to poll."""
        text = (payload.get("text") or "").strip()
        if not text:
            return self._json_response(400, {"ok": False, "error": "Empty reply"})
        from_who = (payload.get("from") or "eve-local").strip() or "eve-local"
        ts = write_reply_md(text, from_who=from_who)

        thread = read_json(STATE / "chat_thread.json", {"messages": []})
        eve_msg = {
            "id": str(uuid.uuid4()),
            "role": "eve",
            "text": text,
            "at": ts,
        }
        thread.setdefault("messages", []).append(eve_msg)
        write_json(STATE / "chat_thread.json", thread)

        workers = read_json(STATE / "workers.json", {"workers": []})
        for w in workers.get("workers", []):
            if w.get("id") == "eve":
                w["status"] = "here"
                w["detail"] = "In the room with you"
        workers["updated"] = now_iso()
        write_json(STATE / "workers.json", workers)

        self._json_response(200, {"ok": True, "time": ts, "from": from_who})

    def _worker_status(self, payload: dict) -> None:
        workers = read_json(STATE / "workers.json", {"workers": []})
        wid = payload.get("id")
        for w in workers.get("workers", []):
            if w.get("id") == wid:
                if "status" in payload:
                    w["status"] = payload["status"]
                if "detail" in payload:
                    w["detail"] = payload["detail"]
        workers["updated"] = now_iso()
        write_json(STATE / "workers.json", workers)
        self._json_response(200, {"ok": True, "workers": workers})

    def log_message(self, fmt, *args):
        print("[%s] %s" % (self.log_date_time_string(), fmt % args))


def _start_https() -> ThreadingHTTPServer | None:
    """Mic needs a secure context — Tailscale MagicDNS cert on :8443."""
    candidates = [
        (CERT_DIR / "dads-envy.tail75cbe7.ts.net.crt", CERT_DIR / "dads-envy.tail75cbe7.ts.net.key"),
    ]
    for crt in sorted(CERT_DIR.glob("*.crt")):
        candidates.append((crt, crt.with_suffix(".key")))
    cert = key = None
    for crt, ky in candidates:
        if crt.exists() and ky.exists():
            cert, key = crt, ky
            break
    if not cert or not key:
        print("HTTPS skip — no Tailscale cert in certs/ (run: tailscale cert dads-envy.tail75cbe7.ts.net)")
        return None
    https = ThreadingHTTPServer(("0.0.0.0", HTTPS_PORT), Handler)
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(str(cert), str(key))
    https.socket = ctx.wrap_socket(https.socket, server_side=True)
    print(f"HTTPS (mic): https://dads-envy.tail75cbe7.ts.net:{HTTPS_PORT}/EXEC_DESK_MOCK.html")
    return https


def main():
    STATE.mkdir(parents=True, exist_ok=True)
    if not (STATE / "chat_thread.json").exists():
        write_json(STATE / "chat_thread.json", {"messages": []})
    if not (STATE / "chat_log.md").exists():
        (STATE / "chat_log.md").write_text("# desk chat log\n", encoding="utf-8")
    mimetypes.add_type("application/javascript", ".js")
    httpd = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
    print(f"Executive Desk: http://0.0.0.0:{PORT}/EXEC_DESK_MOCK.html  (LAN/Tailscale OK)")
    print("Chat POST /api/chat → latest_submit.md · GET /api/reply · POST /api/reply")

    https = _start_https()
    if https:
        threading.Thread(target=https.serve_forever, daemon=True).start()

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nStopped.")
        if https:
            https.shutdown()


if __name__ == "__main__":
    main()
