"""Clipshelf configuration.

Everything lives in config.json in the install root (created by run.py on
first launch). Paths resolve relative to the install root so a zip can be
unpacked anywhere; DATA_DIR is the single knob an installer flips to move
user data to a platform data directory. The GSI token is derived from a
per-install secret, so downloadable game configs just work with no manual
token step.
"""
import hashlib
import json
import os
import secrets
import sys
from pathlib import Path

APP_NAME = "Clipshelf"
PKG_DIR = Path(__file__).parent.resolve()        # src/product
ROOT = PKG_DIR.parent.parent                     # install root (repo root)


def _default_data_dir() -> str:
    r"""Where the library lives when nothing says otherwise.

    Unzipped somewhere, it sits next to the app — that is the whole point of
    a portable copy, and it keeps a USB-stick install self-contained.

    INSTALLED, it must not: the library would be inside Program Files or
    %LOCALAPPDATA%\Programs, which is a directory the installer owns. An
    uninstall then either deletes someone's clips or leaves a confusing
    husk behind, and a per-machine install cannot write there at all. The
    installer drops an `installed` marker beside run.py to say which of the
    two this is."""
    if (ROOT / "installed").exists():
        if os.name == "nt":
            base = os.getenv("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
            return str(Path(base) / "Clipshelf")
        return str(Path.home() / ".local" / "share" / "clipshelf")
    return "data"


def _default_config_file() -> Path:
    r"""Where settings live.

    Portable: beside the app, which is what makes a copied folder — or a USB
    stick — self-contained.

    INSTALLED: next to the library, NOT beside the app, for the same reason
    the library moved. The install directory belongs to the installer. A
    per-machine install lands in Program Files, which the app cannot write
    while running unelevated (as it should), so EVERY settings save raised
    PermissionError and surfaced as a bare HTTP 500. The setup wizard is the
    first save anyone makes, so a fresh install of that kind could not be
    configured at all. An update or uninstall is also free to replace
    whatever sits in the install directory."""
    if (ROOT / "installed").exists():
        return Path(_default_data_dir()) / "config.json"
    return ROOT / "config.json"


# CLIPSHELF_CONFIG points tests (or a portable install) at another
# config.json. PRODUCT_CONFIG is the pre-rename spelling and still works, so
# an install made before the rename keeps finding its own data.
CONFIG_FILE = Path(os.getenv("CLIPSHELF_CONFIG") or os.getenv("PRODUCT_CONFIG")
                   or _default_config_file())
# where installed copies used to keep settings. Read once, so moving them
# does not cost anyone the setup they already did.
LEGACY_CONFIG_FILE = ROOT / "config.json"


class ConfigNotWritable(RuntimeError):
    """Settings could not be written. Carries the path and what to do about
    it, because the answer is always about where it was trying to write."""


DEFAULTS = {
    "vod_dir": "",                 # OBS recording output folder (REQUIRED)
    "data_dir": _default_data_dir(),   # library: db, clips, renders, transcripts
    "port": 8420,
    "obs_ws": {"host": "127.0.0.1", "port": 4455, "password": ""},
    "secret": "",                  # filled on first run
}


def load() -> dict:
    cfg = dict(DEFAULTS)
    src = CONFIG_FILE if CONFIG_FILE.exists() else (
        LEGACY_CONFIG_FILE if LEGACY_CONFIG_FILE.exists() else None)
    if src is not None:
        try:
            cfg.update(json.loads(src.read_text(encoding="utf-8")))
        except Exception as e:
            sys.exit(f"config.json is broken ({e}) — fix or delete it and rerun")
    if not cfg.get("secret"):
        cfg["secret"] = secrets.token_hex(16)
        try:
            save(cfg)
        except Exception:
            pass      # a config we cannot write must not stop the app booting
    return cfg


def save(cfg: dict):
    """Write settings, atomically, and say something useful when we can't.

    A half-written config.json is unrecoverable for the user (load() exits
    on a parse error), so this replaces rather than truncates."""
    try:
        CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
        tmp = CONFIG_FILE.with_name(CONFIG_FILE.name + ".tmp")
        tmp.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
        os.replace(tmp, CONFIG_FILE)
    except OSError as e:
        raise ConfigNotWritable(
            f"Clipshelf can't save settings to {CONFIG_FILE} "
            f"({getattr(e, 'strerror', None) or e}). If it was installed for "
            f"all users, reinstall it for just you — it should never need "
            f"administrator rights to run.") from e


CFG = load()

PORT = int(CFG["port"])
BASE_URL = f"http://127.0.0.1:{PORT}"
# NOTE: modules read `config.VOD_DIR` (never `from config import VOD_DIR`) —
# the recordings folder is set in the setup wizard and changeable in
# settings, so it must resolve live rather than bind at import.
VOD_DIR = str(Path(CFG["vod_dir"]).expanduser()) if CFG["vod_dir"] else ""

DATA = (ROOT / CFG["data_dir"]).resolve() if not os.path.isabs(CFG["data_dir"]) \
    else Path(CFG["data_dir"])
DATA_DIR = DATA                     # installer-era alias: the one knob to move
# plugins sit next to config.json (the install root) — visible and drop-in
# friendly, not buried in the data tree
PLUGINS_DIR = CONFIG_FILE.parent / "plugins"
PROJECTS = DATA / "projects"
ASSETS = DATA / "assets"
RENDERS = DATA / "renders"
JOBS = DATA / "jobs"
SOUNDS = DATA / "sounds"
EXPORTS = DATA / "exports"          # published cuts ("My Edits" in the editor)
HIGHLIGHTS_DIR = DATA / "highlights"
INGEST_DIR = DATA / "ingest"        # captured buffers waiting to be trimmed
THUMBS = DATA / "thumbs"
DB_PATH = DATA / "app.db"

for d in (PLUGINS_DIR, PROJECTS, ASSETS, RENDERS, JOBS, SOUNDS, EXPORTS,
          HIGHLIGHTS_DIR, INGEST_DIR, THUMBS):
    d.mkdir(parents=True, exist_ok=True)

# stable per-install GSI token — baked into downloadable game configs
GSI_TOKEN = hashlib.sha256(f"gsi:{CFG['secret']}".encode()).hexdigest()[:24]

def configured() -> bool:
    """False until the user has pointed us at a recordings folder — the app
    boots anyway and routes to the setup wizard."""
    return bool(VOD_DIR and os.path.isdir(VOD_DIR))


def apply(changes: dict):
    """Merge settings into config.json and refresh derived globals."""
    global VOD_DIR
    CFG.update(changes or {})
    save(CFG)
    VOD_DIR = str(Path(CFG["vod_dir"]).expanduser()) if CFG.get("vod_dir") else ""


FRONTEND = PKG_DIR / "frontend"
NODE = os.getenv("NODE", "node")
RENDER_WORKER = ROOT / "render" / "render_worker.js"
