"""
Launch-time override config — the glue that combines the Display preset
matrix (§12) and curated-cheats application into a single RetroArch
`--appendconfig`-able file, dropped at `<state_dir>/launch/overlay-<gid>.cfg`
and injected into the resolved emulator command right before exec.

Two consumers, one cfg:
  * Display preset (P1.B)        — per (system × look) shader/aspect/integer
                                    settings from `gui.library.display`.
  * Cheats application (CT.C)    — sets `cheats_apply_at_start = "true"` so
                                    the .cht file materialised next to the
                                    ROM by `cheat_apply.materialize_for_game`
                                    actually fires when the game loads.

Heuristic: we only modify RetroArch invocations (`is_retroarch_command()`).
Standalone emulators (PPSSPP, DuckStation, MAME) pass through untouched —
they wouldn't parse `--appendconfig` and we don't have an equivalent
side-channel for them yet.
"""

from __future__ import annotations

import os
import shlex
import re
from typing import Optional

from . import display as display_mod


# Match a `retroarch` token ANYWHERE in the command (not just the leading
# token) — dArkOS-RE wraps RA in a shell pipeline:
#
#   sudo perfmax performance '<rom>'; nice -n -19 /usr/local/bin/retroarch \
#       -L <core> '<rom>'; sudo perfnorm
#
# So the first token is `sudo`, not `retroarch`. We need to detect retroarch
# wherever it lives — bare, absolute-pathed, after `;`, after `nice`, etc.
# Word-boundary on both sides so `retroarch_helper` doesn't false-positive.
# Match `retroarch` only when it's actually being EXECUTED — at command
# position, not as an argument to something else. Three legitimate forms
# we accept:
#   1. start of statement, optionally after `;`/`&&`/`||`/`|`/`(`/start
#   2. path-prefixed (`/usr/local/bin/retroarch`, `./retroarch`)
# We DON'T accept `freej2me.sh retroarch …` — there `retroarch` is an
# argument telling freej2me.sh which emulator was invoking it; injecting
# `--appendconfig` between freej2me.sh and its arg list breaks the wrapper.
# Symptom in the wild: J2ME games "launched" then exited rc=0 silently
# because freej2me.sh couldn't parse the appendconfig flag.
_RETROARCH_RE = re.compile(
    r"(?:(?:^|[;&|()])\s*(?:[A-Za-z_][\w-]*=\S+\s+|sudo\s+|nice\s+(?:-n\s+-?\d+\s+)?)*"
    r"|[/])retroarch(?=\s|$|;)"
)


def is_retroarch_command(cmd: str) -> bool:
    """True iff the command **executes** `retroarch` SOMEWHERE — including
    dArkOS shell pipelines like `sudo perfmax …; nice -n -19 retroarch …`.
    Accepts bare `retroarch` at command position, path-prefixed (e.g.
    `/usr/local/bin/retroarch`), and the `sudo …`/`nice …` prefixes ArkOS
    wraps it in. REJECTS occurrences inside argument lists — e.g.
    `freej2me.sh retroarch freej2me-plus '<rom>'` — there `retroarch` is
    a wrapper's argument, not the executable. See _RETROARCH_RE for the
    full positional rule."""
    if not cmd:
        return False
    return bool(_RETROARCH_RE.search(cmd))


def build_override_cfg(system: str, look: str, *,
                       cheats_at_start: bool,
                       cheat_path: str | None = None,
                       auto_load_state: bool = False) -> str:
    """Compose the override config text.

    Always includes the display preset for (system, look) — its
    aspect/integer/shader settings (§12 manifesto baseline).

    For cheats (cheats_at_start=True), we now set ONLY keys that exist
    in RA's actual configuration.c — verified by grepping the upstream
    source tree:

      apply_cheats_after_load = "true"   — gates `cheat_manager_apply_cheats`
                                            after the unconditional load
      cheat_database_path = "/roms/cheats" — root RA walks for
                                            <library_name>/<rom>.cht

    Earlier overlays also wrote `cheats_apply_at_start`,
    `cheats_apply_at_initialization`, `cheat_settings_path` — those keys
    DO NOT EXIST in RA source, they were guesses from online guides. RA
    silently ignored them, which made debugging painful (override looked
    "set" but did nothing).

    `cheat_path` is no longer used (RA derives the path itself from
    cheat_database_path + library_name + rom_basename). Kept in the
    signature for compatibility with callers that still pass it."""
    parts = [display_mod.cfg_for(system, look)]
    if cheats_at_start:
        parts.append(
            "# AAhead cheats — apply on game load.\n"
            "# RA's command_event_init_cheats() reads `apply_cheats_after_load`;\n"
            "# the .cht we materialised at /roms/cheats/<library_name>/<rom>.cht\n"
            "# (the canonical RA auto-load path) is loaded unconditionally, then\n"
            "# the enabled entries fire because this flag is set.\n"
            'apply_cheats_after_load = "true"\n'
            '# Defensive: pin the cheat root so dArkOS rebuilds that point\n'
            "# `cheat_database_path` elsewhere still find our file.\n"
            'cheat_database_path = "/roms/cheats"\n'
        )
    if auto_load_state:
        # The "Auto" save entry (a `<rom>.state.auto` file) is loaded on
        # content start ONLY when savestate_auto_load is true — and dArkOS
        # ships it FALSE (verified on-card: retroarch.cfg
        # `savestate_auto_load = "false"`). So when the player taps the
        # Auto slot we force it true here. Numbered slots don't need this —
        # they ride `--entryslot N` (RA 1.22.2 on the device), which loads
        # an entry state regardless of this flag.
        parts.append(
            "# AAhead resume — load the auto-save on content start.\n"
            '# dArkOS default is false; the player chose the Auto slot.\n'
            'savestate_auto_load = "true"\n'
        )
    return "".join(parts)


def _atomic_write(path: str, content: str) -> None:
    """tmp + rename; same pattern as PlayState._save / cheat_apply.
    Patched in tests to simulate write failure."""
    os.makedirs(os.path.dirname(path), exist_ok=True)
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as fh:
        fh.write(content)
    os.replace(tmp, path)


def write_overlay(state_dir: str, game_id: str,
                  cfg_text: str) -> Optional[str]:
    """Write the overlay cfg to `<state_dir>/launch/overlay-<gid>.cfg`.
    Returns the absolute path on success, None when the write was tolerated
    away (read-only fs). Caller treats None as 'skip --appendconfig'."""
    if not cfg_text:
        return None
    target = os.path.join(state_dir, "launch", f"overlay-{game_id}.cfg")
    try:
        _atomic_write(target, cfg_text)
    except OSError as exc:                                # noqa: BLE001
        print(f"launch_overlay: write failed at {target} ({exc!r}) — "
              f"launching without the override", flush=True)
        return None
    return target


def inject_appendconfig(cmd: str, cfg_path: str) -> str:
    """Splice `--appendconfig <cfg_path>` into `cmd` right after the
    retroarch executable token (before -L or any other arg). Handles
    shell-pipeline wrappers like dArkOS's
        `sudo perfmax …; … /usr/local/bin/retroarch -L …; sudo perfnorm`
    by locating the retroarch token wherever it sits in the pipeline.
    No-op for non-RetroArch commands."""
    if not is_retroarch_command(cmd):
        return cmd
    quoted = shlex.quote(cfg_path)
    # `(^|[\s;])` — start, whitespace, or pipeline separator
    # `(\S*?retroarch)` — non-greedy capture of the executable token
    # `(\s|$|;)` — whitespace, end-of-string, or pipeline separator after
    return re.sub(r"(^|[\s;])(\S*?retroarch)(\s|$|;)",
                  rf"\1\2 --appendconfig {quoted}\3", cmd, count=1)


def inject_entryslot(cmd: str, slot: int) -> str:
    """Splice `--entryslot <N>` after the retroarch token so RetroArch
    loads save-state slot N on launch (libretro/RetroArch CLI, merged via
    issue #13350). Same pipeline-aware insertion as inject_appendconfig.

    Only meaningful for a NUMBERED slot — the auto-save (slot "auto") is
    loaded by `savestate_auto_load` instead, so callers pass a real int
    here and handle auto separately. No-op for non-RetroArch commands or
    a non-int slot (defensive — keeps a standalone-emulator launch and a
    bad slot from corrupting the command)."""
    if not is_retroarch_command(cmd) or not isinstance(slot, int):
        return cmd
    return re.sub(r"(^|[\s;])(\S*?retroarch)(\s|$|;)",
                  rf"\1\2 --entryslot {slot}\3", cmd, count=1)
