"""
Storage module — St1 / St2 / St3.

Recreates the Claude Design handoff (St1-storage.html, St2-backup.html,
St3-restore.html) pixel-faithfully on the 640×480 stage. Storage talks
human: card *health* not partition tables, "Saves" / "Saves + settings" /
"Everything personal" not tarballs, side-by-side "newer wins" timestamps
not raw checksums.

Screens (class ⇄ handoff):
  St1Storage  ⇄ St1-storage.html  — per-card health, fill bars, checks strip,
                                     3 action buttons (backup / prepare / sync)
  St2Backup   ⇄ St2-backup.html   — 3-tier profile picker + target seg +
                                     last-backup row + big "Back up now" CTA
  St3Restore  ⇄ St3-restore.html  — preview stats + conflict list with a
                                     "newer wins" strategy seg + restore CTA

Every class shares the screen shape: name, __init__(self, app, ...),
on_action(self, action), update(self, dt_ms), draw(self, surf).
Helpers (fill bar, card tile, tier card, target/strategy seg, conflict
row, stat card, checks strip) are module-level functions below.

X is the consumer/technical toggle on St1 (the spec's "show details"):
the per-card key/value detail (label / filesystem / read-only / last
check) is hidden by default and only shown when the user asks for it.
"""

from __future__ import annotations

import os
import time

import pygame

from ..design import tokens as t
from ..shared import widgets as w
from ..shared import fonts, icons, render
from ..shared import backend

# tier index (St2 picker) → storageops tier key
_TIER_KEYS = ("saves", "settings", "full")
# target index → backup destination dir (SD1 = roms partition, SD2 = 2nd card)
_ROMS = os.environ.get("AAHEAD_ROMS_ROOT") or "/roms"
_ROMS2 = os.environ.get("AAHEAD_ROMS2_ROOT") or "/roms2"
_TARGET_DIRS = {0: f"{_ROMS}/backup", 1: f"{_ROMS2}/backup"}


def _fmt_bytes(n) -> str:
    """Consumer-facing size (42 MB / 1.3 GB), never raw bytes."""
    try:
        x = float(n)
    except (TypeError, ValueError):
        return "—"
    for unit in ("B", "KB", "MB", "GB", "TB"):
        if x < 1024 or unit == "TB":
            return f"{int(x)} {unit}" if unit == "B" else \
                (f"{x:.1f} {unit}" if x < 10 else f"{int(round(x))} {unit}")
        x /= 1024
    return f"{int(x)} TB"


def _fmt_when(mtime) -> str:
    """A backup's age in plain words ('today' / '3 d ago' / 'May 13')."""
    try:
        age = time.time() - float(mtime)
    except (TypeError, ValueError):
        return "—"
    if age < 3600:
        return "just now"
    if age < 86400:
        return f"{int(age // 3600)} h ago"
    if age < 7 * 86400:
        return f"{int(age // 86400)} d ago"
    return time.strftime("%b %d", time.localtime(mtime))

# ── stage geometry ────────────────────────────────────────────────────
STAGE_W = t.VIEWPORT_WIDTH          # 640
STAGE_H = t.VIEWPORT_HEIGHT         # 480
SAFE = t.Space.SAFE_AREA            # 16
BODY_W = STAGE_W - SAFE * 2         # 608
FOOTER_Y = 448                      # hairline above footer hints

# the dark inset panel tint reused by checks / target / last / conflicts
_INSET = t.with_alpha(t.Color.BG_DEEP, 0.42)


# ─── shared small parts ───────────────────────────────────────────────


def hero(surf, y, *, icon_name, icon_tint, title, title_size, sub_segments,
         side_big, side_small):
    """
    Module hero band (matches .st-hero / .b-hero / .r-hero): tinted rounded
    glyph + title + secondary subline (list of (text,color) segments,
    concatenated, no separators) + a right-aligned big number / small label.
    Draws a hairline rule beneath. Returns y below the rule.
    """
    gbox = 44
    box = render.rounded_gradient(
        (gbox, gbox), t.with_alpha(icon_tint, 0.22)[:3],
        t.with_alpha(t.Color.ACCENT_CYAN, 0.18)[:3], 11, 135)
    pygame.draw.rect(box, t.with_alpha(icon_tint, 0.40),
                     (0, 0, gbox, gbox), width=1, border_radius=11)
    ic = icons.icon(icon_name, 24, icon_tint)
    box.blit(ic, ((gbox - 24) // 2, (gbox - 24) // 2))
    surf.blit(box, (SAFE, y))

    # right side cluster (measured first so the title can stay clear of it)
    rx = STAGE_W - SAFE
    big = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 20, "Bold", -0.3),
                       side_big, t.Color.TEXT_PRIMARY)
    small = fonts.render(t.Type.META_SM, side_small, t.Color.TEXT_MUTED)
    side_w = max(big.get_width(), small.get_width())
    surf.blit(big, (rx - big.get_width(), y + 2))
    surf.blit(small, (rx - small.get_width(),
                      y + 2 + big.get_height() + 2))

    tx = SAFE + gbox + 14
    title_max = (rx - side_w - 12) - tx
    title_s = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, title_size,
                                       "Bold", -0.5), title, t.Color.TEXT_PRIMARY)
    if title_s.get_width() > title_max:
        title_s = title_s.subsurface((0, 0, max(title_max, 1),
                                      title_s.get_height()))
    surf.blit(title_s, (tx, y + 2))

    sy = y + 2 + title_s.get_height() + 4
    sx = tx
    sub_max = (rx - side_w - 12) - tx
    for text, color in sub_segments:
        seg = fonts.render(t.Type.BODY_SM, text, color)
        if sx + seg.get_width() > tx + sub_max:
            seg = seg.subsurface((0, 0, max(tx + sub_max - sx, 1),
                                  seg.get_height()))
            surf.blit(seg, (sx, sy))
            break
        surf.blit(seg, (sx, sy))
        sx += seg.get_width()

    rule_y = y + 56
    pygame.draw.line(surf, t.Color.BORDER_SOFT,
                     (SAFE, rule_y), (STAGE_W - SAFE, rule_y), 1)
    return rule_y + 1


def _inset_panel(rect, *, radius=10, tint=None):
    """Dark translucent inset card with a soft border (checks/target/last)."""
    body = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
    pygame.draw.rect(body, tint or _INSET, (0, 0, rect.w, rect.h),
                     border_radius=radius)
    pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                     width=1, border_radius=radius)
    return body


def fill_bar(surf, x, y, w_px, frac, *, kind="cyan_rose", h=8):
    """
    Rounded capacity bar. `kind` picks the fill gradient direction/colours:
      "cyan_rose" — cyan→rose (St1 system card)
      "mint_cyan" — mint→cyan (St1 games card, lower usage)
    `frac` 0..1 of capacity used.
    """
    track = render.rounded_fill((w_px, h), t.Color.BORDER_SOFT, 999)
    surf.blit(track, (x, y))
    fw = max(int(w_px * max(0.0, min(frac, 1.0))), h)
    if kind == "mint_cyan":
        c0, c1 = t.Color.SUCCESS, t.Color.ACCENT_CYAN
    else:
        c0, c1 = t.Color.ACCENT_CYAN, t.Color.ACCENT_ROSE
    fill = render.rounded_gradient((fw, h), c0, c1, 999, 90)
    surf.blit(fill, (x, y))


def _num_badge(n, *, mint=False):
    """Small gradient rounded card-number chip (rose→cyan, or mint→cyan)."""
    box = 22
    c0 = t.Color.SUCCESS if mint else t.Color.ACCENT_ROSE
    s = render.rounded_gradient((box, box), c0, t.Color.ACCENT_CYAN, 6, 135)
    lab = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 11, "Bold", 0.0),
                       str(n), t.Color.BG_UP)
    s.blit(lab, ((box - lab.get_width()) // 2, (box - lab.get_height()) // 2))
    return s


def _badge_pill(text, level):
    """Status pill on a card top row. level ∈ {"ok","warn"}."""
    col = t.Color.SUCCESS if level == "ok" else t.Color.ACCENT_PEACH
    lab = fonts.render(t.Type.META_SM, text, col)
    pw, ph = lab.get_width() + 14, 16
    pill = pygame.Surface((pw, ph), pygame.SRCALPHA)
    pygame.draw.rect(pill, t.with_alpha(col, 0.14), (0, 0, pw, ph),
                     border_radius=999)
    pygame.draw.rect(pill, t.with_alpha(col, 0.35), (0, 0, pw, ph),
                     width=1, border_radius=999)
    pill.blit(lab, ((pw - lab.get_width()) // 2, (ph - lab.get_height()) // 2))
    return pill


_KV_COL = {"ok": t.Color.SUCCESS, "warn": t.Color.ACCENT_PEACH,
           None: t.Color.TEXT_SECONDARY}


def sd_card(surf, rect, card, *, focused=False, show_detail=True):
    """
    St1 SD-card tile: number badge + label + health badge, capacity fill bar,
    used/total + free line, and (optionally) a key/value detail grid.
    `card` is the dict built in St1Storage.
    """
    radius = 12
    if focused:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_ELEVATED,
                                       t.Color.SURFACE_BASE, radius, 160)
    else:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_BASE,
                                       t.Color.BG_MID, radius, 160)
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=radius)

    pad_x, pad_t = 14, 12
    cy = pad_t + 11
    # top row: badge + label + health pill
    badge = _num_badge(card["num"], mint=card.get("badge_mint", False))
    body.blit(badge, (pad_x, pad_t))
    pill = _badge_pill(card["badge"], card["badge_level"])
    body.blit(pill, (rect.w - pad_x - pill.get_width(), cy - pill.get_height() // 2))
    lab = fonts.render(t.TextStyle(t.INTER_SEMIBOLD, 14, "SemiBold", -0.1),
                       card["label"], t.Color.TEXT_PRIMARY)
    lab_max = (rect.w - pad_x - pill.get_width() - 8) - (pad_x + badge.get_width() + 8)
    if lab.get_width() > lab_max:
        lab = lab.subsurface((0, 0, max(lab_max, 1), lab.get_height()))
    body.blit(lab, (pad_x + badge.get_width() + 8, cy - lab.get_height() // 2))

    # fill bar
    by = pad_t + 22 + 10
    fill_bar(body, pad_x, by, rect.w - pad_x * 2, card["used_frac"],
             kind=card.get("fill_kind", "cyan_rose"))

    # used/total · free line
    ly = by + 8 + 7
    used_s = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.0),
                          card["used_label"], t.Color.TEXT_PRIMARY)
    tot_s = fonts.render(t.Type.META_SM, " · " + card["total_label"],
                         t.Color.TEXT_MUTED)
    body.blit(used_s, (pad_x, ly))
    body.blit(tot_s, (pad_x + used_s.get_width(), ly + 1))
    free_s = fonts.render(t.Type.META_SM, card["free_label"], t.Color.SUCCESS)
    body.blit(free_s, (rect.w - pad_x - free_s.get_width(), ly + 1))

    # key/value detail grid (consumer toggle hides it)
    if show_detail:
        gy = ly + 12 + 4
        line_h = 15
        for i, (k, v, state) in enumerate(card["kv"]):
            ry = gy + i * line_h
            ks = fonts.render(t.Type.META_SM, k, t.Color.TEXT_MUTED)
            body.blit(ks, (pad_x, ry))
            vs = fonts.render(t.Type.META_SM, v, _KV_COL.get(state))
            vx = pad_x + 70
            maxw = rect.w - pad_x - vx
            if vs.get_width() > maxw:
                vs = vs.subsurface((0, 0, max(maxw, 1), vs.get_height()))
            body.blit(vs, (vx, ry))

    if focused:
        w.draw_focused_card(surf, rect, radius, scale=1.0, base_surface=body)
    else:
        surf.blit(body, rect.topleft)


def _pill_button(text, icon_name, *, icon_color=None):
    """Small rounded pill with a leading icon + mono label (Recheck/Browse)."""
    lab = fonts.render(t.Type.META_SM, text, t.Color.TEXT_SECONDARY)
    ic = icons.icon(icon_name, 11, icon_color or t.Color.ACCENT_CYAN)
    pw = 10 + ic.get_width() + 6 + lab.get_width() + 10
    ph = 26
    pill = render.rounded_fill((pw, ph), t.Color.BG_MID, 999)
    pygame.draw.rect(pill, t.Color.BORDER_SOFT, (0, 0, pw, ph),
                     width=1, border_radius=999)
    x = 10
    pill.blit(ic, (x, (ph - ic.get_height()) // 2))
    x += ic.get_width() + 6
    pill.blit(lab, (x, (ph - lab.get_height()) // 2))
    return pill


def checks_strip(surf, rect, checks, *, run_label="Recheck",
                 run_icon="ph-arrow-clockwise"):
    """
    St1 checks row: inline check items (icon + text w/ bold lead) and a right
    pill button. checks = list of (icon, lead, rest, level) where level ∈
    {"ok","warn","muted"}.
    """
    body = _inset_panel(rect)
    pad = 12
    cy = rect.h // 2
    # right pill measured first so checks never overlap it
    pill = _pill_button(run_label, run_icon)
    pill_x = rect.w - pad - pill.get_width()
    body.blit(pill, (pill_x, cy - pill.get_height() // 2))
    limit = pill_x - 10

    x = pad
    icol = {"ok": t.Color.SUCCESS, "warn": t.Color.ACCENT_PEACH,
            "muted": t.Color.TEXT_MUTED}
    tcol = {"ok": t.Color.TEXT_SECONDARY, "warn": t.Color.TEXT_SECONDARY,
            "muted": t.Color.TEXT_MUTED}
    for icon_name, lead, rest, level in checks:
        ic = icons.icon(icon_name, 14, icol[level])
        if x + 14 > limit:
            break
        body.blit(ic, (x, cy - 7))
        x += 14 + 8
        lead_s = fonts.render(t.TextStyle(t.INTER_MEDIUM, 12, "Medium", 0.0),
                              lead, t.Color.TEXT_PRIMARY if level != "muted"
                              else t.Color.TEXT_MUTED)
        body.blit(lead_s, (x, cy - lead_s.get_height() // 2))
        x += lead_s.get_width()
        if rest:
            rest_s = fonts.render(t.Type.BODY_SM, rest, tcol[level])
            if x + rest_s.get_width() > limit:
                rest_s = rest_s.subsurface((0, 0, max(limit - x, 1),
                                            rest_s.get_height()))
            body.blit(rest_s, (x, cy - rest_s.get_height() // 2))
            x += rest_s.get_width()
        x += 14
    surf.blit(body, rect.topleft)


def action_button(surf, rect, label, icon_name, *, focused=False, dim=False):
    """
    St1 / CTA action button: centered icon + label. Focused gets the elevated
    gradient + rose icon + glow; dim lowers opacity of a resting button.
    """
    radius = t.Radius.BUTTON
    if focused:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_ELEVATED,
                                       t.Color.SURFACE_BASE, radius, 160)
    else:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_BASE,
                                       t.Color.BG_MID, radius, 160)
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=radius)
    col = t.Color.TEXT_PRIMARY
    lab = fonts.render(t.TextStyle(t.INTER_SEMIBOLD if focused else t.INTER_MEDIUM,
                                   13, "SemiBold", 0.0), label, col)
    content_w = lab.get_width()
    ic = None
    if icon_name:
        icol = t.Color.ACCENT_ROSE if focused else t.Color.TEXT_SECONDARY
        ic = icons.icon(icon_name, 14, icol)
        content_w += ic.get_width() + 8
    cx = (rect.w - content_w) // 2
    cyc = rect.h // 2
    if ic:
        body.blit(ic, (cx, cyc - ic.get_height() // 2))
        cx += ic.get_width() + 8
    body.blit(lab, (cx, cyc - lab.get_height() // 2))
    if dim and not focused:
        body.set_alpha(int(255 * 0.55))
    if focused:
        w.draw_focused_card(surf, rect, radius, scale=1.0, base_surface=body)
    else:
        surf.blit(body, rect.topleft)


# ─── St2 tier (profile) card ──────────────────────────────────────────


def tier_card(surf, rect, tier, *, active=False, focused=False):
    """
    St2 profile tier: icon chip + title + radio, a wrapped description, and a
    dashed-top size/count footer. Active = elevated gradient + filled radio +
    rose icon; focused active adds the glow + gradient border.
    """
    radius = 12
    if active:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_ELEVATED,
                                       t.Color.SURFACE_BASE, radius, 160)
    else:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_BASE,
                                       t.Color.BG_MID, radius, 160)
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=radius)

    pad_x, pad_t = 14, 12
    # top: icon chip
    chip = 28
    if active:
        cbg = render.rounded_gradient((chip, chip),
                                      t.with_alpha(t.Color.ACCENT_ROSE, 0.20)[:3],
                                      t.with_alpha(t.Color.ACCENT_CYAN, 0.20)[:3],
                                      7, 135)
        pygame.draw.rect(cbg, t.with_alpha(t.Color.ACCENT_ROSE, 0.40),
                         (0, 0, chip, chip), width=1, border_radius=7)
        ic = icons.icon(tier["icon"], 16, t.Color.ACCENT_ROSE)
    else:
        cbg = pygame.Surface((chip, chip), pygame.SRCALPHA)
        pygame.draw.rect(cbg, t.with_alpha((255, 255, 255), 0.04),
                         (0, 0, chip, chip), border_radius=7)
        pygame.draw.rect(cbg, t.Color.BORDER_SOFT, (0, 0, chip, chip),
                         width=1, border_radius=7)
        ic = icons.icon(tier["icon"], 16, t.Color.TEXT_SECONDARY)
    cbg.blit(ic, ((chip - 16) // 2, (chip - 16) // 2))
    body.blit(cbg, (pad_x, pad_t))

    # radio (top-right)
    rr = 8
    rcx, rcy = rect.w - pad_x - rr, pad_t + chip // 2
    if active:
        ring = render.rounded_gradient((rr * 2, rr * 2), t.Color.ACCENT_ROSE,
                                       t.Color.ACCENT_CYAN, 999, 135)
        body.blit(ring, (rcx - rr, rcy - rr))
        pygame.draw.circle(body, t.Color.BG_UP, (rcx, rcy), rr - 4)
    else:
        pygame.draw.circle(body, t.Color.BORDER_BRIGHT, (rcx, rcy), rr, 2)

    # label (Space Grotesk bold 14), under/right of chip — wraps to 2 lines max
    lab_x = pad_x
    lab_y = pad_t + chip + 6
    lab_max = rect.w - pad_x * 2
    lab_y = _draw_wrapped(body, tier["label"],
                          t.TextStyle(t.SPACE_GROTESK_BOLD, 14, "Bold", -0.2),
                          t.Color.TEXT_PRIMARY, lab_x, lab_y, lab_max,
                          line_h=17, max_lines=2)

    # description (wrapped)
    desc_y = lab_y + 4
    _draw_wrapped(body, tier["desc"], t.Type.BODY_SM, t.Color.TEXT_SECONDARY,
                  pad_x, desc_y, rect.w - pad_x * 2, line_h=15, max_lines=4)

    # size footer pinned to bottom with a dashed rule above it
    foot_h = 22
    fy = rect.h - foot_h
    w.dashed_hline(body, pad_x, rect.w - pad_x, fy, t.Color.BORDER_SOFT, dash=4, gap=3)
    size_lead = fonts.render(t.Type.META_SM, "~ ", t.Color.TEXT_MUTED)
    size_val = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.0),
                            tier["size"], t.Color.TEXT_PRIMARY)
    body.blit(size_lead, (pad_x, fy + 8))
    body.blit(size_val, (pad_x + size_lead.get_width(), fy + 8))
    cnt = fonts.render(t.Type.META_SM, tier["count"], t.Color.ACCENT_CYAN)
    body.blit(cnt, (rect.w - pad_x - cnt.get_width(), fy + 8))

    if focused and active:
        w.draw_focused_card(surf, rect, radius, scale=1.0, base_surface=body)
    else:
        surf.blit(body, rect.topleft)


def _draw_wrapped(surf, text, style, color, x, y, max_w, *, line_h, max_lines):
    """Greedy word-wrap. Returns y after the last drawn line."""
    words = text.split()
    line = ""
    lines = []
    for word in words:
        trial = (line + " " + word).strip()
        if fonts.measure(style, trial)[0] <= max_w or not line:
            line = trial
        else:
            lines.append(line)
            line = word
        if len(lines) == max_lines:
            break
    if line and len(lines) < max_lines:
        lines.append(line)
    if len(lines) == max_lines and line and lines[-1] != line:
        lines[-1] = line
    for i, ln in enumerate(lines[:max_lines]):
        # ellipsise the final visible line if it overflows
        seg = fonts.render(style, ln, color)
        if seg.get_width() > max_w:
            seg = seg.subsurface((0, 0, max_w, seg.get_height()))
        surf.blit(seg, (x, y + i * line_h))
    return y + len(lines[:max_lines]) * line_h


# ─── segmented (target / strategy) ────────────────────────────────────


def mono_segmented(surf, rect, options, selected_idx, *, focused=False,
                   focused_idx=None, icons_list=None, h=None, font_px=10,
                   pad_x=11):
    """
    Compact mono segmented pill (St2 target / St3 strategy). options = list of
    label strings; optional icons_list parallel list of icon names (or None).
    Selected segment gets the elevated bg + cyan label. Auto-sizes each segment
    to its content. Returns the total width consumed.
    """
    hh = h or rect.h
    style = t.TextStyle(t.JETBRAINS_MONO, font_px, "Regular", 0.3)
    # measure segment widths
    seg_w = []
    for i, label in enumerate(options):
        lw = fonts.measure(style, label)[0]
        iw = 0
        if icons_list and icons_list[i]:
            iw = 11 + 5
        seg_w.append(pad_x * 2 + iw + lw)
    total = sum(seg_w) + 4  # 2px outer padding each side
    track = render.rounded_fill((total, hh), t.Color.BG_MID, 999)
    pygame.draw.rect(track, t.Color.BORDER_SOFT, (0, 0, total, hh),
                     width=1, border_radius=999)
    x = 2
    for i, label in enumerate(options):
        on = (i == selected_idx)
        sw = seg_w[i]
        sr = pygame.Rect(x, 2, sw, hh - 4)
        if on:
            seg_bg = render.rounded_fill((sr.w, sr.h), t.Color.SURFACE_ELEVATED, 999)
            track.blit(seg_bg, sr.topleft)
        col = t.Color.ACCENT_CYAN if on else t.Color.TEXT_MUTED
        cx = sr.x + pad_x
        ccy = sr.centery
        if icons_list and icons_list[i]:
            ic = icons.icon(icons_list[i], 11, col)
            track.blit(ic, (cx, ccy - ic.get_height() // 2))
            cx += 11 + 5
        lab = fonts.render(style, label, col)
        track.blit(lab, (cx, ccy - lab.get_height() // 2))
        x += sw
    surf.blit(track, rect.topleft)
    # focus ring around the focused option
    if focused and focused_idx is not None:
        fx = rect.x + 2 + sum(seg_w[:focused_idx])
        fr = pygame.Rect(fx, rect.y + 2, seg_w[focused_idx], hh - 4)
        surf.blit(render.gradient_border((fr.w, fr.h), 999), fr.topleft)
    return total


# ─── St3 conflict row + stat card ─────────────────────────────────────


def stat_card(surf, rect, *, key, value, value_state, detail):
    """St3 preview column: uppercase key, big colored value, small detail."""
    radius = 10
    body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_BASE,
                                   t.Color.BG_MID, radius, 160)
    pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                     width=1, border_radius=radius)
    pad = 12
    k = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 9, "Regular", 0.7),
                     key, t.Color.TEXT_MUTED, upper=True)
    body.blit(k, (pad, 8))
    vcol = {"ok": t.Color.SUCCESS, "warn": t.Color.ACCENT_PEACH,
            "cyan": t.Color.ACCENT_CYAN}.get(value_state, t.Color.TEXT_PRIMARY)
    v = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 18, "Bold", -0.3),
                     value, vcol)
    body.blit(v, (pad, 8 + k.get_height() + 3))
    d = fonts.render(t.Type.BODY_SM, detail, t.Color.TEXT_SECONDARY)
    if d.get_width() > rect.w - pad * 2:
        d = d.subsurface((0, 0, rect.w - pad * 2, d.get_height()))
    body.blit(d, (pad, rect.h - 8 - d.get_height()))
    surf.blit(body, rect.topleft)


def conflict_row(surf, body, y, row, row_w, *, pad):
    """
    One St3 conflict line on `body`: warn glyph + name (mono, with cyan
    location), then two timestamp pills (backup vs device). The winning side
    (device when "newer wins") gets a mint pill with a trailing check.
    """
    cy = y + 9
    ic = icons.icon("ph-warning-circle", 12, t.Color.ACCENT_PEACH)
    body.blit(ic, (pad, cy - 6))

    # right side: two pills measured right→left
    rx = row_w - pad
    # device pill (b) — winner styling if row says so. The bundled mono font
    # has no ✓ glyph, so the winner check is drawn as a ph-check icon.
    win = row.get("win", True)
    bcol = t.Color.SUCCESS if win else t.Color.TEXT_SECONDARY
    blab = fonts.render(t.Type.META_SM, row["device"], bcol)
    check = icons.icon("ph-check", 9, bcol) if win else None
    bpw = blab.get_width() + 14 + ((check.get_width() + 4) if check else 0)
    rx -= bpw
    bpill = pygame.Surface((bpw, 16), pygame.SRCALPHA)
    pygame.draw.rect(bpill, t.with_alpha(bcol, 0.10), (0, 0, bpw, 16),
                     border_radius=4)
    pygame.draw.rect(bpill, t.with_alpha(bcol, 0.30), (0, 0, bpw, 16),
                     width=1, border_radius=4)
    bx = 7
    bpill.blit(blab, (bx, (16 - blab.get_height()) // 2))
    if check:
        bpill.blit(check, (bx + blab.get_width() + 4, (16 - check.get_height()) // 2))
    body.blit(bpill, (rx, cy - 8))
    rx -= 8
    # backup pill (a)
    acol = t.Color.ACCENT_PEACH
    alab = fonts.render(t.Type.META_SM, row["backup"], acol)
    apw = alab.get_width() + 14
    rx -= apw
    apill = pygame.Surface((apw, 16), pygame.SRCALPHA)
    pygame.draw.rect(apill, t.with_alpha(acol, 0.10), (0, 0, apw, 16),
                     border_radius=4)
    pygame.draw.rect(apill, t.with_alpha(acol, 0.25), (0, 0, apw, 16),
                     width=1, border_radius=4)
    apill.blit(alab, ((apw - alab.get_width()) // 2, (16 - alab.get_height()) // 2))
    body.blit(apill, (rx, cy - 8))
    rx -= 8

    # name fills the middle, file in primary + location in cyan, ellipsised
    nx = pad + 12 + 8
    fname = fonts.render(t.Type.META_SM, row["file"] + " · ", t.Color.TEXT_PRIMARY)
    loc = fonts.render(t.Type.META_SM, row["loc"], t.Color.ACCENT_CYAN)
    maxw = max(10, rx - nx - 8)
    if fname.get_width() + loc.get_width() > maxw:
        # prefer keeping the filename; clip the location
        if fname.get_width() < maxw:
            loc = loc.subsurface((0, 0, max(maxw - fname.get_width(), 1),
                                  loc.get_height()))
        else:
            fname = fname.subsurface((0, 0, maxw, fname.get_height()))
            loc = None
    body.blit(fname, (nx, cy - fname.get_height() // 2))
    if loc is not None:
        body.blit(loc, (nx + fname.get_width(), cy - loc.get_height() // 2))


# ══════════════════════════════════════════════════════════════════════
#  St1 — Storage health
# ══════════════════════════════════════════════════════════════════════


class St1Storage:
    name = "storage"

    def __init__(self, app):
        self.app = app
        # focusable order: card 0, card 1, then 3 actions (2..4)
        self.focus = 0
        self.show_detail = False   # X reveals the per-card key/value tier

        self.cards = [
            dict(num=1, label="System card", badge="healthy", badge_level="ok",
                 used_frac=0.78, fill_kind="cyan_rose",
                 used_label="50 GB used", total_label="64 GB total",
                 free_label="14 GB free",
                 kv=[("label", "EASYROMS", None),
                     ("filesystem", "exfat · clean", "ok"),
                     ("read-only", "no", "ok"),
                     ("last check", "2 min ago", None)]),
            dict(num=2, label="Games card", badge="healthy", badge_level="ok",
                 badge_mint=True, used_frac=0.26, fill_kind="mint_cyan",
                 used_label="8 GB used", total_label="32 GB total",
                 free_label="24 GB free",
                 kv=[("label", "EASYROMS2", None),
                     ("filesystem", "exfat · clean", "ok"),
                     ("read-only", "no", "ok"),
                     ("last check", "2 min ago", None)]),
        ]
        self.checks = [
            ("ph-check", "Real capacity", " matches label", "ok"),
            ("ph-check", "Write test", " passed", "ok"),
            ("ph-circle", "Deep test", " (needs empty card)", "muted"),
        ]
        self.actions = [
            ("Back up saves", "ph-download-simple"),
            ("Prepare new card", "ph-plus-circle"),
            ("Sync between cards", "ph-arrow-clockwise"),
        ]

    # ── input ──────────────────────────────────────────────────────────
    def on_action(self, action):
        n = 2 + len(self.actions)  # 5 focusables
        if action == "left":
            if self.focus == 1:
                self.focus = 0
            elif self.focus >= 2:
                self.focus = max(2, self.focus - 1)
        elif action == "right":
            if self.focus == 0:
                self.focus = 1
            elif self.focus >= 2:
                self.focus = min(n - 1, self.focus + 1)
        elif action == "up":
            if self.focus >= 2:
                self.focus = 0 if self.focus == 2 else 1
        elif action == "down":
            if self.focus < 2:
                self.focus = 2 + self.focus  # card→aligned action
        elif action == "a":
            self._activate()
        elif action == "x":
            self.show_detail = not self.show_detail
            self.app.toast("Card details on" if self.show_detail
                           else "Card details off")
        elif action == "y":
            self.app.toast("Rechecking cards…")
        elif action == "b":
            self.app.pop()

    def _activate(self):
        if self.focus in (0, 1):
            self.app.toast(f"{self.cards[self.focus]['label']} — looks healthy")
        else:
            idx = self.focus - 2
            label = self.actions[idx][0]
            if idx == 0:
                self.app.push(St2Backup(self.app))
            else:
                self.app.toast(f"{label} — coming soon")

    def update(self, dt_ms):
        pass

    # ── draw ───────────────────────────────────────────────────────────
    def draw(self, surf):
        w.status_bar(surf, title="Storage", back=True)
        y = hero(surf, 56, icon_name="ph-check-circle",
                 icon_tint=t.Color.SUCCESS, title="Cards look healthy",
                 title_size=20,
                 sub_segments=[("Both SD cards checked · ", t.Color.TEXT_SECONDARY),
                               ("no read-only or fake-size issues",
                                t.Color.SUCCESS)],
                 side_big="38 GB", side_small="free total")

        # two SD cards side by side
        gap = 10
        card_w = (BODY_W - gap) // 2
        card_h = 140 if self.show_detail else 96
        cy0 = y + 10
        for i, card in enumerate(self.cards):
            cx = SAFE + i * (card_w + gap)
            sd_card(surf, pygame.Rect(cx, cy0, card_w, card_h), card,
                    focused=(self.focus == i), show_detail=self.show_detail)

        # checks strip
        chk_y = cy0 + card_h + 10
        checks_strip(surf, pygame.Rect(SAFE, chk_y, BODY_W, 40), self.checks)

        # 3 action buttons
        act_y = chk_y + 40 + 8
        a_gap = 8
        a_w = (BODY_W - a_gap * 2) // 3
        for i, (label, icon_name) in enumerate(self.actions):
            ax = SAFE + i * (a_w + a_gap)
            action_button(surf, pygame.Rect(ax, act_y, a_w, 42), label,
                          icon_name, focused=(self.focus == 2 + i),
                          dim=(self.focus != 2 + i and i != 0))

        w.footer_hints(surf, [("A", "open"), ("X", "show details"),
                              ("Y", "recheck"), ("B", "back")])


# ══════════════════════════════════════════════════════════════════════
#  St2 — Back up saves (snapshot wizard)
# ══════════════════════════════════════════════════════════════════════


class St2Backup:
    name = "storage-backup"

    def __init__(self, app):
        self.app = app
        # tiers 0..2 ; CTA buttons 3 (Cancel) / 4 (Back up). target seg = 5
        self.tier = 1               # "Saves & settings" recommended/default
        self.focus = 1              # focus starts on the recommended tier
        # Is a 2nd SD card mounted? True/False live, None on dev (assume yes).
        self.has_sd2 = backend.storage_second_sd()
        # Default to SD 2 (the safer off-card copy) when it's there; fall back
        # to SD 1 when there's no 2nd card so the default never dead-ends.
        self.target = 0 if self.has_sd2 is False else 1

        self.tiers = [
            dict(icon="ph-game-controller", label="Just saves",
                 desc="Game saves and state slots only. Quickest, smallest.",
                 size="42 MB", count="214 saves"),
            dict(icon="ph-sliders-horizontal", label="Saves & settings",
                 desc="Saves, controller mapping, audio + power profiles, "
                      "Wi-Fi nets.",
                 size="68 MB", count="recommended"),
            dict(icon="ph-file-text", label="Everything personal",
                 desc="Above + themes, screenshots, port settings. "
                      "No ROMs / BIOS.",
                 size="340 MB", count="full profile"),
        ]
        self.targets = ["SD 1", "SD 2", "over Wi-Fi"]
        self.target_icons = ["ph-device-mobile", "ph-device-mobile",
                             "ph-wifi-high"]
        self.target_path = self._target_label()
        self.eta = "~ 12 s"

        # ── real backup state (rendered in-UI; NEVER a console/TUI) ──────
        self.phase = "idle"          # idle | running | done | error
        self.job = None              # off-thread tar (backend.call_async)
        self.result = None           # {ok, archive, size, error} on finish
        self.elapsed = 0.0           # spinner clock
        self.last_backup_text = self._read_last_backup()

    def _read_last_backup(self) -> str:
        """Real 'last backup' line when live; demo text otherwise."""
        lb = backend.storage_latest_backup(_TARGET_DIRS.get(0, ""))
        if not lb:
            return None        # not live (dev) → draw() keeps the demo row
        return f"{_fmt_when(lb['mtime'])} · {_fmt_bytes(lb['size'])}"

    def _target_label(self) -> str:
        """Path shown next to the target picker — flags a missing 2nd card."""
        if self.target == 2:
            return "over Wi-Fi"
        if self.target == 1 and self.has_sd2 is False:
            return "insert a 2nd card"
        return _TARGET_DIRS.get(self.target, "—")

    # ── input ──────────────────────────────────────────────────────────
    def on_action(self, action):
        # while the backup modal is up, AAhead owns the screen — swallow nav.
        if self.phase == "running":
            return
        if self.phase in ("done", "error"):
            if action in ("a", "b"):
                self._dismiss_modal()
            return
        if action == "left":
            if self.focus <= 2:
                self.focus = self.tier = max(0, self.focus - 1)
            elif self.focus == 4:
                self.focus = 3
        elif action == "right":
            if self.focus <= 2:
                self.focus = self.tier = min(2, self.focus + 1)
            elif self.focus == 3:
                self.focus = 4
        elif action == "down":
            if self.focus <= 2:
                self.focus = 5            # into target seg
            elif self.focus == 5:
                self.focus = 4            # into CTA
        elif action == "up":
            if self.focus == 5:
                self.focus = self.tier
            elif self.focus in (3, 4):
                self.focus = 5
        elif action == "a":
            self._activate()
        elif action == "x":
            self.app.push(St3Restore(self.app))   # browse backups → restore
        elif action == "y":
            self.app.toast("Schedule backups — coming soon")
        elif action == "b":
            self.app.pop()

    def _activate(self):
        if self.focus <= 2:
            self.tier = self.focus
            self.app.toast(f"{self.tiers[self.tier]['label']} selected")
        elif self.focus == 5:
            self.target = (self.target + 1) % len(self.targets)
            self.target_path = self._target_label()
        elif self.focus == 3:
            self.app.pop()
        elif self.focus == 4:
            self._begin_backup()

    # ── real backup (off-thread; progress rendered, never a console) ─────
    def _begin_backup(self):
        if self.target == 2:                      # "over Wi-Fi" — no receiver
            self.app.toast("Wi-Fi backup target — coming soon")
            return
        if self.target == 1 and self.has_sd2 is False:
            # SD 2 chosen but no 2nd card mounted → in-UI error, write nothing
            # (run_backup also refuses, but don't even start the modal-as-running)
            self.phase = "error"
            self.elapsed = 0.0
            self.result = {"error": "No 2nd SD card — insert one to back it up there"}
            return
        tier_key = _TIER_KEYS[self.tier]
        target_dir = _TARGET_DIRS[self.target]
        self.phase = "running"
        self.elapsed = 0.0
        self.result = None
        self.job = None
        if backend.live_enabled():
            self.job = backend.call_async("storage.backup",
                                          backend.storage_backup,
                                          tier_key, target_dir)
        # not live → self.job stays None; update() simulates a short delay

    def _resolve_backup(self, res):
        # res: dict from storageops, or None (couldn't start), or a simulated dict
        if res is None:
            self.phase = "error"
            self.result = {"error": "couldn't start backup"}
        elif res.get("ok"):
            self.phase = "done"
            self.result = res
            self.last_backup_text = f"just now · {_fmt_bytes(res.get('size'))}"
        else:
            self.phase = "error"
            self.result = res

    def _dismiss_modal(self):
        self.phase = "idle"
        self.job = None
        self.result = None

    def update(self, dt_ms):
        if self.phase != "running":
            return
        self.elapsed += dt_ms
        if self.job is not None:
            if self.job.done:
                self._resolve_backup(self.job.result)
        elif self.elapsed >= 1600:        # dev/simulation path (not live)
            est = {"saves": 42, "settings": 68, "full": 340}[_TIER_KEYS[self.tier]]
            self._resolve_backup({"ok": True, "size": est * 1024 * 1024,
                                  "archive": "(demo)", "error": None})

    # ── draw ───────────────────────────────────────────────────────────
    def draw(self, surf):
        w.status_bar(surf, title="Back up", back=True)
        y = hero(surf, 56, icon_name="ph-download-simple",
                 icon_tint=t.Color.SUCCESS, title="Make a snapshot",
                 title_size=20,
                 sub_segments=[("Saves and settings, copied somewhere safe. ",
                                t.Color.TEXT_SECONDARY),
                               ("You can restore later.", t.Color.TEXT_PRIMARY)],
                 side_big="7 d", side_small="since last backup")

        # 3-tier picker
        gap = 10
        tier_w = (BODY_W - gap * 2) // 3
        tier_h = 138
        ty0 = y + 10
        for i, tier in enumerate(self.tiers):
            tx = SAFE + i * (tier_w + gap)
            tier_card(surf, pygame.Rect(tx, ty0, tier_w, tier_h), tier,
                      active=(self.tier == i), focused=(self.focus == i))

        # target picker
        tg_y = ty0 + tier_h + 10
        tg_rect = pygame.Rect(SAFE, tg_y, BODY_W, 44)
        surf.blit(_inset_panel(tg_rect, tint=t.with_alpha(t.Color.BG_DEEP, 0.5)),
                  tg_rect.topleft)
        pad = 14
        lbl = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.4),
                           "SAVE TO", t.Color.TEXT_MUTED, upper=True)
        surf.blit(lbl, (tg_rect.x + pad, tg_rect.centery - lbl.get_height() // 2))
        seg_x = tg_rect.x + pad + lbl.get_width() + 10
        seg_rect = pygame.Rect(seg_x, tg_rect.y + (44 - 28) // 2, 1, 28)
        seg_w = mono_segmented(surf, seg_rect, self.targets, self.target,
                               focused=(self.focus == 5), focused_idx=self.target,
                               icons_list=self.target_icons, h=28)
        # path meta (right) — caret drawn instead of a "→" (no mono glyph)
        arrow = icons.icon("ph-caret-right", 10, t.Color.TEXT_SECONDARY)
        path = fonts.render(t.Type.META_SM, self.target_path,
                            t.Color.TEXT_SECONDARY)
        avail_x = seg_x + seg_w + 12
        max_path = max(tg_rect.right - pad - avail_x - arrow.get_width() - 4, 1)
        if path.get_width() > max_path:
            path = path.subsurface((0, 0, max_path, path.get_height()))
        block_w = arrow.get_width() + 4 + path.get_width()
        bx = tg_rect.right - pad - block_w
        surf.blit(arrow, (bx, tg_rect.centery - arrow.get_height() // 2))
        surf.blit(path, (bx + arrow.get_width() + 4,
                         tg_rect.centery - path.get_height() // 2))

        # last backup row
        last_y = tg_y + 44 + 8
        last_rect = pygame.Rect(SAFE, last_y, BODY_W, 36)
        lbody = _inset_panel(last_rect, radius=8)
        lp = 12
        pygame.draw.circle(lbody, t.Color.SUCCESS, (lp + 3, last_rect.h // 2), 3)
        lead = fonts.render(t.TextStyle(t.INTER_MEDIUM, 12, "Medium", 0.0),
                            "Last backup: ", t.Color.TEXT_PRIMARY)
        rest = fonts.render(t.Type.BODY_SM,
                            self.last_backup_text or
                            "May 13 · Saves & settings · 64 MB · on SD 2",
                            t.Color.TEXT_SECONDARY)
        lx = lp + 6 + 8
        lbody.blit(lead, (lx, last_rect.h // 2 - lead.get_height() // 2))
        lbody.blit(rest, (lx + lead.get_width(),
                          last_rect.h // 2 - rest.get_height() // 2))
        browse = _pill_button("Browse all", "ph-list",
                              icon_color=t.Color.TEXT_SECONDARY)
        lbody.blit(browse, (last_rect.w - lp - browse.get_width(),
                            last_rect.h // 2 - browse.get_height() // 2))
        surf.blit(lbody, last_rect.topleft)

        # CTA row (1 : 1.5)
        cta_y = last_y + 36 + 8
        cgap = 10
        cancel_w = int((BODY_W - cgap) * (1 / 2.5))
        backup_w = BODY_W - cgap - cancel_w
        action_button(surf, pygame.Rect(SAFE, cta_y, cancel_w, 44), "Cancel",
                      None, focused=(self.focus == 3), dim=(self.focus != 3))
        action_button(surf, pygame.Rect(SAFE + cancel_w + cgap, cta_y,
                                        backup_w, 44),
                      f"Back up now · {self.eta}", "ph-download-simple",
                      focused=(self.focus == 4), dim=False)

        w.footer_hints(surf, [("A", "back up"), ("X", "browse backups"),
                              ("Y", "schedule"), ("B", "back")])

        if self.phase != "idle":
            self._draw_modal(surf)

    def _draw_modal(self, surf):
        """In-UI backup progress/result. AAhead owns the framebuffer — there is
        NO console or TUI; tar runs silently off-thread and we render this."""
        w.scrim(surf, alpha=0.55)
        cw, ch = 400, 188
        rect = pygame.Rect((STAGE_W - cw) // 2, (STAGE_H - ch) // 2, cw, ch)
        w.modal_card(surf, rect)
        surf.blit(render.gradient_border((cw, ch), t.Radius.CARD), rect.topleft)
        pad = 24
        cx = rect.centerx
        tier_label = self.tiers[self.tier]["label"]

        if self.phase == "running":
            w.conic_ring(surf, cx, rect.y + 52, 18,
                         (self.elapsed % 1100) / 1100, thickness=4)
            title = fonts.render(t.Type.TITLE, "Backing up…", t.Color.TEXT_PRIMARY)
            surf.blit(title, (cx - title.get_width() // 2, rect.y + 80))
            sub = fonts.render(t.Type.BODY_SM,
                               f"{tier_label} · {self.target_path}",
                               t.Color.TEXT_SECONDARY)
            surf.blit(sub, (cx - sub.get_width() // 2, rect.y + 80 + 26))
            note = fonts.render(t.Type.META_SM, "keep the device powered",
                                t.Color.TEXT_MUTED)
            surf.blit(note, (cx - note.get_width() // 2, rect.bottom - pad - 4))
            return

        ok = self.phase == "done"
        col = t.Color.SUCCESS if ok else t.Color.ERROR
        ic = icons.icon("ph-check-circle" if ok else "ph-warning-circle", 34, col)
        surf.blit(ic, (cx - ic.get_width() // 2, rect.y + 26))
        title = fonts.render(t.Type.TITLE,
                             "Backup complete" if ok else "Backup failed", col)
        surf.blit(title, (cx - title.get_width() // 2, rect.y + 66))
        if ok:
            detail = f"{_fmt_bytes((self.result or {}).get('size'))} · {tier_label}"
        else:
            detail = (self.result or {}).get("error") or "something went wrong"
        ds = fonts.render(t.Type.BODY_SM, detail, t.Color.TEXT_SECONDARY)
        if ds.get_width() > cw - pad * 2:
            ds = ds.subsurface((0, 0, cw - pad * 2, ds.get_height()))
        surf.blit(ds, (cx - ds.get_width() // 2, rect.y + 66 + 28))
        hint = fonts.render(t.Type.META_SM, "A  continue", t.Color.ACCENT_CYAN)
        surf.blit(hint, (cx - hint.get_width() // 2, rect.bottom - pad - 4))


# ══════════════════════════════════════════════════════════════════════
#  St3 — Restore from backup
# ══════════════════════════════════════════════════════════════════════


class St3Restore:
    name = "storage-restore"

    def __init__(self, app, snapshot="May 13 · Saves & settings"):
        self.app = app
        self.snapshot = snapshot
        self.strategy = 0          # 0 newer wins (default) / 1 ask each / 2 backup
        self.focus = 2             # focus the Restore CTA (matches HTML)
        # focus map: 0 strategy seg, 1 Cancel, 2 Restore

        self.stats = [
            dict(key="will copy in", value="198 files", state="ok",
                 detail="unchanged from backup"),
            dict(key="conflicts", value="9 files", state="warn",
                 detail="both sides newer than backup"),
            dict(key="will leave alone", value="19 files", state="cyan",
                 detail="not in this backup"),
        ]
        self.strategies = ["newer wins", "ask each", "backup wins"]
        self.conflicts = [
            dict(file="SOTN.savestate", loc="PSX/Castlevania",
                 backup="backup · 7 d ago", device="device · 2 h ago", win=True),
            dict(file="FF7.sav", loc="PSX/Final Fantasy VII",
                 backup="backup · 7 d ago", device="device · yesterday",
                 win=True),
            dict(file="controller.cfg", loc="mapping",
                 backup="backup · 7 d ago", device="device · 4 h ago", win=True),
            dict(file="tekken3.state0", loc="PSX/Tekken 3",
                 backup="backup · 7 d ago", device="device · 1 d ago", win=True),
            dict(file="audio.toml", loc="Bluetooth volume",
                 backup="backup · 7 d ago", device="device · 6 h ago", win=True),
        ]
        self.eta = "9 s"

    # ── input ──────────────────────────────────────────────────────────
    def on_action(self, action):
        if action == "left":
            if self.focus == 0:
                self.strategy = max(0, self.strategy - 1)
            elif self.focus == 2:
                self.focus = 1
        elif action == "right":
            if self.focus == 0:
                self.strategy = min(2, self.strategy + 1)
            elif self.focus == 1:
                self.focus = 2
        elif action == "up":
            if self.focus in (1, 2):
                self.focus = 0
        elif action == "down":
            if self.focus == 0:
                self.focus = 2
        elif action == "a":
            if self.focus == 0:
                self.strategy = (self.strategy + 1) % len(self.strategies)
                self.app.toast(f"Conflicts: {self.strategies[self.strategy]}")
            elif self.focus == 1:
                self.app.pop()
            else:
                self.app.toast(f"Restoring · {self.snapshot}…")
        elif action == "x":
            self.strategy = (self.strategy + 1) % len(self.strategies)
            self.app.toast(f"Conflicts: {self.strategies[self.strategy]}")
        elif action == "y":
            self.app.toast("Review each conflict — coming soon")
        elif action == "b":
            self.app.pop()

    def update(self, dt_ms):
        pass

    # ── draw ───────────────────────────────────────────────────────────
    def draw(self, surf):
        w.status_bar(surf, title="Restore", back=True)
        y = hero(surf, 56, icon_name="ph-arrow-clockwise",
                 icon_tint=t.Color.ACCENT_ROSE,
                 title=f'Restore "{self.snapshot}"', title_size=19,
                 sub_segments=[("214 saves · 12 settings files · 7 Wi-Fi "
                                "networks", t.Color.TEXT_PRIMARY),
                               (" — preview before anything writes.",
                                t.Color.TEXT_SECONDARY)],
                 side_big="64 MB", side_small="backup size")

        # 3 preview stat columns
        gap = 8
        stat_w = (BODY_W - gap * 2) // 3
        stat_h = 62
        sy0 = y + 10
        for i, st in enumerate(self.stats):
            sx = SAFE + i * (stat_w + gap)
            stat_card(surf, pygame.Rect(sx, sy0, stat_w, stat_h),
                      key=st["key"], value=st["value"],
                      value_state=st["state"], detail=st["detail"])

        # conflicts panel fills the remaining body up to the CTA row
        conf_y = sy0 + stat_h + 8
        cta_h = 42
        cta_y = FOOTER_Y - 8 - cta_h
        conf_rect = pygame.Rect(SAFE, conf_y, BODY_W, cta_y - 8 - conf_y)
        self._draw_conflicts(surf, conf_rect)

        # CTA row (1 : 1.4)
        cgap = 10
        cancel_w = int((BODY_W - cgap) * (1 / 2.4))
        restore_w = BODY_W - cgap - cancel_w
        action_button(surf, pygame.Rect(SAFE, cta_y, cancel_w, cta_h), "Cancel",
                      "ph-x", focused=(self.focus == 1), dim=(self.focus != 1))
        action_button(surf, pygame.Rect(SAFE + cancel_w + cgap, cta_y,
                                        restore_w, cta_h),
                      f"Restore · {self.eta}", "ph-arrow-clockwise",
                      focused=(self.focus == 2), dim=False)

        w.footer_hints(surf, [("A", "restore"), ("X", "change strategy"),
                              ("Y", "review each"), ("B", "cancel")])

    def _draw_conflicts(self, surf, rect):
        body = _inset_panel(rect)
        pad = 12
        # header: "CONFLICTS" + count pill + strategy seg (right)
        hy = 8
        h3 = fonts.render(t.TextStyle(t.INTER_SEMIBOLD, 10, "SemiBold", 1.3),
                          "CONFLICTS", t.Color.TEXT_MUTED, upper=True)
        body.blit(h3, (pad, hy + 2))
        cx = pad + h3.get_width() + 8
        cnt = fonts.render(t.Type.META_SM, "9 files", t.Color.ACCENT_PEACH)
        cpw = cnt.get_width() + 12
        cpill = pygame.Surface((cpw, 14), pygame.SRCALPHA)
        pygame.draw.rect(cpill, t.with_alpha(t.Color.ACCENT_PEACH, 0.12),
                         (0, 0, cpw, 14), border_radius=999)
        pygame.draw.rect(cpill, t.with_alpha(t.Color.ACCENT_PEACH, 0.30),
                         (0, 0, cpw, 14), width=1, border_radius=999)
        cpill.blit(cnt, ((cpw - cnt.get_width()) // 2, (14 - cnt.get_height()) // 2))
        body.blit(cpill, (cx, hy + 1))
        # strategy seg (right) — measured first so it can be right-aligned
        total = _seg_measure(self.strategies, font_px=9, pad_x=8)
        seg_rect = pygame.Rect(rect.w - pad - total, hy - 2, 1, 20)
        mono_segmented(surf=body, rect=seg_rect, options=self.strategies,
                       selected_idx=self.strategy, focused=(self.focus == 0),
                       focused_idx=self.strategy, h=20, font_px=9, pad_x=8)

        # conflict rows
        row_h = 20
        ry = hy + max(h3.get_height(), 20) + 6
        rows_area_bottom = rect.h - 30   # leave room for totals footer
        shown = 0
        for row in self.conflicts:
            if ry + row_h > rows_area_bottom:
                break
            conflict_row(surf, body, ry, row, rect.w, pad=pad)
            ry += row_h
            shown += 1

        # totals footer (dashed top)
        ty = rect.h - 24
        w.dashed_hline(body, pad, rect.w - pad, ty, t.Color.BORDER_SOFT, dash=4, gap=3)
        # the HTML hard-codes "4 more conflicts"; keep that wording faithful
        left_lead = fonts.render(t.Type.META_SM, "4 more conflicts · ",
                                 t.Color.TEXT_MUTED)
        strat_v = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.0),
                               '"newer wins"', t.Color.TEXT_PRIMARY)
        tail = fonts.render(t.Type.META_SM, " resolves all in your favor",
                            t.Color.TEXT_MUTED)
        fx = pad
        fy = ty + 6
        body.blit(left_lead, (fx, fy)); fx += left_lead.get_width()
        body.blit(strat_v, (fx, fy)); fx += strat_v.get_width()
        body.blit(tail, (fx, fy))
        # "Y to inspect each →" — caret drawn (no mono → glyph)
        right = fonts.render(t.Type.META_SM, "Y to inspect each",
                             t.Color.ACCENT_CYAN)
        rcaret = icons.icon("ph-caret-right", 10, t.Color.ACCENT_CYAN)
        rblock = right.get_width() + 4 + rcaret.get_width()
        rx2 = rect.w - pad - rblock
        body.blit(right, (rx2, fy))
        body.blit(rcaret, (rx2 + right.get_width() + 4,
                           fy + (right.get_height() - rcaret.get_height()) // 2))

        surf.blit(body, rect.topleft)


def _seg_measure(options, *, font_px, pad_x, icons_list=None):
    """Total pixel width a mono_segmented with these options will occupy."""
    style = t.TextStyle(t.JETBRAINS_MONO, font_px, "Regular", 0.3)
    total = 4
    for i, label in enumerate(options):
        lw = fonts.measure(style, label)[0]
        iw = (11 + 5) if (icons_list and icons_list[i]) else 0
        total += pad_x * 2 + iw + lw
    return total
