"""
RetroArch save-state enumeration.

A real play session leaves `.state` files on the card. This module reads
them so the game card can show the player their actual saves and resume a
specific slot — the thing the "Load save" button was a toast stub for.

RetroArch naming (libretro/RetroArch `content_get_savestate_path` +
`save.c`):

    <base>.state        slot 0 — the default "current" slot
    <base>.state<N>     slot N (1, 2, … e.g. "Sonic 2.state1")
    <base>.state.auto   the auto-save written on exit / power-button
                        ("quick mode" on ArkOS-style firmware); RetroArch
                        auto-loads it on next launch when
                        `savestate_auto_load = true`
    <statefile>.png     a screenshot sidecar RetroArch writes next to each
                        state ("…state1.png") — ideal load-screen previews

`<base>` is the ROM filename WITHOUT its extension (the content basename).
The states live in RetroArch's `savestate_directory`. We can't read the
device's retroarch.cfg from a dev box, so the directory is resolved from
(in priority order):

    1. AAHEAD_SAVESTATE_DIR env  — explicit single dir (tests / overrides)
    2. a `save_root` passed by the caller (read from the diagnostic dump
       of retroarch.cfg once S0 lands), joined with the system if it's a
       per-system tree
    3. next to the ROM — RetroArch's default when `savestate_directory`
       is left blank
    4. common ArkOS-style central stores: /roms/savestates/<system>,
       /roms/<system>/savestates

Everything is best-effort: a missing dir, an unreadable entry, an
unparseable name → skipped, never raised. Callers degrade to "no saves
yet" rather than crash — same defaults-over-defeat contract as the rest
of the library layer.
"""

from __future__ import annotations

import os
import re
from dataclasses import dataclass
from typing import Optional


# A state file is "<base>.state", "<base>.state<N>", or "<base>.state.auto".
# Captured group 1 = the slot suffix ("", a digit run, or ".auto").
def _slot_re(base: str) -> "re.Pattern[str]":
    # re.escape the base so brackets / parens in ROM names (very common —
    # "Sonic (USA) [!]") are matched literally, not as regex.
    return re.compile(
        re.escape(base) + r"\.state(\.auto|\d*)$", re.IGNORECASE)


@dataclass
class SaveState:
    """One on-disk RetroArch save state."""
    slot: object          # int (0,1,2…) for numbered slots, or "auto"
    path: str             # absolute path to the .state file
    mtime: float          # epoch seconds — for "when" + newest-first sort
    size: int             # bytes
    thumbnail: Optional[str] = None   # sidecar .png path, if RA wrote one
    is_auto: bool = False             # the quick-mode auto-save

    @property
    def label(self) -> str:
        """Human label for the slot — "Auto" or "Slot N"."""
        return "Auto" if self.is_auto else f"Slot {self.slot}"


def _parse_slot(filename: str, base: str):
    """Return the slot for a state filename, or None if it isn't one.
    Slot is the int 0 (bare `.state`), an int N (`.stateN`), or the string
    "auto" (`.state.auto`)."""
    m = _slot_re(base).match(filename)
    if not m:
        return None
    suffix = m.group(1)
    if suffix == ".auto":
        return "auto"
    if suffix == "":
        return 0
    try:
        return int(suffix)
    except ValueError:
        return None


def candidate_dirs(rom_path: str, system: str,
                   save_root: Optional[str] = None) -> list[str]:
    """The directories that might hold this ROM's states, in priority
    order. Caller scans the first that exists. Never raises; a None/empty
    rom_path just yields the central-store guesses."""
    dirs: list[str] = []
    env = os.environ.get("AAHEAD_SAVESTATE_DIR")
    if env:
        dirs.append(env)
    if save_root:
        # save_root may be a flat dir or the parent of per-system subdirs;
        # try both so we work whether retroarch.cfg points at one shared
        # dir or a per-content tree.
        dirs.append(os.path.join(save_root, system))
        dirs.append(save_root)
    if rom_path:
        dirs.append(os.path.dirname(rom_path))           # RA default
    # ArkOS-style central stores (device paths; harmless if absent on a dev box)
    dirs.append(os.path.join("/roms", "savestates", system))
    dirs.append(os.path.join("/roms", system, "savestates"))
    # de-dupe, preserve order
    seen, out = set(), []
    for d in dirs:
        if d and d not in seen:
            seen.add(d)
            out.append(d)
    return out


def list_states(rom_path: str, system: str = "",
                save_root: Optional[str] = None) -> list[SaveState]:
    """Enumerate save states for `rom_path`. Returns them sorted newest
    first, with the auto-save (if any) ALWAYS first regardless of mtime —
    it's the "resume exactly where you left off" entry the UI leads with.

    Scans the FIRST candidate dir that exists AND actually contains a
    state for this ROM; this avoids merging a stale central-store copy
    with a fresh next-to-ROM one. Empty list when nothing is found."""
    if not rom_path:
        return []
    base = os.path.splitext(os.path.basename(rom_path))[0]
    for d in candidate_dirs(rom_path, system, save_root):
        try:
            if not os.path.isdir(d):
                continue
            entries = os.listdir(d)
        except OSError:
            continue
        found: list[SaveState] = []
        names = set(entries)
        for name in entries:
            slot = _parse_slot(name, base)
            if slot is None:
                continue
            full = os.path.join(d, name)
            try:
                st = os.stat(full)
            except OSError:
                continue
            thumb = f"{name}.png"
            found.append(SaveState(
                slot=slot, path=full, mtime=st.st_mtime, size=st.st_size,
                thumbnail=(os.path.join(d, thumb) if thumb in names else None),
                is_auto=(slot == "auto"),
            ))
        if found:
            found.sort(key=lambda s: (not s.is_auto, -s.mtime))
            return found
    return []


def auto_state_path(rom_path: str, system: str = "",
                    save_root: Optional[str] = None) -> Optional[str]:
    """Absolute path to the `<base>.state.auto` file if it exists — used by
    Restart (E.2) to drop the auto-save so RetroArch doesn't auto-load it
    over a from-scratch start. None when there's no auto-save."""
    for s in list_states(rom_path, system, save_root):
        if s.is_auto:
            return s.path
    return None
