#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
#
# pocketds-steam-default-proton11 — pin Proton 11 ARM64 as the default
# Steam Play compatibility tool for *all* titles on the Pocket DS.
#
# Without this, after pocketds-steam-install-proton drops Proton11ARM
# into ~/.local/share/Steam/compatibilitytools.d/, the user still has
# to go into Steam → Settings → Compatibility → "Enable Steam Play for
# all other titles" and pick "Proton 11 ARM64" from the dropdown. We
# bake that mapping in by editing config.vdf directly:
#
#   InstallConfigStore.Software.Valve.Steam.CompatToolMapping."0"
#       name     = <internal tool name from compatibilitytool.vdf>
#       config   = ""
#       priority = "250"
#
# AppID "0" is Valve's "fallback for any non-native title" key — same
# slot the GUI toggles when you flip the global toggle in Settings.
#
# Steam owns config.vdf and rewrites it whenever it shuts down. To
# avoid losing the mapping, we run this:
#
#   * once on install (via pocketds-steam-install-proton.service ExecStartPost)
#   * defensively from the Steam launcher wrapper on every cold start,
#     guarded by a marker so it's a no-op after the first apply
#
# This script is a hand-rolled VDF reader/writer — Fedora doesn't ship
# the `vdf` PyPI module and we don't want to drag in pip on a device
# image. The Steam KeyValues format is a balanced "key" "value" /
# "key" { ... } tree with quoted strings and tab indentation; the
# parser below handles exactly what Steam emits (no escapes inside
# tokens, no conditional [$OS] suffixes, no multi-line values).

from __future__ import annotations

import os
import re
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Union

STEAMROOT = Path(os.path.expanduser("~/.local/share/Steam"))
CONFIG_VDF = STEAMROOT / "config" / "config.vdf"
COMPAT_DIR = STEAMROOT / "compatibilitytools.d"
MARKER = STEAMROOT / ".pocketds-default-proton11.done"

# Steam parses CompatToolMapping at next-launch. A bare `priority` of
# 250 is what the Steam UI writes when you select a tool from the
# global Steam Play dropdown; mirror that exactly so the UI doesn't
# notice an out-of-band write.
DEFAULT_PRIORITY = "250"


VDFNode = Union[str, dict]


def log(msg: str) -> None:
    print(f"pocketds-steam-default-proton11: {msg}", file=sys.stderr)


def find_proton_tool_name() -> str | None:
    """Scan compatibilitytools.d/*/compatibilitytool.vdf for a Proton 11
    ARM64 entry and return its internal tool name (the key under
    `compat_tools`). Falls back to the directory basename if the vdf
    doesn't parse cleanly."""
    if not COMPAT_DIR.is_dir():
        return None

    # Prefer a manifest match so we honour whatever name the toolmaker
    # baked in (e.g. "proton_11arm64").
    for child in sorted(COMPAT_DIR.iterdir()):
        if not child.is_dir() or not child.name.lower().startswith("proton"):
            continue
        manifest = child / "compatibilitytool.vdf"
        if not manifest.is_file():
            continue
        try:
            tree = parse_vdf(manifest.read_text(encoding="utf-8", errors="replace"))
        except Exception:
            continue
        # compatibilitytools.vdf shape:
        # "compatibilitytools" { "compat_tools" { "proton_11arm64" {...} } }
        ct = tree.get("compatibilitytools", {})
        if isinstance(ct, dict):
            tools = ct.get("compat_tools", {})
            if isinstance(tools, dict) and tools:
                # Return the first (and usually only) key.
                return next(iter(tools.keys()))
        # Manifest existed but didn't parse — use the dir name as the
        # last-chance fallback. Most Proton builds set internal name
        # == directory basename.
        return child.name

    # No compatibilitytool.vdf anywhere; fall back to dir name search.
    for child in sorted(COMPAT_DIR.iterdir()):
        if child.is_dir() and "proton" in child.name.lower() and "arm" in child.name.lower():
            return child.name
    return None


# ── minimal VDF parser/serialiser ───────────────────────────────────

_TOKEN_RE = re.compile(r'"((?:[^"\\]|\\.)*)"|([{}])')


def parse_vdf(text: str) -> dict:
    tokens = []
    for m in _TOKEN_RE.finditer(text):
        if m.group(1) is not None:
            tokens.append(("STR", m.group(1)))
        else:
            tokens.append((m.group(2), m.group(2)))
    pos = 0

    def parse_block() -> dict:
        nonlocal pos
        result: dict = {}
        while pos < len(tokens):
            kind, val = tokens[pos]
            if kind == "}":
                pos += 1
                return result
            if kind != "STR":
                raise ValueError(f"unexpected token {tokens[pos]!r}")
            key = val
            pos += 1
            if pos >= len(tokens):
                raise ValueError(f"truncated at key {key!r}")
            nkind, nval = tokens[pos]
            if nkind == "{":
                pos += 1
                result[key] = parse_block()
            elif nkind == "STR":
                result[key] = nval
                pos += 1
            else:
                raise ValueError(f"unexpected value token {tokens[pos]!r}")
        return result

    return parse_block()


def serialise(tree: dict, indent: int = 0) -> str:
    out = []
    pad = "\t" * indent
    for k, v in tree.items():
        if isinstance(v, dict):
            out.append(f'{pad}"{k}"')
            out.append(f"{pad}{{")
            out.append(serialise(v, indent + 1))
            out.append(f"{pad}}}")
        else:
            out.append(f'{pad}"{k}"\t\t"{v}"')
    return "\n".join(out)


def deep_set(tree: dict, path: list[str], value: VDFNode) -> None:
    node = tree
    for key in path[:-1]:
        if key not in node or not isinstance(node[key], dict):
            node[key] = {}
        node = node[key]
    node[path[-1]] = value


def atomic_write(target: Path, content: str) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp = tempfile.mkstemp(prefix=".pocketds-vdf.", dir=str(target.parent))
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            f.write(content)
            f.write("\n")
        # Preserve mode of any existing file so Steam doesn't notice.
        if target.exists():
            shutil.copymode(target, tmp)
        else:
            os.chmod(tmp, 0o644)
        os.replace(tmp, target)
    except Exception:
        try:
            os.unlink(tmp)
        except FileNotFoundError:
            pass
        raise


# ── main ────────────────────────────────────────────────────────────


def steam_running() -> bool:
    uid = os.getuid()
    for proc in Path("/proc").iterdir():
        if not proc.name.isdigit():
            continue
        try:
            if proc.stat().st_uid == uid and (proc / "comm").read_text().strip() == "steam":
                return True
        except OSError:
            continue
    return False


def main() -> int:
    if MARKER.exists():
        log(f"marker present ({MARKER}) — default already pinned")
        return 0

    if steam_running():
        log("Steam is running and would overwrite config.vdf on exit — deferring to the next launch")
        return 0

    tool_name = find_proton_tool_name()
    if tool_name is None:
        log("no Proton 11 ARM64 found under compatibilitytools.d/ — nothing to pin")
        return 0

    if CONFIG_VDF.exists():
        try:
            tree = parse_vdf(CONFIG_VDF.read_text(encoding="utf-8", errors="replace"))
        except Exception as exc:
            log(f"could not parse {CONFIG_VDF}: {exc}; refusing to overwrite")
            return 1
    else:
        # Steam will fill in everything else on its first launch; we
        # just need enough scaffolding for it to read CompatToolMapping
        # without complaining.
        tree = {"InstallConfigStore": {"Software": {"Valve": {"Steam": {}}}}}

    mapping = {
        "name": tool_name,
        "config": "",
        "priority": DEFAULT_PRIORITY,
    }
    deep_set(
        tree,
        ["InstallConfigStore", "Software", "Valve", "Steam", "CompatToolMapping", "0"],
        mapping,
    )

    atomic_write(CONFIG_VDF, serialise(tree))
    MARKER.parent.mkdir(parents=True, exist_ok=True)
    MARKER.touch()
    log(f"default Steam Play tool pinned to {tool_name!r}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
