"""
Launch resolver — game/rom → the emulator command that actually runs on *this*
device, by reusing EmulationStation's working per-system config.

Decision (see IMPLEMENTATION_PLAN): we are NOT a fork of ES, but the device's
`es_systems.cfg` already encodes which emulator/core works for each system on
this exact dArkOS build. So we read it and reuse its `<command>` — wrapped behind
our own small contract — instead of re-deriving a core map and owning every
quirk. We mirror ES's token semantics exactly (`%ROM%` is shell-quoted, like ES
does), so a command we build is the command ES would have run.

Flow:
  load_es_systems(path) → {es_name: EsSystem}        parse once
  resolve(system, rom_path, es)  → LaunchSpec | None  our system → ES command
  resolve_game(game, state_dir)  → LaunchSpec | None  via installed.json (id→rom)

Unresolved (no es_systems.cfg, or system absent) → None → the G3 "can't launch"
state. No silent guessing.
"""

from __future__ import annotations

import os
import re
import shlex
import xml.etree.ElementTree as ET
from dataclasses import dataclass

from . import systems
from . import ingest

# Where es_systems.cfg lives on ArkOS/dArkOS (first existing wins). Override with
# AAHEAD_ES_SYSTEMS for dev/testing.
DEFAULT_ES_PATHS = (
    "/etc/emulationstation/es_systems.cfg",
    "/home/ark/.emulationstation/es_systems.cfg",
    os.path.expanduser("~/.emulationstation/es_systems.cfg"),
    "/opt/system/emulationstation/es_systems.cfg",
)

# ArkOS es_systems commands carry placeholders that *EmulationStation* fills in
# per-system/per-game (it owns emulator/core selection). We're not ES, so we fill
# them ourselves. The CPU governor is non-fatal (perfmax errors are swallowed by
# the command's `;`), so "performance" is safe; the make-or-break tokens are the
# emulator binary and the libretro core — which we resolve by *checking what's
# actually installed* on the device rather than hard-guessing a filename.
GOVERNOR = "performance"

# Per system: candidate libretro core names, best first. resolve() picks the first
# whose `<core>_libretro.so` exists on the device; if none verify (e.g. on a dev
# box), the first is used. Keyed by our canonical system names.
CORE_CANDIDATES: dict[str, tuple[str, ...]] = {
    "GBA": ("mgba", "gpsp", "vba_next", "vbam"),
    "Game Boy": ("gambatte", "sameboy", "tgbdual"),
    "Game Boy Color": ("gambatte", "sameboy", "tgbdual"),
    "Game Gear": ("genesis_plus_gx", "gearsystem", "smsplus"),
    "Genesis": ("genesis_plus_gx", "picodrive", "blastem"),
    "Master System": ("genesis_plus_gx", "gearsystem", "picodrive", "smsplus"),
    "N64": ("mupen64plus_next", "parallel_n64"),
    "NES": ("fceumm", "nestopia", "quicknes"),
    "Neo Geo": ("fbneo", "fbalpha2012_neogeo", "geolith"),
    "PC Engine": ("mednafen_pce_fast", "mednafen_pce", "mednafen_supergrafx"),
    "PS1": ("pcsx_rearmed", "swanstation", "duckstation", "mednafen_psx_hw"),
    "PSP": ("ppsspp",),
    "SNES": ("snes9x", "snes9x2010", "snes9x2005", "bsnes_mercury_balanced"),
}

_CORE_SO = re.compile(r"""(\S*%CORE%[^\s'"]*\.so)""")


@dataclass(frozen=True)
class EsSystem:
    name: str            # es <name> (folder-ish, e.g. "snes", "psx")
    fullname: str
    path: str            # <path> e.g. /roms/snes
    exts: tuple[str, ...]
    command: str         # <command> template (may be a full shell line)
    platform: str = ""
    default_emulator: str = "retroarch"   # first <emulator> = ES's default
    cores: tuple[str, ...] = ()           # that emulator's <core>s, default first


@dataclass
class LaunchSpec:
    system: str          # our canonical system name
    es_name: str         # the ES system it resolved to
    rom: str             # rom path
    command: str         # ready-to-run shell command (tokens substituted)
    template: str        # the original <command>, for logging/debug
    game_id: str | None = None

    def as_dict(self) -> dict:
        return {"system": self.system, "es_name": self.es_name, "rom": self.rom,
                "command": self.command, "template": self.template,
                "game_id": self.game_id}


# ── parse ──────────────────────────────────────────────────────────────
def _es_path(path: str | None) -> str | None:
    if path:
        return path if os.path.exists(path) else None
    env = os.environ.get("AAHEAD_ES_SYSTEMS")
    if env and os.path.exists(env):
        return env
    for cand in DEFAULT_ES_PATHS:
        if os.path.exists(cand):
            return cand
    return None


def load_es_systems(path: str | None = None) -> dict[str, EsSystem]:
    """Parse es_systems.cfg → {es_name(lower): EsSystem}. {} if not found/invalid
    (so callers degrade to the unresolved state rather than crashing)."""
    p = _es_path(path)
    if not p:
        return {}
    try:
        root = ET.parse(p).getroot()
    except (ET.ParseError, OSError):
        return {}
    out: dict[str, EsSystem] = {}
    for node in root.findall("system"):
        name = (node.findtext("name") or "").strip()
        cmd = (node.findtext("command") or "").strip()
        if not name or not cmd:
            continue
        exts = tuple(
            e.lower().lstrip(".")
            for e in (node.findtext("extension") or "").split()
            if e.strip()
        )
        # ES declares its defaults in <emulators>: the FIRST <emulator> and its
        # FIRST <core> are what ES uses absent a per-game override. Reuse them.
        default_emulator, cores = "retroarch", ()
        emus = node.find("emulators")
        if emus is not None:
            first = emus.find("emulator")
            if first is not None:
                default_emulator = (first.get("name") or "retroarch").strip()
                cores = tuple(
                    (c.text or "").strip()
                    for c in first.findall("./cores/core") if (c.text or "").strip()
                )
        out[name.lower()] = EsSystem(
            name=name,
            fullname=(node.findtext("fullname") or "").strip(),
            path=(node.findtext("path") or "").strip(),
            exts=exts,
            command=cmd,
            platform=(node.findtext("platform") or "").strip(),
            default_emulator=default_emulator,
            cores=cores,
        )
    return out


# ── command building (mirror ES token semantics) ─────────────────────
def build_command(template: str, rom_path: str, es_name: str, *,
                  emulator: str = "retroarch", core: str = "",
                  governor: str = GOVERNOR) -> str:
    """Substitute ES tokens. `%ROM%` is shell-quoted (as ES does); `%ROM_RAW%`
    is literal. `%EMULATOR%/%CORE%/%GOVERNOR%` are the ArkOS placeholders ES would
    fill. Unknown tokens are left untouched."""
    base = os.path.splitext(os.path.basename(rom_path))[0]
    # if a template already wraps the token in quotes, substitute raw inside them
    # (don't double-quote); bare %ROM% gets ES-style shell quoting.
    out = template.replace('"%ROM%"', '"%ROM_RAW%"').replace("'%ROM%'", "'%ROM_RAW%'")
    repl = {
        "%ROM%": shlex.quote(rom_path),
        "%ROM_RAW%": rom_path,
        "%BASENAME%": base,
        "%GAMEDIR%": os.path.dirname(rom_path),
        "%ROMPATH%": os.path.dirname(rom_path),
        "%SYSTEM%": es_name,
        "%EMULATOR%": emulator,
        "%CORE%": core,
        "%GOVERNOR%": governor,
    }
    for token, value in repl.items():
        out = out.replace(token, value)
    return out.strip()


def _pick_core(system: str, template: str, emulator: str, entry: EsSystem) -> str:
    """Choose the libretro core: ES's declared cores first (its default leads),
    then our own candidates as a safety net. Each is validated by checking the
    real `<core>_libretro.so` exists on the device, so a missing default falls
    through gracefully instead of failing the launch."""
    declared = list(entry.cores)
    extra = [c for c in CORE_CANDIDATES.get(system, ()) if c not in declared]
    candidates = declared + extra
    if not candidates:
        return ""
    m = _CORE_SO.search(template)
    pattern = m.group(1) if m else None
    for core in candidates:
        if not pattern:
            return core
        probe = pattern.replace("%EMULATOR%", emulator).replace("%CORE%", core)
        if os.path.exists(probe):
            return core
    return candidates[0]


# ── resolve ────────────────────────────────────────────────────────────
def _entry_for_system(system: str, es: dict[str, EsSystem]) -> EsSystem | None:
    """Find the ES system for our canonical name, tolerating naming drift
    (e.g. ES 'megadrive' vs our organize folder 'genesis' — both are Genesis)."""
    s = systems.by_name(system)
    if not s:
        return None
    for alias in s.folders:                       # snes, sfc … megadrive, genesis …
        if alias in es:
            return es[alias]
    for entry in es.values():                     # last resort: match by <path>
        if os.path.basename(entry.path.rstrip("/")).lower() in s.folders:
            return entry
    return None


def resolve(system: str, rom_path: str,
            es: dict[str, EsSystem] | None = None,
            game_id: str | None = None) -> LaunchSpec | None:
    """Our (system, rom) → a runnable command from the device's ES config, or
    None when the system isn't configured on this device."""
    if es is None:
        es = load_es_systems()
    entry = _entry_for_system(system, es)
    if not entry:
        return None
    tmpl = entry.command
    emulator = entry.default_emulator or "retroarch"     # ES's declared default
    core = _pick_core(system, tmpl, emulator, entry) if "%CORE%" in tmpl else ""
    return LaunchSpec(
        system=system, es_name=entry.name, rom=rom_path,
        command=build_command(tmpl, rom_path, entry.name,
                              emulator=emulator, core=core),
        template=tmpl, game_id=game_id,
    )


def resolve_game(game: dict, state_dir: str,
                 es: dict[str, EsSystem] | None = None) -> LaunchSpec | None:
    """Catalog game → LaunchSpec, using installed.json (game id → rom path)
    written by ingest. None if the game isn't installed or its system isn't
    configured."""
    installed = ingest.load_installed(state_dir)
    rom = installed.get(game.get("id"))
    if not rom or not game.get("system"):
        return None
    return resolve(game["system"], rom, es=es, game_id=game.get("id"))
