#!/usr/bin/env python3
"""odysseus-theme — shell wrapper for the theme system.

Built-in preset definitions live in `static/js/theme.js` (the `THEMES`
export). Each user's chosen preset + custom-color overrides persist
in `data/user_prefs.json` under that user's key, so we can read/write
themes from the shell without needing the browser.

    odysseus-theme list                          # built-in preset names
    odysseus-theme users                         # users with a saved theme
    odysseus-theme get --user alice@example.com  # one user's theme blob
    odysseus-theme set --user alice@example.com --preset chatgpt
    odysseus-theme export --user alice@example.com --pretty > theme.json

`set --preset NAME` clears any custom-color overrides — pass
`--keep-colors` to preserve them (useful when switching presets but
keeping your manual tweaks).
"""

from __future__ import annotations
import sys
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
quiet_logs()

import argparse, json, logging, os, shutil, subprocess, sys, tempfile
from pathlib import Path

_USER_PREFS_PATH = _REPO_ROOT / "data" / "user_prefs.json"
_THEME_JS = _REPO_ROOT / "static" / "js" / "theme.js"


def _load_prefs() -> dict:
    if not _USER_PREFS_PATH.exists():
        return {"_users": {}}
    try:
        data = json.loads(_USER_PREFS_PATH.read_text())
    except json.JSONDecodeError as e:
        fail(f"user_prefs.json is corrupt: {e}")
    if not isinstance(data, dict):
        fail("user_prefs.json is corrupt: expected an object")
    users = data.setdefault("_users", {})
    if not isinstance(users, dict):
        fail("user_prefs.json is corrupt: _users must be an object")
    return data


def _save_prefs(data: dict) -> None:
    _USER_PREFS_PATH.parent.mkdir(parents=True, exist_ok=True)
    # Backup so a bad write doesn't nuke everyone's theme.
    if _USER_PREFS_PATH.exists():
        try:
            (_USER_PREFS_PATH.with_suffix(_USER_PREFS_PATH.suffix + ".bak")).write_bytes(
                _USER_PREFS_PATH.read_bytes()
            )
        except Exception:
            pass
    tmp = _USER_PREFS_PATH.with_suffix(_USER_PREFS_PATH.suffix + ".tmp")
    tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False))
    tmp.replace(_USER_PREFS_PATH)


def _builtin_preset_names() -> list[str]:
    """Read the THEMES export from theme.js. Uses node so we don't
    have to write a JS parser. Falls back to a regex scrape if node
    isn't available."""
    if not _THEME_JS.exists():
        return []
    if shutil.which("node"):
        script = (
            f"const m = await import('{_THEME_JS.as_posix()}'); "
            "console.log(JSON.stringify(Object.keys(m.THEMES || {})));"
        )
        try:
            out = subprocess.run(
                ["node", "--input-type=module", "-e", script],
                capture_output=True, text=True, timeout=10,
            )
            if out.returncode == 0:
                lines = [l for l in out.stdout.splitlines() if l.strip()]
                if lines:
                    return json.loads(lines[-1])
        except Exception:
            pass
    # Fallback: regex-scrape from the file. Less precise than node,
    # but works without a JS runtime. We only match keys at depth 1
    # (exactly 2 leading spaces) so nested `advanced: { userBubbleBg: ... }`
    # entries don't leak into the preset list.
    import re
    src = _THEME_JS.read_text()
    m = re.search(r"export\s+const\s+THEMES\s*=\s*\{(.*?)\n\};", src, re.DOTALL)
    if not m:
        return []
    body = m.group(1)
    return re.findall(r"^  ([a-zA-Z_][\w]*)\s*:", body, re.MULTILINE)


# ─── list ────────────────────────────────────────────────────────────

def cmd_list(args):
    names = _builtin_preset_names()
    emit({"presets": names, "count": len(names)}, args)


# ─── users ───────────────────────────────────────────────────────────

def cmd_users(args):
    users = _load_prefs().get("_users", {})
    out = []
    for username, prefs in users.items():
        theme = (prefs or {}).get("theme") or {}
        out.append({
            "user": username,
            "preset": theme.get("name") or "",
            "has_custom_colors": bool(theme.get("colors")),
            "font": theme.get("font") or "",
            "density": theme.get("density") or "",
        })
    out.sort(key=lambda r: r["user"])
    emit(out, args)


# ─── get ─────────────────────────────────────────────────────────────

def cmd_get(args):
    users = _load_prefs().get("_users", {})
    prefs = users.get(args.user)
    if prefs is None:
        fail(f"no prefs for user {args.user!r}")
    theme = (prefs or {}).get("theme") or {}
    if not theme:
        emit({"user": args.user, "theme": None}, args)
        return
    emit({"user": args.user, "theme": theme}, args)


# ─── set ─────────────────────────────────────────────────────────────

def cmd_set(args):
    valid = _builtin_preset_names()
    if valid and args.preset not in valid:
        fail(
            f"unknown preset {args.preset!r}. Available: {', '.join(valid) or '(none discovered)'}"
        )
    data = _load_prefs()
    users = data.setdefault("_users", {})
    user_prefs = users.setdefault(args.user, {})
    theme = dict(user_prefs.get("theme") or {})
    theme["name"] = args.preset
    if not args.keep_colors:
        theme.pop("colors", None)
    user_prefs["theme"] = theme
    _save_prefs(data)
    emit({"ok": True, "user": args.user, "preset": args.preset, "kept_colors": args.keep_colors}, args)


# ─── export ──────────────────────────────────────────────────────────

def cmd_export(args):
    users = _load_prefs().get("_users", {})
    prefs = users.get(args.user)
    if prefs is None:
        fail(f"no prefs for user {args.user!r}")
    emit(prefs.get("theme") or {}, args)


def _build_parser():
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--pretty", action="store_true")
    p = argparse.ArgumentParser(prog="odysseus-theme", parents=[common])
    sub = p.add_subparsers(dest="cmd", required=True)

    pl = sub.add_parser("list", parents=[common])
    pl.set_defaults(func=cmd_list)

    pu = sub.add_parser("users", parents=[common])
    pu.set_defaults(func=cmd_users)

    pg = sub.add_parser("get", parents=[common])
    pg.add_argument("--user", required=True)
    pg.set_defaults(func=cmd_get)

    ps = sub.add_parser("set", parents=[common])
    ps.add_argument("--user", required=True)
    ps.add_argument("--preset", required=True)
    ps.add_argument("--keep-colors", action="store_true",
                    help="don't clear custom color overrides")
    ps.set_defaults(func=cmd_set)

    pe = sub.add_parser("export", parents=[common])
    pe.add_argument("--user", required=True)
    pe.set_defaults(func=cmd_export)

    return p


if __name__ == "__main__":
    sys.exit(run(_build_parser()))
