"""
G1/G2/G3 — Game card (design v3).

Fixed split-pane key-art header (art right, text left) with the actions; below it
a scrolling stack: the Clerk's paragraph, the **assist panel** ("How do you want
to play?" — relaxed mode, no judgment), media (screenshots + art → fullscreen),
saves, series, and in-catalog recs. Bigger type, quiet 28px chrome.

States: played (Continue + saves) · never-played (Play) · not-installed (Add).
Focus: actions → assist → media → saves → series → recs (those present).
"""

from __future__ import annotations

import os
import time

import pygame

from ..design import tokens as t
from ..shared import fonts, render
from ..widgets import chrome, components as c, images, hero as herow
from ..library import savestates

BODY_BOT = t.VIEWPORT_HEIGHT - t.Space.FOOTER_HINT_HEIGHT      # 452
HERO = pygame.Rect(0, t.Space.STATUS_BAR_HEIGHT, t.VIEWPORT_WIDTH, 180)  # 28..208
BELOW_TOP = HERO.bottom + 8                                    # 216
BELOW_H = BODY_BOT - BELOW_TOP                                 # 236
SAFE = 14
CONTENT_W = t.VIEWPORT_WIDTH
SS_W, SS_H, SS_GAP = 196, 110, 6
REC_W, REC_ART_H = 110, 146


class GameCardScreen:
    name = "game_card"

    def __init__(self, app, gid, *, installed=True):
        self.app = app
        self.gid = gid
        self.installed = installed
        cat = app.catalog
        self.game = cat.get(gid)
        self.cont = cat.continue_entry(gid) if installed else None
        self.recs = cat.recommendations(gid)
        self.fav = app.library.play_state().is_favorite(gid)

        a = self.game.get("assets", {})
        self.media = [self._abs(r) for r in (a.get("screenshots") or [])]
        self.media_caps = [f"Screenshot {i+1}" for i in range(len(self.media))]
        for i, r in enumerate(a.get("artworks") or []):
            self.media.append(self._abs(r)); self.media_caps.append(f"Art {i+1}")

        self.actions = (["play"] + (["restart"] if self.cont else []) + ["favorite"]
                        if installed else ["add", "favorite"])

        self.series = cat.game_series(gid)        # library-relative (name · N of M)
        self.has_series = bool(self.series and self.series.get("name")
                               and self.series["name"] in cat.series_groups())

        # Real RetroArch save states left on the card for this ROM (S1/S2).
        # Resolves the rom path from installed.json; off-device or no-states
        # → [] and the section is simply omitted. Replaces the old mock that
        # read `self.cont["saves"]` (which real play_state never populates).
        self.states = []
        if installed:
            try:
                rom_path = (self.app.library.installed().get(gid)
                            or self.game.get("path") or "")
                self.states = savestates.list_states(
                    rom_path, self.game.get("system", ""))
            except Exception as exc:                      # noqa: BLE001
                print(f"savestates lookup failed for {gid}: {exc!r}", flush=True)
                self.states = []

        self.sections = ["actions"]
        if installed:
            self.sections.append("assist")          # how do you want to play?
        if self.media:
            self.sections.append("media")
        if self.states:
            self.sections.append("saves")
        if self.has_series:
            self.sections.append("series")
        if self.recs:
            self.sections.append("recs")

        self.sec = 0
        self.col = {k: 0 for k in
                    ("actions", "assist", "media", "saves", "series", "recs")}
        self.scroll = 0
        self._max_scroll = 0
        self._sec_top = {}

    def _abs(self, rel):
        root = self.app.catalog.asset_root
        return os.path.join(root, rel) if (root and rel) else None

    @property
    def _cur(self):
        return self.sections[self.sec]

    def _len(self, sec):
        return {"actions": len(self.actions), "assist": 1, "media": len(self.media),
                "saves": len(self.states),
                "series": 1, "recs": len(self.recs)}.get(sec, 0)

    # ── input ────────────────────────────────────────────────────────
    def on_action(self, action):
        if action == "b":
            self.app.pop()
        elif action == "down":
            self.sec = min(len(self.sections) - 1, self.sec + 1)
        elif action == "up":
            self.sec = max(0, self.sec - 1)
        elif action == "left":
            self.col[self._cur] = max(0, self.col[self._cur] - 1)
        elif action == "right":
            self.col[self._cur] = min(max(0, self._len(self._cur) - 1), self.col[self._cur] + 1)
        elif action == "a":
            self._activate()
        elif action == "x" and self.installed:
            self.app.open_quick_menu(self.gid)        # in-game Guide preview

    def _activate(self):
        s = self._cur
        g = self.game
        if s == "actions":
            act = self.actions[self.col["actions"]]
            if act == "play":
                self.app.launch_game(g)               # Play / Continue → real launch
            elif act == "add":
                self.app.open_addgames()              # not on device → the store
            elif act == "restart":
                # Restart = "from scratch": same emulator command as Continue,
                # but the .launch flag carries `MODE=restart` so launch.sh
                # drops the autosave before exec'ing the command. (The actual
                # autosave-delete is launch.sh's job — Step E.2, needs the
                # device-side savestate path knowledge.)
                self.app.launch_game(g, restart=True)
            else:
                # Single source of truth — PlayState persists the change to
                # disk atomically AND, via the App.run() swap above, mirrors
                # it back into catalog.play_state for any cross-screen reads.
                self.fav = self.app.library.play_state().toggle_favorite(self.gid)
                self.app.toast("Added to favorites" if self.fav else "Removed from favorites")
        elif s == "assist":
            self.app.open_assist(self.game)
        elif s == "media":
            self.app.open_image(self.media, self.col["media"], self.media_caps)
        elif s == "saves":
            sv = self.states[self.col["saves"]]
            # Auto-save → default Continue (savestate_auto_load handles it);
            # numbered slot → launch with --entryslot N. launch_game exits
            # the shell, so no toast needed — the game takes over.
            self.app.launch_game(self.game, slot=sv.slot)
        elif s == "series":
            self.app.open_series(self.series["name"])
        elif s == "recs":
            self.app.open_game(self.recs[self.col["recs"]]["game"]["id"])

    def update(self, dt_ms):
        pass

    # ── draw ─────────────────────────────────────────────────────────
    def draw(self, surf):
        surf.fill(t.Color.BG_DEEP)
        self._draw_hero(surf)
        # scrolling "below" region
        content = pygame.Surface((CONTENT_W, 760), pygame.SRCALPHA)
        bottom = self._draw_below(content)
        self._max_scroll = max(0, bottom - BELOW_H)
        target = 0 if self._cur == "actions" else max(0, self._sec_top.get(self._cur, 0) - 8)
        self.scroll = max(0, min(target, self._max_scroll))
        view = content.subsurface((0, self.scroll, CONTENT_W,
                                   min(BELOW_H, content.get_height() - self.scroll)))
        surf.blit(view, (0, BELOW_TOP))
        if self.scroll < self._max_scroll:                # more below → fade, don't hard-cut
            c.scroll_fade(surf, bottom=BODY_BOT)

        chrome.status_bar(surf, back=f"Library · {self.game['system']}", gear=False)
        if self._max_scroll > 0:
            x = t.VIEWPORT_WIDTH - 5
            pygame.draw.rect(surf, t.with_alpha(t.Color.TEXT_PRIMARY, .05),
                             (x, BELOW_TOP, 2, BELOW_H), border_radius=2)
            frac = self.scroll / self._max_scroll
            th = max(20, int(BELOW_H * BELOW_H / (self._max_scroll + BELOW_H)))
            pygame.draw.rect(surf, t.Color.TEXT_MUTED,
                             (x, BELOW_TOP + int((BELOW_H - th) * frac), 2, th), border_radius=2)
        a_label = {"actions": ("Add" if not self.installed else ("Continue" if self.cont else "Play")),
                   "assist": "Set up", "media": "View", "saves": "Load",
                   "series": "View", "recs": "Open"}.get(self._cur, "Select")
        chrome.footer_hints(surf, [("A", a_label), ("B", "Back")])

    def _draw_hero(self, surf):
        g = self.game
        cat = self.app.catalog
        eyebrow = "The Clerk recommends" if self.installed else "Not on this device · add it"
        meta = [g["system"], g.get("year"), (g.get("genre") or [None])[0]]
        herow.split_hero(surf, HERO, art_path=cat.asset_path(g, "hero"),
                         eyebrow=eyebrow, title=g["title"], title_style=t.Type.HERO_CARD,
                         meta=meta, rating=g.get("rating"), quote=None,
                         cta_label=None, art_focus=(0.5, 0.35), text_top=12)
        self._draw_actions(surf, HERO.x + 18, HERO.bottom - 12 - 32)

    def _draw_actions(self, surf, x, y):
        cx = x
        for i, act in enumerate(self.actions):
            focused = (self._cur == "actions" and self.col["actions"] == i)
            if act in ("play", "add"):
                label = ("Add to library" if act == "add" else
                         (f"Continue · {self.cont['playtime']}" if self.cont else "Play"))
                r = pygame.Rect(cx, y, fonts.measure(t.Type.BTN, label)[0] + 32 + 16, 32)
                c.button_primary(surf, r, label, focused=focused)
            elif act == "restart":
                r = pygame.Rect(cx, y, fonts.measure(t.Type.CAPTION, "Restart")[0] + 40, 32)
                c.button_ghost(surf, r, "Restart", icon_name="ph-arrow-clockwise", focused=focused)
            else:
                lbl = "Favorited" if self.fav else "Favorite"
                r = pygame.Rect(cx, y, fonts.measure(t.Type.CAPTION, lbl)[0] + 40, 32)
                c.button_ghost(surf, r, lbl, icon_name="ph-star",
                               icon_color=t.Color.ACCENT_PEACH if self.fav else None, focused=focused)
            cx = r.right + 8

    # ── below (content coords; 0 = BELOW_TOP) ────────────────────────
    def _draw_below(self, surf):
        y = 2
        y = self._draw_clerk(surf, y)
        if "assist" in self.sections:
            self._sec_top["assist"] = y + 10
            y = self._draw_assist(surf, y + 10)
        if "media" in self.sections:
            self._sec_top["media"] = y + 10
            y = self._draw_media(surf, y + 10)
        if "saves" in self.sections:
            self._sec_top["saves"] = y + 10
            y = self._draw_saves(surf, y + 10)
        if "series" in self.sections:
            self._sec_top["series"] = y + 10
            y = self._draw_series(surf, y + 10)
        if "recs" in self.sections:
            self._sec_top["recs"] = y + 10
            y = self._draw_recs(surf, y + 10)
        return y + 8

    def _draw_clerk(self, surf, y):
        x, w = SAFE, CONTENT_W - 2 * SAFE
        lines = c.wrap_text(t.Type.CLERK, self.game.get("description", ""), w - 24)
        h = 10 + 16 + 6 + len(lines) * 20 + 10
        card = render.rounded_gradient((w, h), t.Color.SURFACE_BASE, t.Color.BG_MID, 9, 160)
        pygame.draw.rect(card, t.Color.BORDER_SOFT, (0, 0, w, h), width=1, border_radius=9)
        surf.blit(card, (x, y))
        lab = fonts.render(t.Type.EYEBROW, "From the Clerk", t.Color.ACCENT_ROSE, upper=True)
        surf.blit(lab, (x + 12, y + 10))
        ly = y + 10 + lab.get_height() + 6
        for ln in lines:
            surf.blit(fonts.render(t.Type.CLERK, ln, t.Color.TEXT_SECONDARY), (x + 12, ly))
            ly += 20
        return y + h

    def _draw_assist(self, surf, y):
        # An explaining BUTTON — no inline toggle, nothing on by default. It opens
        # the "How do you want to play?" modal where the player sets things up.
        # The subtitle reflects THIS game's curated cheats (first few labels +
        # "+N more"), so the player previews what the row will offer before
        # tapping in; empty-curated falls back to the catch-all "tune the game"
        # hint so a no-cheats game still has a clear call to set up.
        x, w = SAFE, CONTENT_W - 2 * SAFE
        focused = (self._cur == "assist")
        h = 58
        peach = t.Color.ACCENT_PEACH
        card = pygame.Surface((w, h), pygame.SRCALPHA)
        pygame.draw.rect(card, t.with_alpha(peach, 0.06), (0, 0, w, h), border_radius=9)
        pygame.draw.rect(card, t.with_alpha(peach, 0.30 if focused else 0.20),
                         (0, 0, w, h), width=1, border_radius=9)
        card.blit(fonts.render(t.Type.EYEBROW, "How do you want to play?", peach, upper=True),
                  (14, 11))
        # Per-game subtitle from the curated cheats; ellipsizes if too wide.
        curated = self.app.catalog.cheats_for(self.gid)
        if curated:
            labels = [c.get("label", "") for c in curated if c.get("label")]
            preview = " · ".join(labels[:2])
            if len(labels) > 2:
                preview += f" · +{len(labels) - 2} more"
            enabled = self.app.library.play_state().enabled_cheats(self.gid)
            n_on = sum(1 for c in curated if c.get("label") in enabled)
            if n_on:
                preview += f"  ({n_on} on)"
        else:
            preview = ("Tune the game your way — infinite lives, "
                       "no timers, unlocks and more.")
        # Fit the preview to the available width (subtract the right-side pill).
        avail = w - 14 - 14 - 100        # leave room for the "Set up" pill
        preview = c._ellipsize(t.Type.CAPTION, preview, avail)
        card.blit(fonts.render(t.Type.CAPTION, preview, t.Color.TEXT_SECONDARY),
                  (14, 31))
        # right-aligned "A · Set up" affordance (this row opens a modal)
        label = "Set up"
        lw = fonts.measure(t.Type.CAPTION, label)[0]
        cd = 18
        pill_w = 8 + cd + 6 + lw + 10
        px = w - 14 - pill_w
        pyc = (h - 26) // 2
        pygame.draw.rect(card, t.with_alpha(peach, 0.14), (px, pyc, pill_w, 26), border_radius=999)
        pygame.draw.circle(card, peach, (px + 8 + cd // 2, pyc + 13), cd // 2)
        a = fonts.render(t.Type.GP, "A", t.Color.BG_DEEP)
        card.blit(a, (px + 8 + cd // 2 - a.get_width() // 2, pyc + 13 - a.get_height() // 2))
        card.blit(fonts.render(t.Type.CAPTION, label, t.Color.TEXT_PRIMARY),
                  (px + 8 + cd + 6, pyc + 7))
        surf.blit(card, (x, y))
        if focused:
            surf.blit(render.gradient_border((w, h), 9), (x, y))
        return y + h

    def _draw_media(self, surf, y):
        active = (self._cur == "media")
        head = "Screens & art" if any(cap.startswith("Art") for cap in self.media_caps) else "Screens"
        c.rail_header(surf, SAFE, y, head, f"{len(self.media)}", max_w=CONTENT_W - 2 * SAFE)
        ty = y + 22
        view = (t.VIEWPORT_WIDTH - 2 * SAFE) // (SS_W + SS_GAP) + 1
        col = self.col["media"]
        start = max(0, min(col - view + 2, max(0, len(self.media) - view))) if active else 0
        for k in range(start, min(start + view + 1, len(self.media))):
            x = SAFE + (k - start) * (SS_W + SS_GAP)
            r = pygame.Rect(x, ty, SS_W, SS_H)
            # dark matte + native-aspect screenshot (nearest for pixel-art)
            pygame.draw.rect(surf, images.LETTERBOX, r, border_radius=5)
            img = images.screenshot_fit(self.media[k], SS_W, SS_H)
            focused = (active and k == col)
            if focused:
                glow, gpos = render.focus_glow(r, 5); surf.blit(glow, gpos)
            surf.blit(img, r.topleft)
            if focused:
                surf.blit(render.gradient_border((SS_W, SS_H), 5), r.topleft)
        return ty + SS_H

    def _draw_saves(self, surf, y):
        active = (self._cur == "saves")
        c.rail_header(surf, SAFE, y, "Your saves on this game",
                      f"{len(self.states)} on file", max_w=CONTENT_W - 2 * SAFE)
        sy = y + 22
        sw = 196
        # Horizontal strip; left-side slot chip OR the RA-written thumbnail
        # (if present), then label + relative "when". Auto-save leads (the
        # list is pre-sorted that way) and reads "Auto" in peach so it's
        # visually the "resume exactly" entry.
        col = self.col["saves"]
        view = (CONTENT_W - 2 * SAFE) // (sw + 8) + 1
        start = max(0, min(col - view + 2, max(0, len(self.states) - view))) if active else 0
        for i in range(start, min(start + view + 1, len(self.states))):
            sv = self.states[i]
            sx = SAFE + (i - start) * (sw + 8)
            r = pygame.Rect(sx, sy, sw, 44)
            card = render.rounded_fill((r.w, r.h), t.Color.SURFACE_BASE, t.Radius.BUTTON)
            pygame.draw.rect(card, t.Color.BORDER_SOFT, (0, 0, r.w, r.h), width=1,
                             border_radius=t.Radius.BUTTON)
            accent = t.Color.ACCENT_PEACH if sv.is_auto else t.Color.ACCENT_CYAN
            # thumbnail (RA sidecar .png) when we have one, else a slot chip
            if sv.thumbnail:
                thumb = images.cover(sv.thumbnail, 34, 24, radius=4, mode="cover")
                card.blit(thumb, (8, 10))
            else:
                pygame.draw.rect(card, t.with_alpha(accent, 0.12), (9, 11, 22, 22),
                                 border_radius=5)
                chip = "A" if sv.is_auto else str(sv.slot)
                sl = fonts.render(t.Type.MONO, chip, accent)
                card.blit(sl, (9 + 11 - sl.get_width() // 2, 11 + 11 - sl.get_height() // 2))
            tx = 8 + (34 if sv.thumbnail else 22) + 10
            card.blit(fonts.render(t.Type.BODY_STRONG, sv.label, t.Color.TEXT_PRIMARY), (tx, 7))
            when = _rel_time(sv.mtime)
            card.blit(fonts.render(t.Type.TILE_SUB, when, t.Color.TEXT_MUTED), (tx, 26))
            focused = (active and i == col)
            if focused:
                glow, gpos = render.focus_glow(r, t.Radius.BUTTON); surf.blit(glow, gpos)
            surf.blit(card, r.topleft)
            if focused:
                surf.blit(render.gradient_border((r.w, r.h), t.Radius.BUTTON), r.topleft)
        return sy + 44

    def _draw_series(self, surf, y):
        s = self.series
        focused = (self._cur == "series")
        c.rail_header(surf, SAFE, y, "Series", "", max_w=CONTENT_W - 2 * SAFE)
        ry = y + 22
        r = pygame.Rect(SAFE, ry, CONTENT_W - 2 * SAFE, 42)
        body = render.rounded_gradient((r.w, r.h), t.Color.SURFACE_BASE, t.Color.BG_MID, t.Radius.BUTTON, 160)
        if not focused:
            pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, r.w, r.h), width=1, border_radius=t.Radius.BUTTON)
        line = f"Part of the {s['name']} series"
        body.blit(fonts.render(t.Type.BODY, line, t.Color.TEXT_SECONDARY), (14, (r.h - 18) // 2))
        hint = fonts.render(t.Type.CAPTION, "View all →", t.Color.ACCENT_CYAN)
        body.blit(hint, (r.w - 14 - hint.get_width(), (r.h - hint.get_height()) // 2))
        surf.blit(body, r.topleft)
        if focused:
            surf.blit(render.gradient_border((r.w, r.h), t.Radius.BUTTON), r.topleft)
        return ry + 42

    def _draw_recs(self, surf, y):
        active = (self._cur == "recs")
        c.rail_header(surf, SAFE, y, "If you finished this, the Clerk says", "deep cuts",
                      max_w=CONTENT_W - 2 * SAFE)
        ty = y + 22
        view = (t.VIEWPORT_WIDTH - 2 * SAFE) // (REC_W + 6) + 1
        col = self.col["recs"]
        start = max(0, min(col - view + 2, max(0, len(self.recs) - view))) if active else 0
        order = [k for k in range(start, min(start + view + 1, len(self.recs)))]
        for k in order:
            rg = self.recs[k]["game"]
            x = SAFE + (k - start) * (REC_W + 6)
            r = pygame.Rect(x, ty, REC_W, REC_ART_H)
            c.poster_tile(surf, r, path=self.app.catalog.asset_path(rg, "box"), name=rg["title"],
                          badge=c.rating_badge(rg.get("rating")), badge_r=c.sys_year(rg),
                          chips=[((rg.get("genre") or [None])[0], t.Color.TEXT_SECONDARY)],
                          sub=None, focused=(active and k == col), dim=active)
        ry = ty + REC_ART_H + 22
        return ry


def _rel_time(epoch: float) -> str:
    """Compact 'when' for a save state's mtime: 'just now' / 'Nm ago' /
    'Nh ago' / 'Nd ago' / a date past a week. Deterministic-friendly:
    purely relative to wall-clock now."""
    try:
        delta = max(0, time.time() - float(epoch))
    except (TypeError, ValueError):
        return ""
    mins = int(delta // 60)
    if mins < 1:
        return "just now"
    if mins < 60:
        return f"{mins}m ago"
    hrs = mins // 60
    if hrs < 24:
        return f"{hrs}h ago"
    days = hrs // 24
    if days < 7:
        return f"{days}d ago"
    return time.strftime("%b %d", time.localtime(epoch))
