"""
System overlays — QM Quick menu, RC Reconnecting, RB Reboot, BC Battery.

Unlike the dialogs in dialogs.py these are a mix of forms: a bottom-anchored
dock (Quick menu), a transient top banner (Reconnecting), and two centered
modals (Reboot / Battery). Each renders its underlay screen first so the
user keeps mental context, then its own surface on top.

  QuickMenu        ⇄ QM-quick-menu.html      (Start dock, not centered)
  Reconnecting     ⇄ RC-reconnecting.html    (top banner, no full scrim)
  RebootConfirm    ⇄ RB-reboot-confirm.html  (centered modal + 15s timer)
  BatteryCritical  ⇄ BC-battery-critical.html (centered peach/error modal)

S12 toast is handled by the App itself, so it is intentionally absent here.
"""

from __future__ import annotations

import math

import pygame

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


STAGE_W = t.VIEWPORT_WIDTH
STAGE_H = t.VIEWPORT_HEIGHT
SAFE = t.Space.SAFE_AREA


# ─── shared helpers ───────────────────────────────────────────────────


def _gradient_hairline(surf, rect, radius, *, c0=t.Color.ACCENT_ROSE,
                       c1=t.Color.ACCENT_CYAN, alpha=140, width=1):
    """The rose→cyan 1px hairline border that rims every overlay surface."""
    border = render.gradient_border((rect.w, rect.h), radius,
                                    width=width, angle_deg=135.0)
    border = border.copy()
    if (c0, c1) != (t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN):
        tint = render.linear_gradient((rect.w, rect.h), c0, c1, 135.0).convert_alpha()
        border.blit(tint, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
    border.set_alpha(alpha)
    surf.blit(border, rect.topleft)


def _round_panel(surf, rect, radius, *, fill_a=t.Color.SURFACE_ELEVATED,
                 fill_b=t.Color.SURFACE_BASE):
    """An elevated rounded panel body (gradient fill) drawn at rect."""
    body = render.rounded_gradient((rect.w, rect.h), fill_a, fill_b, radius, 160)
    surf.blit(body, rect.topleft)


def _inset_box(surf, rect, radius, *, fill=t.Color.BG_DEEP, fill_alpha=0.45,
               border=t.Color.BORDER_SOFT, border_alpha=1.0):
    """A sunken inner box (the rgba(15,11,31,.x) panels in the handoff)."""
    box = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
    pygame.draw.rect(box, t.with_alpha(fill, fill_alpha),
                     (0, 0, rect.w, rect.h), border_radius=radius)
    bc = border if border_alpha >= 1.0 else t.with_alpha(border, border_alpha)
    pygame.draw.rect(box, bc, (0, 0, rect.w, rect.h),
                     width=1, border_radius=radius)
    surf.blit(box, rect.topleft)


def _spinner_ring(surf, cx, cy, r, angle_deg, *, thickness=4):
    """A rose→cyan partial conic ring (RC banner spinner), rotated by angle."""
    start = angle_deg
    sweep = 290.0          # leaves a transparent gap, like the conic-gradient
    steps = 40
    for i in range(steps):
        frac = i / (steps - 1)
        a0 = math.radians(start + sweep * i / steps)
        a1 = math.radians(start + sweep * (i + 1) / steps)
        col = render._lerp(t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, frac)
        # fade the leading tail toward transparent for the comet look
        alpha = int(255 * min(1.0, frac * 1.6))
        ro, ri = r, r - thickness
        pts = [
            (cx + ro * math.cos(a0), cy + ro * math.sin(a0)),
            (cx + ro * math.cos(a1), cy + ro * math.sin(a1)),
            (cx + ri * math.cos(a1), cy + ri * math.sin(a1)),
            (cx + ri * math.cos(a0), cy + ri * math.sin(a0)),
        ]
        seg = pygame.Surface((surf.get_width(), surf.get_height()), pygame.SRCALPHA)
        pygame.draw.polygon(seg, (*col, alpha), pts)
        surf.blit(seg, (0, 0))


def _key_pill(text, *, color=t.Color.TEXT_SECONDARY, border=t.Color.TEXT_MUTED):
    """A small rectangular key-cap (SELECT / START) for combo hints."""
    style = t.TextStyle(t.JETBRAINS_MONO, 9, "Regular", 0.4)
    lbl = fonts.render(style, text, color)
    pw, ph = lbl.get_width() + 12, 18
    s = pygame.Surface((pw, ph), pygame.SRCALPHA)
    pygame.draw.rect(s, border, (0, 0, pw, ph), width=1, border_radius=4)
    s.blit(lbl, ((pw - lbl.get_width()) // 2, (ph - lbl.get_height()) // 2))
    return s


def _glyph_circle(ch, *, color=t.Color.TEXT_SECONDARY, border=t.Color.TEXT_MUTED,
                  d=18):
    """A round A/B/L/R button glyph (matches widgets._btn_glyph but sized)."""
    s = pygame.Surface((d, d), pygame.SRCALPHA)
    pygame.draw.circle(s, border, (d // 2, d // 2), d // 2, 1)
    style = t.TextStyle(t.SPACE_GROTESK_SEMIBOLD, max(9, d - 8), "SemiBold", 0.0)
    lbl = fonts.render(style, ch, color)
    s.blit(lbl, ((d - lbl.get_width()) // 2, (d - lbl.get_height()) // 2))
    return s


def _modal_keys(surf, y, hints):
    """Centered A/B key hint row used by the RB / BC modals.

    hints = [("A", "reboot"), ("B", "later")] — round glyph + muted label.
    """
    parts = []
    for g, lbl in hints:
        glyph = _glyph_circle(g)
        label = fonts.render(t.Type.CAPTION, lbl, t.Color.TEXT_MUTED)
        parts.append((glyph, label))
    inner_gap, outer_gap = 6, 18
    total = sum(g.get_width() + inner_gap + l.get_width() for g, l in parts)
    total += outer_gap * (len(parts) - 1)
    x = (STAGE_W - total) // 2
    for g, l in parts:
        surf.blit(g, (x, y - g.get_height() // 2))
        x += g.get_width() + inner_gap
        surf.blit(l, (x, y - l.get_height() // 2))
        x += l.get_width() + outer_gap


# ─── QM — Quick menu (Start dock) ─────────────────────────────────────


class QuickMenu:
    """QM — Start-summoned bottom dock. Brightness/volume sliders + 6 tiles.

    Not a centered modal: it docks to the bottom edge over a dimmed underlay,
    so the user keeps context of the screen behind it. Focus order is the two
    sliders (0,1) then the six tiles (2..7). L/R bumpers tweak a focused
    slider. START (or B) dismisses; A on the Exit tile leaves to the launcher.
    """
    name = "quick_menu"

    # focus indices for the 6-tile row
    _TILE_BASE = 2

    def __init__(self, app, underlay=None, *, brightness=68, volume=50):
        self.app = app
        self.underlay = underlay
        self.brightness = brightness
        self.volume = volume
        self.focus = 0  # 0 brightness · 1 volume · 2..7 tiles
        # toggle tiles: (icon, label, sub, state)  state: on/off/warn/danger
        self.tiles = [
            ("ph-bluetooth", "Audio", "AirPods", "on"),
            ("ph-wifi-high", "WiFi", "Home 5G", "on"),
            ("ph-broadcast", "Profile", "balanced", "warn"),
            ("ph-test-tube", "FX", "scanlines", "off"),
            ("ph-hourglass", "Sleep", "15 min", "off"),
            ("ph-x-circle", "Exit", "to launcher", "danger"),
        ]

    # ── input ─────────────────────────────────────────────────────────
    def on_action(self, action):
        if action in ("start", "b"):
            self.app.pop()
            return
        if action == "l":
            self._nudge(-5)
            return
        if action == "r":
            self._nudge(+5)
            return
        if action == "left":
            self._move(-1)
        elif action == "right":
            self._move(+1)
        elif action == "up":
            # sliders sit above the tile row; jump up into the slider band
            if self.focus >= self._TILE_BASE:
                self.focus = 0 if (self.focus - self._TILE_BASE) < 3 else 1
        elif action == "down":
            if self.focus < self._TILE_BASE:
                # drop into the tile row beneath the slider column
                self.focus = self._TILE_BASE + (0 if self.focus == 0 else 3)
        elif action == "a":
            self._activate()

    def _move(self, step):
        if self.focus < self._TILE_BASE:
            # within the 2-slider row
            self.focus = max(0, min(1, self.focus + step))
        else:
            i = self.focus - self._TILE_BASE
            i = max(0, min(len(self.tiles) - 1, i + step))
            self.focus = self._TILE_BASE + i

    def _nudge(self, delta):
        if self.focus == 0:
            self.brightness = max(0, min(100, self.brightness + delta))
        elif self.focus == 1:
            self.volume = max(0, min(100, self.volume + delta))
        else:
            # a focused tile + bumper: still adjust the nearest slider so the
            # bumpers always do something sensible from anywhere in the dock
            self.brightness = max(0, min(100, self.brightness + delta))

    def _activate(self):
        if self.focus < self._TILE_BASE:
            return  # sliders adjust via bumpers, A is a no-op on them
        icon, label, sub, state = self.tiles[self.focus - self._TILE_BASE]
        if state == "danger":           # Exit · to launcher
            self.app.request_exit()
        else:
            self.app.toast(f"{label} — coming soon")

    def update(self, dt_ms):
        pass

    # ── draw ──────────────────────────────────────────────────────────
    def draw(self, surf):
        w.draw_underlay(surf, self.underlay)
        # scrim (the dock dims the screen behind it; .55 in the handoff)
        w.scrim(surf, alpha=0.55)
        # mask the underlay's footer so it doesn't peek out below the dock
        surf.fill(t.Color.BG_DEEP, (0, 448, 640, t.Space.FOOTER_HINT_HEIGHT))

        pad = 14
        gap = 12
        panel = pygame.Rect(SAFE, 0, STAGE_W - SAFE * 2, 0)
        # measured section heights
        top_h = 56
        sliders_h = 56
        tiles_h = 70
        foot_h = 30
        panel.h = pad + top_h + gap + sliders_h + gap + tiles_h + gap + foot_h + pad
        panel.y = STAGE_H - SAFE - panel.h

        _round_panel(surf, panel, 16)
        _gradient_hairline(surf, panel, 16, alpha=150, width=2)

        y = panel.y + pad
        self._draw_top(surf, panel, y, top_h)
        y += top_h + gap
        self._draw_sliders(surf, panel, y, sliders_h, pad, gap)
        y += sliders_h + gap
        self._draw_tiles(surf, panel, y, tiles_h, pad)
        y += tiles_h + gap
        self._draw_foot(surf, panel, y, foot_h)

    # top strip: clock + date · now-playing pill · battery pill
    def _draw_top(self, surf, panel, y, h):
        x = panel.x + 14
        clock = fonts.render(t.Type.HERO, "14:32", t.Color.TEXT_PRIMARY)
        surf.blit(clock, (x, y))
        sec = fonts.render(t.Type.STATE, ":18", t.Color.TEXT_MUTED)
        surf.blit(sec, (x + clock.get_width() + 4, y + clock.get_height() - sec.get_height() - 4))
        date = fonts.render(t.Type.META_SM, "tue 20 may · day 142", t.Color.TEXT_MUTED)
        surf.blit(date, (x, y + clock.get_height() + 2))

        # battery pill (far right)
        bat_w = self._draw_now_battery(surf, panel, y, h)
        # now-playing pill (to the left of the battery pill)
        self._draw_now_playing(surf, panel, y, h, right_edge=panel.right - 14 - bat_w - 12)

    def _draw_now_battery(self, surf, panel, y, h):
        ic = icons.icon("ph-battery-high", 14, t.Color.SUCCESS)
        pct = fonts.render(t.Type.MONO_TECH, "87%", t.Color.SUCCESS)
        eta = fonts.render(t.Type.MONO_TECH, "· 2h 48m", t.Color.TEXT_MUTED)
        inner_gap = 6
        pw = 10 + ic.get_width() + inner_gap + pct.get_width() + inner_gap + eta.get_width() + 10
        ph = 28
        px = panel.right - 14 - pw
        py = y + (h - ph) // 2
        _inset_box(surf, pygame.Rect(px, py, pw, ph), 999, fill=t.Color.BG_DEEP,
                   fill_alpha=0.5)
        cx = px + 10
        cy = py + ph // 2
        surf.blit(ic, (cx, cy - ic.get_height() // 2)); cx += ic.get_width() + inner_gap
        surf.blit(pct, (cx, cy - pct.get_height() // 2)); cx += pct.get_width() + inner_gap
        surf.blit(eta, (cx, cy - eta.get_height() // 2))
        return pw

    def _draw_now_playing(self, surf, panel, y, h, *, right_edge):
        ph = 32
        pw = 178
        px = right_edge - pw
        py = y + (h - ph) // 2
        _inset_box(surf, pygame.Rect(px, py, pw, ph), 999, fill=t.Color.BG_DEEP,
                   fill_alpha=0.5)
        # gradient art square
        gd = render.rounded_gradient(
            (24, 24),
            render._lerp(t.Color.ACCENT_ROSE, t.Color.BG_MID, 0.6),
            render._lerp(t.Color.ACCENT_CYAN, t.Color.BG_MID, 0.6),
            6, 135)
        pygame.draw.rect(gd, t.Color.BORDER_SOFT, (0, 0, 24, 24), width=1, border_radius=6)
        gl = icons.icon("ph-game-controller", 14, t.Color.ACCENT_ROSE)
        gd.blit(gl, ((24 - 14) // 2, (24 - 14) // 2))
        gx = px + 8
        surf.blit(gd, (gx, py + (ph - 24) // 2))
        tx = gx + 24 + 8
        ttl = fonts.render(t.Type.BODY_SM, "Castlevania: SotN", t.Color.TEXT_PRIMARY)
        sub = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 9, "Regular", 0.0),
                           "PSX · session 42m", t.Color.TEXT_MUTED)
        # clip the title to the pill
        maxw = px + pw - 10 - tx
        if ttl.get_width() > maxw:
            ttl = ttl.subsurface((0, 0, max(1, maxw), ttl.get_height()))
        stack_h = ttl.get_height() + sub.get_height()
        sy = py + (ph - stack_h) // 2
        surf.blit(ttl, (tx, sy))
        surf.blit(sub, (tx, sy + ttl.get_height()))

    # 2-up sliders: brightness (focused by default) + volume
    def _draw_sliders(self, surf, panel, y, h, pad, gap):
        col_w = (panel.w - pad * 2 - gap) // 2
        specs = [
            (0, "ph-lightbulb", t.Color.ACCENT_PEACH, "Brightness", self.brightness),
            (1, "ph-speaker-simple-high", t.Color.TEXT_SECONDARY, "Volume", self.volume),
        ]
        for idx, icon_name, icon_col, label, value in specs:
            cx = panel.x + pad + (col_w + gap) * idx
            rect = pygame.Rect(cx, y, col_w, h)
            focused = (self.focus == idx)
            _inset_box(surf, rect, 10, fill=t.Color.BG_DEEP, fill_alpha=0.4)
            if focused:
                _gradient_hairline(surf, rect, 10, alpha=255, width=1)
            # top row: icon + label + pct
            ic = icons.icon(icon_name, 16, icon_col)
            surf.blit(ic, (rect.x + 12, rect.y + 10))
            lab = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.0),
                               label, t.Color.TEXT_SECONDARY)
            surf.blit(lab, (rect.x + 12 + 16 + 8, rect.y + 10 + (16 - lab.get_height()) // 2))
            pct = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.0),
                               f"{value}%", t.Color.ACCENT_CYAN)
            surf.blit(pct, (rect.right - 12 - pct.get_width(),
                            rect.y + 10 + (16 - pct.get_height()) // 2))
            # bar
            bx = rect.x + 12
            by = rect.y + 10 + 16 + 8
            bw = rect.w - 24
            w.slider(surf, bx, by, bw, value / 100.0)

    # 6-cell quick-toggle grid
    def _draw_tiles(self, surf, panel, y, h, pad):
        n = len(self.tiles)
        gap = 8
        cell_w = (panel.w - pad * 2 - gap * (n - 1)) // n
        for i, (icon_name, label, sub, state) in enumerate(self.tiles):
            cx = panel.x + pad + (cell_w + gap) * i
            rect = pygame.Rect(cx, y, cell_w, h)
            focused = (self.focus == self._TILE_BASE + i)
            self._draw_tile(surf, rect, icon_name, label, sub, state, focused)

    def _draw_tile(self, surf, rect, icon_name, label, sub, state, focused):
        # base / on / danger backgrounds
        if state == "on":
            body = render.rounded_gradient(
                (rect.w, rect.h),
                render._lerp(t.Color.ACCENT_CYAN, t.Color.BG_DEEP, 0.55),
                render._lerp(t.Color.SURFACE_GLASS, t.Color.BG_DEEP, 0.4),
                10, 160)
            pygame.draw.rect(body, t.with_alpha(t.Color.ACCENT_CYAN, 0.35),
                             (0, 0, rect.w, rect.h), width=1, border_radius=10)
            icon_col = t.Color.ACCENT_CYAN
            lbl_col = t.Color.TEXT_PRIMARY
        else:
            body = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
            pygame.draw.rect(body, t.with_alpha(t.Color.BG_DEEP, 0.45),
                             (0, 0, rect.w, rect.h), border_radius=10)
            pygame.draw.rect(body, t.Color.BORDER_SOFT,
                             (0, 0, rect.w, rect.h), width=1, border_radius=10)
            if state == "warn":
                icon_col = t.Color.ACCENT_PEACH
            elif state == "danger":
                icon_col = t.Color.ERROR
            else:
                icon_col = t.Color.TEXT_SECONDARY
            lbl_col = t.Color.ERROR if state == "danger" else t.Color.TEXT_SECONDARY

        ic = icons.icon(icon_name, 22, icon_col)
        body.blit(ic, ((rect.w - 22) // 2, 10))
        lab = fonts.render(t.TextStyle(t.INTER_MEDIUM, 11, "Medium", 0.0), label, lbl_col)
        body.blit(lab, ((rect.w - lab.get_width()) // 2, 10 + 22 + 4))
        subc = t.Color.ERROR if state == "danger" else t.Color.TEXT_MUTED
        sb = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 9, "Regular", 0.0), sub, subc)
        # clip sub-label so it never overruns the narrow cell
        if sb.get_width() > rect.w - 4:
            sb = sb.subsurface((0, 0, rect.w - 4, sb.get_height()))
        body.blit(sb, ((rect.w - sb.get_width()) // 2, 10 + 22 + 4 + lab.get_height() + 1))

        if focused:
            glow, gpos = render.focus_glow(rect, 10, spread=10)
            surf.blit(glow, gpos)
            surf.blit(body, rect.topleft)
            surf.blit(render.gradient_border((rect.w, rect.h), 10), rect.topleft)
        else:
            surf.blit(body, rect.topleft)

    # dashed-top footer with the four control hints
    def _draw_foot(self, surf, panel, y, h):
        # dashed divider
        w.dashed_hline(surf, panel.x + 4, panel.right - 4, y, t.Color.BORDER_SOFT)
        cy = y + 8 + (h - 8) // 2

        # build hint clusters as flat surfaces, then center them as a group
        clusters = []

        # A toggle / open
        clusters.append([
            ("s", _glyph_circle("A")),
            ("s", fonts.render(t.Type.CAPTION, "toggle / open", t.Color.TEXT_MUTED)),
        ])
        # L R slider
        clusters.append([
            ("s", _glyph_circle("L")),
            ("s", _glyph_circle("R")),
            ("s", fonts.render(t.Type.CAPTION, "slider", t.Color.TEXT_MUTED)),
        ])
        # SELECT + START dismiss
        clusters.append([
            ("s", _key_pill("SELECT")),
            ("s", fonts.render(t.Type.META_SM, "+", t.Color.TEXT_MUTED)),
            ("s", _key_pill("START")),
            ("s", fonts.render(t.Type.CAPTION, "dismiss", t.Color.TEXT_MUTED)),
        ])
        # A on Exit · back to games (rose accent)
        clusters.append([
            ("s", _glyph_circle("A", color=t.Color.ACCENT_ROSE, border=t.Color.ACCENT_ROSE)),
            ("s", fonts.render(t.Type.CAPTION, "on Exit · back to games", t.Color.ACCENT_ROSE)),
        ])

        inner_gap = 6
        cluster_surfs = []
        for items in clusters:
            cw = sum(s.get_width() for _, s in items) + inner_gap * (len(items) - 1)
            ch = max(s.get_height() for _, s in items)
            cs = pygame.Surface((cw, ch), pygame.SRCALPHA)
            cx = 0
            for _, s in items:
                cs.blit(s, (cx, (ch - s.get_height()) // 2))
                cx += s.get_width() + inner_gap
            cluster_surfs.append(cs)

        outer_gap = 16
        total = sum(c.get_width() for c in cluster_surfs) + outer_gap * (len(cluster_surfs) - 1)
        x = (STAGE_W - total) // 2
        for c in cluster_surfs:
            surf.blit(c, (x, cy - c.get_height() // 2))
            x += c.get_width() + outer_gap


# ─── RC — Reconnecting (top banner) ───────────────────────────────────


class Reconnecting:
    """RC — transient top banner shown after wake while a link is recovering.

    Non-blocking: the underlay stays fully visible (no scrim), only a pill
    banner drops in at the top. It spins until done or a 9s timeout, then
    pops itself. B cancels.
    """
    name = "reconnecting"

    TIMEOUT_MS = 9000

    def __init__(self, app, underlay=None, *, target="Home 5G",
                 stage="acquiring IP via DHCP", attempt=2, attempts=3):
        self.app = app
        self.underlay = underlay
        self.target = target
        self.stage = stage
        self.attempt = attempt
        self.attempts = attempts
        self.elapsed_ms = 2400.0   # matches the "2.4 s elapsed" copy
        self._spin_deg = 0.0
        self._done = False

    def on_action(self, action):
        if action in ("b", "start") and not self._done:
            self._done = True
            self.app.pop()

    def update(self, dt_ms):
        if self._done:
            return
        self.elapsed_ms += dt_ms
        self._spin_deg = (self._spin_deg + dt_ms * 0.33) % 360.0
        if self.elapsed_ms >= self.TIMEOUT_MS:
            # give up quietly; in production this swaps to a success/fail toast
            self._done = True
            self.app.pop()

    def draw(self, surf):
        w.draw_underlay(surf, self.underlay)

        bw = 500
        bh = 50
        rect = pygame.Rect((STAGE_W - bw) // 2, 56, bw, bh)
        _round_panel(surf, rect, 14)
        _gradient_hairline(surf, rect, 14, alpha=170, width=2)

        # spinner ring (left)
        sx = rect.x + 14 + 13
        sy = rect.centery
        _spinner_ring(surf, sx, sy, 13, self._spin_deg, thickness=4)

        # text block
        tx = rect.x + 14 + 26 + 12
        # title: "Reconnecting to <target>" with cyan target
        lead = fonts.render(t.Type.STATE, "Reconnecting to ", t.Color.TEXT_PRIMARY)
        tgt = fonts.render(t.Type.STATE, self.target, t.Color.ACCENT_CYAN)
        title_y = rect.y + 10
        surf.blit(lead, (tx, title_y))
        surf.blit(tgt, (tx + lead.get_width(), title_y))

        # sub: "resumed from sleep · <stage> · N.N s elapsed"
        secs = self.elapsed_ms / 1000.0
        sub_style = t.TextStyle(t.JETBRAINS_MONO, 10, "Regular", 0.0)
        s1 = fonts.render(sub_style, "resumed from sleep · ", t.Color.TEXT_MUTED)
        s2 = fonts.render(sub_style, self.stage, t.Color.ACCENT_ROSE)
        s3 = fonts.render(sub_style, f" · {secs:.1f} s elapsed", t.Color.TEXT_MUTED)
        sub_y = title_y + lead.get_height() + 2
        sxx = tx
        for seg in (s1, s2, s3):
            surf.blit(seg, (sxx, sub_y))
            sxx += seg.get_width()

        # right cluster: attempt dots + cancel chip
        chip = self._cancel_chip()
        chip_x = rect.right - 14 - chip.get_width()
        surf.blit(chip, (chip_x, rect.centery - chip.get_height() // 2))

        dots = self._attempt_dots()
        dots_x = chip_x - 8 - dots.get_width()
        surf.blit(dots, (dots_x, rect.centery - dots.get_height() // 2))

    def _attempt_dots(self):
        d, gap = 6, 4
        n = self.attempts
        s = pygame.Surface((d * n + gap * (n - 1), d), pygame.SRCALPHA)
        for i in range(n):
            cx = i * (d + gap) + d // 2
            if i < self.attempt - 1:
                col = t.Color.TEXT_SECONDARY        # done
            elif i == self.attempt - 1:
                col = t.Color.ACCENT_CYAN           # now
            else:
                col = t.Color.BORDER_SOFT           # pending
            pygame.draw.circle(s, col, (cx, d // 2), d // 2)
        return s

    def _cancel_chip(self):
        glyph = _glyph_circle("B", d=16)
        lbl = fonts.render(t.Type.CAPTION, "cancel", t.Color.TEXT_SECONDARY)
        inner = 6
        pw = 10 + glyph.get_width() + inner + lbl.get_width() + 10
        ph = 26
        s = pygame.Surface((pw, ph), pygame.SRCALPHA)
        pygame.draw.rect(s, t.with_alpha(t.Color.BG_DEEP, 0.55), (0, 0, pw, ph),
                         border_radius=999)
        pygame.draw.rect(s, t.Color.BORDER_SOFT, (0, 0, pw, ph),
                         width=1, border_radius=999)
        cx = 10
        s.blit(glyph, (cx, (ph - glyph.get_height()) // 2)); cx += glyph.get_width() + inner
        s.blit(lbl, (cx, (ph - lbl.get_height()) // 2))
        return s


# ─── RB — Reboot required (centered modal + timer) ────────────────────


class RebootConfirm:
    """RB — post-install confirm with a 15s auto-reboot countdown.

    Centered modal over a heavily-dimmed underlay. A list shows exactly what
    the reboot activates. Focus defaults to "Reboot now" (positive primary);
    "Later" / B postpones. If the countdown hits zero it auto-confirms.
    """
    name = "reboot_confirm"

    def __init__(self, app, underlay=None, *, on_reboot=None, on_later=None,
                 seconds=15):
        self.app = app
        self.underlay = underlay
        self.on_reboot = on_reboot
        self.on_later = on_later
        self.total_ms = seconds * 1000
        self.remaining_ms = float(self.total_ms)
        self.focus = 1  # 0 Later · 1 Reboot now
        self._glyph_deg = 0.0
        self._done = False
        self.changes = [
            "btusb.ko replaced · CSR8510 dongles supported",
            "autosuspend off · audio lag down 40 ms",
            "bluez 5.66 installed · A2DP-only by default",
        ]

    def on_action(self, action):
        if action in ("left", "right"):
            self.focus ^= 1
        elif action == "b":
            self._later()
        elif action == "y":
            self._later()
        elif action == "a":
            if self.focus == 1:
                self._reboot()
            else:
                self._later()

    def _reboot(self):
        if self._done:
            return
        self._done = True
        self.app.pop()
        if self.on_reboot:
            self.on_reboot()
        else:
            self.app.toast("Restarting…")

    def _later(self):
        if self._done:
            return
        self._done = True
        self.app.pop()
        if self.on_later:
            self.on_later()

    def update(self, dt_ms):
        if self._done:
            return
        self.remaining_ms -= dt_ms
        self._glyph_deg = (self._glyph_deg + dt_ms * 0.06) % 360.0  # slow 6s spin
        if self.remaining_ms <= 0:
            self.remaining_ms = 0
            self._reboot()

    def draw(self, surf):
        w.draw_underlay(surf, self.underlay)
        w.scrim(surf, alpha=0.62)

        cw = 420
        rect = pygame.Rect((STAGE_W - cw) // 2, 0, cw, 0)
        pad = 24
        # measure content height
        head_h = 44
        body_lines = w.wrap_chars("The Bluetooth fix is installed but won't switch "
                                  "on until you restart. Your game progress is saved.", 52)
        body_h = len(body_lines) * 22
        changes_h = 10 + len(self.changes) * 18 + 10
        ctr_h = 22
        btns_h = 44
        keys_h = 22
        rect.h = (pad + head_h + 12 + body_h + 12 + changes_h + 12 + ctr_h
                  + 14 + btns_h + 10 + keys_h + 18)
        rect.y = (STAGE_H - rect.h) // 2

        _round_panel(surf, rect, 14)
        _gradient_hairline(surf, rect, 14, alpha=160, width=2)

        y = rect.y + pad
        # head: rotating glyph + title
        gb = pygame.Rect(rect.x + pad, y, 44, 44)
        glyph_bg = render.rounded_gradient(
            (44, 44),
            render._lerp(t.Color.ACCENT_ROSE, t.Color.BG_DEEP, 0.6),
            render._lerp(t.Color.ACCENT_CYAN, t.Color.BG_DEEP, 0.6),
            12, 135)
        pygame.draw.rect(glyph_bg, t.with_alpha(t.Color.ACCENT_CYAN, 0.4),
                         (0, 0, 44, 44), width=1, border_radius=12)
        surf.blit(glyph_bg, gb.topleft)
        gly = icons.icon("ph-arrow-clockwise", 24, t.Color.ACCENT_CYAN)
        gly = pygame.transform.rotozoom(gly, -self._glyph_deg, 1.0)
        surf.blit(gly, (gb.centerx - gly.get_width() // 2,
                        gb.centery - gly.get_height() // 2))
        title = fonts.render(t.Type.TITLE, "Restart to finish setup", t.Color.TEXT_PRIMARY)
        surf.blit(title, (gb.right + 12, gb.centery - title.get_height() // 2))
        y += head_h + 12

        # body copy
        for line in body_lines:
            ls = fonts.render(t.Type.BODY, line, t.Color.TEXT_SECONDARY)
            surf.blit(ls, (rect.x + pad, y))
            y += 22
        y += 12 - 0

        # changes box
        cbox = pygame.Rect(rect.x + pad, y, rect.w - pad * 2, changes_h)
        _inset_box(surf, cbox, 8, fill=t.Color.BG_DEEP, fill_alpha=0.55)
        cy = cbox.y + 10
        for change in self.changes:
            ic = icons.icon("ph-check", 12, t.Color.SUCCESS)
            surf.blit(ic, (cbox.x + 12, cy + 1))
            cs = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 11, "Regular", 0.0),
                              change, t.Color.TEXT_SECONDARY)
            maxw = cbox.right - 12 - (cbox.x + 12 + 12 + 10)
            if cs.get_width() > maxw:
                cs = cs.subsurface((0, 0, max(1, maxw), cs.get_height()))
            surf.blit(cs, (cbox.x + 12 + 12 + 10, cy))
            cy += 18
        y += changes_h + 12

        # countdown strip: ring + "auto-restart in Ns" + right hint
        self._draw_counter(surf, rect, y, pad)
        y += ctr_h + 14

        # buttons
        gap = 10
        # grid-template-columns: 1fr 1.3fr → Later narrower, Reboot wider
        total_units = 1.0 + 1.3
        avail = rect.w - pad * 2 - gap
        lw = int(avail * (1.0 / total_units))
        rw = avail - lw
        later_rect = pygame.Rect(rect.x + pad, y, lw, btns_h)
        reboot_rect = pygame.Rect(later_rect.right + gap, y, rw, btns_h)
        w.button(surf, later_rect, "Later", focused=(self.focus == 0))
        w.button(surf, reboot_rect, "Restart now", icon_name="ph-arrow-clockwise",
                 focused=(self.focus == 1))
        y += btns_h + 10

        # key hints
        _modal_keys(surf, y + keys_h // 2, [("A", "restart"), ("B", "later")])

    def _draw_counter(self, surf, rect, y, pad):
        frac = self.remaining_ms / self.total_ms if self.total_ms else 0
        secs = int(math.ceil(self.remaining_ms / 1000.0))
        # cyan ring (12px) showing remaining fraction
        cx = rect.x + pad + 6
        cy = y + 6
        _mini_ring(surf, cx, cy, 6, frac, color=t.Color.ACCENT_CYAN, thickness=3)
        # "auto-restart in N s"
        tx = rect.x + pad + 12 + 10
        a = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 11, "Regular", 0.0),
                         "auto-restart in ", t.Color.TEXT_MUTED)
        b = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 11, "Regular", 0.0),
                         f"{secs} s", t.Color.ACCENT_CYAN)
        surf.blit(a, (tx, y))
        surf.blit(b, (tx + a.get_width(), y))
        # right hint
        r = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 11, "Regular", 0.0),
                         "B to postpone · Y for later", t.Color.TEXT_MUTED)
        surf.blit(r, (rect.right - pad - r.get_width(), y))


# ─── BC — Battery critical (centered peach/error modal) ───────────────


class BatteryCritical:
    """BC — system-wide critical-battery alert (5%).

    Centered modal with an error-tinted gradient rim, a big pulsing battery
    glyph, a 3-column stat strip, and a primary "Enter battery saver" CTA.
    A = saver, B = dismiss.
    """
    name = "battery_critical"

    def __init__(self, app, underlay=None, *, percent=5,
                 on_saver=None, minutes_left="~12 min", profile="balanced"):
        self.app = app
        self.underlay = underlay
        self.percent = percent
        self.minutes_left = minutes_left
        self.profile = profile
        self.on_saver = on_saver
        self.focus = 1  # 0 Dismiss · 1 Enter battery saver
        self._pulse_ms = 0.0

    def on_action(self, action):
        if action in ("left", "right"):
            self.focus ^= 1
        elif action == "b":
            self.app.pop()
        elif action == "a":
            if self.focus == 1:
                self.app.pop()
                if self.on_saver:
                    self.on_saver()
                else:
                    self.app.toast("Battery saver on")
            else:
                self.app.pop()

    def update(self, dt_ms):
        self._pulse_ms = (self._pulse_ms + dt_ms) % 1600.0

    def draw(self, surf):
        w.draw_underlay(surf, self.underlay)
        w.scrim(surf, alpha=0.60)

        cw = 460
        pad = 24
        rect = pygame.Rect((STAGE_W - cw) // 2, 0, cw, 0)
        icon_h = 88
        title_h = 30
        copy_lines = w.wrap_chars("5% remaining. Plug into a charger now to keep "
                                  "playing. We'll pause the game at 2% to protect "
                                  "your save.", 50)
        copy_h = len(copy_lines) * 20
        strip_h = 58
        btns_h = 44
        keys_h = 22
        rect.h = (pad + icon_h + 8 + title_h + 8 + copy_h + 14 + strip_h
                  + 14 + btns_h + 10 + keys_h + 18)
        rect.y = (STAGE_H - rect.h) // 2

        _round_panel(surf, rect, 16)
        # error→peach rim
        _gradient_hairline(surf, rect, 16, c0=t.Color.ERROR, c1=t.Color.ACCENT_PEACH,
                           alpha=180, width=2)

        y = rect.y + pad
        # big battery glyph (pulsing)
        self._draw_big_battery(surf, rect.centerx, y + icon_h // 2)
        y += icon_h + 8

        # title (error color, centered)
        title = fonts.render(t.Type.HERO, "Battery is critical", t.Color.ERROR)
        surf.blit(title, (rect.centerx - title.get_width() // 2, y))
        y += title_h + 8

        # copy (centered, secondary)
        for line in copy_lines:
            ls = fonts.render(t.Type.STATE, line, t.Color.TEXT_SECONDARY)
            surf.blit(ls, (rect.centerx - ls.get_width() // 2, y))
            y += 20
        y += 14

        # 3-col eta strip
        strip = pygame.Rect(rect.x + pad, y, rect.w - pad * 2, strip_h)
        _inset_box(surf, strip, 10, fill=t.Color.BG_DEEP, fill_alpha=0.55)
        cols = [
            (f"{self.percent}%", "level", t.Color.ERROR),
            (self.minutes_left, "until auto-pause", t.Color.WARNING),
            (self.profile, "profile", t.Color.SUCCESS),
        ]
        col_w = strip.w // 3
        for i, (val, key, vcol) in enumerate(cols):
            cx = strip.x + col_w * i + col_w // 2
            v = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 16, "Bold", -0.2),
                             val, vcol)
            surf.blit(v, (cx - v.get_width() // 2, strip.y + 10))
            k = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 9, "Regular", 0.5),
                             key, t.Color.TEXT_MUTED, upper=True)
            surf.blit(k, (cx - k.get_width() // 2, strip.y + 10 + v.get_height() + 4))
        y += strip_h + 14

        # buttons: 1fr 1.4fr → Dismiss narrower, saver wider
        gap = 10
        total_units = 1.0 + 1.4
        avail = rect.w - pad * 2 - gap
        lw = int(avail * (1.0 / total_units))
        rw = avail - lw
        dismiss_rect = pygame.Rect(rect.x + pad, y, lw, btns_h)
        saver_rect = pygame.Rect(dismiss_rect.right + gap, y, rw, btns_h)
        w.button(surf, dismiss_rect, "Dismiss", focused=(self.focus == 0))
        w.button(surf, saver_rect, "Battery saver", icon_name="ph-battery-low",
                 focused=(self.focus == 1))
        y += btns_h + 10

        _modal_keys(surf, y + keys_h // 2, [("A", "saver mode"), ("B", "dismiss")])

    def _draw_big_battery(self, surf, cx, cy):
        # pulse 1.0 → 1.02 scale, 0.85 → 1.0 alpha
        phase = (math.sin(self._pulse_ms / 1600.0 * 2 * math.pi - math.pi / 2) + 1) / 2
        scale = 1.0 + 0.02 * phase
        # radial error halo behind
        halo_r = int(44 * scale)
        halo = pygame.Surface((halo_r * 2, halo_r * 2), pygame.SRCALPHA)
        for i in range(halo_r, 0, -2):
            a = int(40 * (i / halo_r) ** 2 * (0.6 + 0.4 * phase))
            pygame.draw.circle(halo, t.with_alpha(t.Color.ERROR, a / 255.0),
                               (halo_r, halo_r), i)
        surf.blit(halo, (cx - halo_r, cy - halo_r))

        # battery shell (70×36 base) scaled
        bw, bh = int(70 * scale), int(36 * scale)
        shell = pygame.Surface((bw + 8, bh), pygame.SRCALPHA)
        pygame.draw.rect(shell, t.Color.ERROR, (0, 0, bw, bh),
                         width=3, border_radius=5)
        # nub
        pygame.draw.rect(shell, t.Color.ERROR,
                         (bw, bh // 2 - 7, 4, 14), border_radius=2)
        # fill (5%)
        fill_w = max(3, int((bw - 8) * self.percent / 100.0))
        pygame.draw.rect(shell, t.Color.ERROR, (4, 4, fill_w, bh - 8), border_radius=2)
        # percent label centered
        pct = fonts.render(t.TextStyle(t.JETBRAINS_MONO, 12, "Regular", -0.3),
                           f"{self.percent}%", t.Color.TEXT_PRIMARY)
        shell.blit(pct, ((bw - pct.get_width()) // 2, (bh - pct.get_height()) // 2))
        surf.blit(shell, (cx - (bw + 8) // 2 + 2, cy - bh // 2))


# ─── small drawing utilities ──────────────────────────────────────────


def _mini_ring(surf, cx, cy, r, frac, *, color, thickness=3):
    """A tiny progress ring (countdown). frac 1→0 sweeps clockwise from top."""
    pygame.draw.circle(surf, t.Color.BORDER_SOFT, (cx, cy), r, thickness)
    frac = max(0.0, min(1.0, frac))
    if frac <= 0:
        return
    start = -90.0
    sweep = 360.0 * frac
    steps = max(2, int(sweep / 6))
    for i in range(steps):
        a0 = math.radians(start + sweep * i / steps)
        a1 = math.radians(start + sweep * (i + 1) / steps)
        ro, ri = r, r - thickness
        pts = [
            (cx + ro * math.cos(a0), cy + ro * math.sin(a0)),
            (cx + ro * math.cos(a1), cy + ro * math.sin(a1)),
            (cx + ri * math.cos(a1), cy + ri * math.sin(a1)),
            (cx + ri * math.cos(a0), cy + ri * math.sin(a0)),
        ]
        pygame.draw.polygon(surf, color, pts)
