"""
Bluetooth remaining states — S2b / S3b / S3c / S6 / S6b.

Recreates the Claude Design handoff (final-GHVscC) Bluetooth edge-states in
pygame, matching the H1 console grammar (status bar + hero/empty + footer)
and the consumer-language sweep already applied across the BT module.

Screens (class ⇄ handoff):
  S2bNoAdapter ⇄ S2b-bluetooth-off.html  no built-in BT → USB dongle help
  S3bPasskey   ⇄ S3b-pair-pin.html       6-digit passkey confirm (Match/Reject)
  S3cEmptyScan ⇄ S3c-pair-empty.html     scan found 0 devices + tips checklist
  S6Setup      ⇄ S6-setup.html           "Set up Bluetooth audio" step wizard
  S6bSetupFailed ⇄ S6b-setup-failed.html a setup step failed → retry/skip/abort

IMPORTANT — consumer language only. S6/S6b never mention CSR / btusb /
vermagic / BlueZ / /boot/Image. Substeps read "Install audio engine /
Set up adapter compatibility / Tune audio connection / Connect audio
routing". Engineering vocabulary lives only in the Engineering screens.

The owner's note: the R36S has NO built-in Bluetooth, so S2b is reframed
as a no-adapter state (mirror of wifi.py's W9NoAdapter), not an "adapter
off" toggle.

Every class shares the screen shape: name, __init__(self, app, ...),
on_action(self, action), update(self, dt_ms), draw(self, surf).
Actions are strings: up/down/left/right/a/b/x/y/l/r.

This module only defines screens + local helpers; it never edits main.py
or widgets.py. Helpers not in the shared kit (passkey digit cells, the
step row, tip checklist row, the no-adapter dongle chips) live here at
module level.
"""

from __future__ import annotations

import pygame

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

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

# ── local text styles for sizes the shared Type table doesn't carry ──
# (handoff uses 22px empty/setup heroes and an 18px passkey-card title;
#  Type.HERO is 28 and Type.TITLE is 20 — these stay faithful to the CSS.)
_HERO_22 = t.TextStyle(t.SPACE_GROTESK_BOLD, 22, "Bold", -0.5)
_TITLE_18 = t.TextStyle(t.SPACE_GROTESK_SEMIBOLD, 18, "SemiBold", -0.3)
_DIGIT = t.TextStyle(t.JETBRAINS_MONO, 28, "Regular", 0.0)
_PCT = t.TextStyle(t.JETBRAINS_MONO, 13, "Regular", 0.0)


# ─── module-level helpers ─────────────────────────────────────────────


def _step_chip(current: int, total: int, *, halted: bool = False) -> pygame.Surface:
    """
    Pill: "step N of M" with the N tinted cyan (or error + "· halted" when
    the wizard is paused). Matches .step-chip in the handoff.
    """
    bg = t.with_alpha(t.Color.SURFACE_GLASS, 0.6)
    border = t.Color.BORDER_SOFT if not halted else t.with_alpha(t.Color.ERROR, 0.5)
    num_col = t.Color.ACCENT_CYAN if not halted else t.Color.ERROR

    pre = fonts.render(t.Type.META_SM, "step ", t.Color.TEXT_SECONDARY)
    num = fonts.render(t.Type.META_SM, str(current), num_col)
    tail = fonts.render(t.Type.META_SM,
                        f" of {total}" + (" · halted" if halted else ""),
                        t.Color.TEXT_SECONDARY)
    pad_x, pad_y = 10, 3
    inner_w = pre.get_width() + num.get_width() + tail.get_width()
    inner_h = max(pre.get_height(), num.get_height(), tail.get_height())
    w_px, h_px = inner_w + pad_x * 2, inner_h + pad_y * 2
    s = pygame.Surface((w_px, h_px), pygame.SRCALPHA)
    pygame.draw.rect(s, bg, (0, 0, w_px, h_px), border_radius=t.Radius.PILL)
    pygame.draw.rect(s, border, (0, 0, w_px, h_px), width=1,
                     border_radius=t.Radius.PILL)
    x = pad_x
    cy = h_px // 2
    for g in (pre, num, tail):
        s.blit(g, (x, cy - g.get_height() // 2))
        x += g.get_width()
    return s


def _setup_hero(surf, y, title, sub) -> int:
    """Big setup title + secondary paragraph. Returns y below the block."""
    title_s = fonts.render(_HERO_22, title, t.Color.TEXT_PRIMARY)
    surf.blit(title_s, (SAFE, y))
    sy = y + title_s.get_height() + 4
    sub_s = fonts.render(t.Type.BODY, sub, t.Color.TEXT_SECONDARY)
    # clip to safe width if needed
    maxw = STAGE_W - SAFE * 2
    if sub_s.get_width() > maxw:
        sub_s = sub_s.subsurface((0, 0, maxw, sub_s.get_height()))
    surf.blit(sub_s, (SAFE, sy))
    return sy + sub_s.get_height()


def _spinner_ring(surf, cx, cy, r, frac, *, thickness=3, sweep_deg=270.0):
    """
    A rose→cyan arc of `sweep_deg` whose start angle = frac*360 (so it spins
    as frac advances). Track behind, then color-interpolated polygon segments.
    """
    import math
    pygame.draw.circle(surf, t.Color.BORDER_SOFT, (cx, cy), r, thickness)
    start = -90.0 + frac * 360.0
    steps = max(2, int(sweep_deg / 6))
    ro, ri = r + thickness / 2, r - thickness / 2
    for i in range(steps):
        a0 = math.radians(start + sweep_deg * i / steps)
        a1 = math.radians(start + sweep_deg * (i + 1) / steps)
        col = render._lerp(t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, i / steps)
        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, col, pts)


def _step_row(surf, rect, *, status, title, sub, pct, pct_color,
              ring_frac=0.0):
    """
    A setup step row (matches .step). status drives the marker + framing:
      "done"        → mint check in tinted disc, title dimmed
      "in_progress" → spinning rose→cyan ring, bright border + tinted bg,
                      cyan subline; `ring_frac` animates the sweep
      "pending"     → muted hollow circle, dimmed title
      "error"       → error warning-circle in tinted disc, error subline
    `pct`/`pct_color` is the right-aligned status word (done/45%/queued/err).
    """
    radius = 10
    if status == "in_progress":
        body = render.rounded_gradient(
            (rect.w, rect.h), t.with_alpha(t.Color.BORDER_BRIGHT, 0.30),
            t.with_alpha(t.Color.BG_MID, 0.6), radius, 160)
        pygame.draw.rect(body, t.Color.BORDER_BRIGHT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=radius)
    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 = 14
    box = 26
    mcx, mcy = pad + box // 2, rect.h // 2

    # right-aligned status word, measured first to bound the label
    pct_s = fonts.render(_PCT, pct, pct_color)
    pct_x = rect.w - pad - pct_s.get_width()
    body.blit(pct_s, (pct_x, (rect.h - pct_s.get_height()) // 2))

    # marker
    if status == "done":
        pygame.draw.circle(body, t.with_alpha(t.Color.SUCCESS, 0.14),
                           (mcx, mcy), box // 2)
        ck = icons.icon("ph-check", 18, t.Color.SUCCESS)
        body.blit(ck, (mcx - 9, mcy - 9))
    elif status == "error":
        pygame.draw.circle(body, t.with_alpha(t.Color.ERROR, 0.16),
                           (mcx, mcy), box // 2)
        wc = icons.icon("ph-warning-circle", 18, t.Color.ERROR)
        body.blit(wc, (mcx - 9, mcy - 9))
    elif status == "in_progress":
        # spinner ring: a rose→cyan ~270° arc whose start angle rotates with
        # ring_frac (matches the handoff's `animation: spin`). Drawn as short
        # polygon segments so it can color-interpolate along the sweep.
        _spinner_ring(body, mcx, mcy, box // 2 - 3, ring_frac, thickness=3)
    else:  # pending
        pygame.draw.circle(body, t.with_alpha(t.Color.TEXT_MUTED, 0.16),
                           (mcx, mcy), box // 2)
        ci = icons.icon("ph-circle", 18, t.Color.TEXT_MUTED)
        body.blit(ci, (mcx - 9, mcy - 9))

    # label stack (title + mono subline)
    tx = pad + box + 14
    title_col = (t.Color.TEXT_PRIMARY if status == "in_progress"
                 else t.Color.TEXT_SECONDARY)
    title_s = fonts.render(t.Type.BODY_STRONG, title, title_col)
    sub_col = (t.Color.ACCENT_CYAN if status == "in_progress" else
               t.Color.ERROR if status == "error" else t.Color.TEXT_MUTED)
    sub_s = fonts.render(t.Type.META_SM, sub, sub_col)
    # clip label to avoid overrunning the pct column
    maxw = pct_x - tx - 10
    if title_s.get_width() > maxw:
        title_s = title_s.subsurface((0, 0, max(maxw, 1), title_s.get_height()))
    if sub_s.get_width() > maxw:
        sub_s = sub_s.subsurface((0, 0, max(maxw, 1), sub_s.get_height()))
    stack_h = title_s.get_height() + 2 + sub_s.get_height()
    sy = (rect.h - stack_h) // 2
    body.blit(title_s, (tx, sy))
    body.blit(sub_s, (tx, sy + title_s.get_height() + 2))

    surf.blit(body, rect.topleft)


def _setup_footer_bar(surf, y, frac, now_text, tail_text, *, error=False):
    """Progress bar + meta line (now / 'N of M · P%'). Matches .setup-footer."""
    track = render.rounded_fill((STAGE_W - SAFE * 2, 6), t.Color.BORDER_SOFT, 999)
    surf.blit(track, (SAFE, y))
    fw = max(int((STAGE_W - SAFE * 2) * frac), 6)
    if error:
        fill = render.rounded_fill((fw, 6), t.Color.ERROR, 999)
    else:
        fill = render.rounded_gradient((fw, 6), t.Color.ACCENT_ROSE,
                                       t.Color.ACCENT_CYAN, 999, 90)
    surf.blit(fill, (SAFE, y))

    my = y + 6 + 6
    now_col = t.Color.ERROR if error else t.Color.ACCENT_CYAN
    now_s = fonts.render(t.Type.META_SM, now_text, now_col)
    surf.blit(now_s, (SAFE, my))
    tail_s = fonts.render(t.Type.META_SM, tail_text, t.Color.TEXT_MUTED)
    surf.blit(tail_s, (STAGE_W - SAFE - tail_s.get_width(), my))


# ─── S2b — no Bluetooth adapter (mirror of W9NoAdapter) ───────────────


class S2bNoAdapter:
    """
    S2b ⇄ S2b-bluetooth-off.html, reframed per the owner: the R36S has no
    built-in Bluetooth, so this is a no-adapter state (mirror of wifi's
    W9NoAdapter). Big slashed BT glyph + plain head/sub + mint reassurance
    pill + known-good USB dongle chips. No MAC, no rfkill, no driver names
    beyond the chip family.
    """
    name = "bt-no-adapter"

    def __init__(self, app):
        self.app = app
        self.focus = 0

    def on_action(self, action):
        if action == "a":
            self.app.toast("Compatibility list — coming soon")
        elif action == "b":
            self.app.pop()

    def update(self, dt_ms):
        pass

    def draw(self, surf):
        w.status_bar(surf, title="Bluetooth", back=True)
        na = w.chip("no adapter", t.Color.TEXT_MUTED)
        surf.blit(na, (STAGE_W - SAFE - 150 - na.get_width(), 18))

        cx = STAGE_W // 2
        # big slashed glyph card 104×104
        gy = 96
        card = render.rounded_gradient((104, 104), t.Color.SURFACE_BASE,
                                       t.Color.BG_MID, 26, 160)
        pygame.draw.rect(card, t.Color.BORDER_SOFT, (0, 0, 104, 104), width=1,
                         border_radius=26)
        card.blit(icons.icon("ph-bluetooth", 60, t.Color.TEXT_DISABLED),
                  ((104 - 60) // 2, (104 - 60) // 2))
        # diagonal slash (the "no" stroke)
        pygame.draw.line(card, t.with_alpha(t.Color.TEXT_MUTED, 0.55),
                         (18, 78), (86, 30), 2)
        card.set_alpha(int(255 * 0.85))
        surf.blit(card, (cx - 52, gy))

        head = fonts.render(t.Type.HERO, "No Bluetooth adapter",
                            t.Color.TEXT_PRIMARY)
        surf.blit(head, (cx - head.get_width() // 2, gy + 104 + 14))

        # sub: two lines, dongle phrase bold
        sy = gy + 104 + 14 + head.get_height() + 8
        l1 = fonts.render(t.Type.BODY, "R36S doesn't have built-in Bluetooth.",
                          t.Color.TEXT_SECONDARY)
        surf.blit(l1, (cx - l1.get_width() // 2, sy))
        sy += l1.get_height() + 2
        l2a = fonts.render(t.Type.BODY_STRONG, "Plug a USB Bluetooth dongle",
                           t.Color.TEXT_PRIMARY)
        l2b = fonts.render(t.Type.BODY, " into the OTG port to add wireless audio.",
                           t.Color.TEXT_SECONDARY)
        l2w = l2a.get_width() + l2b.get_width()
        # if the combined line is too wide, drop the tail to a third line
        if l2w <= STAGE_W - SAFE * 2:
            surf.blit(l2a, (cx - l2w // 2, sy))
            surf.blit(l2b, (cx - l2w // 2 + l2a.get_width(), sy))
            sy += l2a.get_height() + 12
        else:
            surf.blit(l2a, (cx - l2a.get_width() // 2, sy))
            sy += l2a.get_height() + 2
            l2b2 = fonts.render(t.Type.BODY, "into the OTG port to add wireless audio.",
                                t.Color.TEXT_SECONDARY)
            surf.blit(l2b2, (cx - l2b2.get_width() // 2, sy))
            sy += l2b2.get_height() + 12

        # mint reassurance pill
        rea_ic = icons.icon("ph-game-controller", 13, t.Color.SUCCESS)
        rea_txt = fonts.render(t.Type.BODY_SM,
                               "Built-in speaker works fine — a dongle just adds wireless.",
                               t.Color.SUCCESS)
        pill_w = 14 + rea_ic.get_width() + 8 + rea_txt.get_width() + 14
        pill_h = 28
        pill = pygame.Surface((pill_w, pill_h), pygame.SRCALPHA)
        pygame.draw.rect(pill, t.with_alpha(t.Color.SUCCESS, 0.08),
                         (0, 0, pill_w, pill_h), border_radius=999)
        pygame.draw.rect(pill, t.with_alpha(t.Color.SUCCESS, 0.30),
                         (0, 0, pill_w, pill_h), width=1, border_radius=999)
        pill.blit(rea_ic, (14, (pill_h - rea_ic.get_height()) // 2))
        pill.blit(rea_txt, (14 + rea_ic.get_width() + 8,
                            (pill_h - rea_txt.get_height()) // 2))
        surf.blit(pill, (cx - pill_w // 2, sy))
        sy += pill_h + 12

        # known-good dongle chips (chip families + "more")
        chips = [("CSR8510", "works"), ("RTL8761B", "works"),
                 ("BCM20702", "works"), ("+ more in compatibility list", None)]
        chip_surfs = []
        for name, ok in chips:
            nlab = fonts.render(t.Type.BODY_SM, name, t.Color.TEXT_SECONDARY)
            ok_lab = (fonts.render(t.Type.META_SM, f"· {ok}", t.Color.SUCCESS)
                      if ok else None)
            cw = 12 + nlab.get_width() + (6 + ok_lab.get_width() if ok_lab else 0) + 12
            ch = 24
            cs = pygame.Surface((cw, ch), pygame.SRCALPHA)
            pygame.draw.rect(cs, t.with_alpha(t.Color.BG_DEEP, 0.4), (0, 0, cw, ch),
                             border_radius=999)
            pygame.draw.rect(cs, t.Color.BORDER_SOFT, (0, 0, cw, ch), width=1,
                             border_radius=999)
            cs.blit(nlab, (12, (ch - nlab.get_height()) // 2))
            if ok_lab:
                cs.blit(ok_lab, (12 + nlab.get_width() + 6,
                                 (ch - ok_lab.get_height()) // 2))
            chip_surfs.append(cs)

        # lay out chips centered, wrapping to two rows
        gap = 6
        max_w = STAGE_W - SAFE * 2
        rows, cur, cur_w = [], [], 0
        for cs in chip_surfs:
            if cur and cur_w + cs.get_width() + gap > max_w:
                rows.append((cur, cur_w))
                cur, cur_w = [], 0
            cur.append(cs)
            cur_w += cs.get_width() + gap
        if cur:
            rows.append((cur, cur_w))
        ry = sy
        for row_chips, row_w in rows:
            rx = cx - (row_w - gap) // 2
            for cs in row_chips:
                surf.blit(cs, (rx, ry))
                rx += cs.get_width() + gap
            ry += 24 + gap

        w.footer_hints(surf, [("A", "compatibility list"), ("B", "back")])


# ─── S3b — confirm passkey during pairing ─────────────────────────────


class S3bPasskey:
    """
    S3b ⇄ S3b-pair-pin.html. Shown when the pairing agent asks the user to
    confirm a 6-digit code matches on both devices. A faint ghost list keeps
    the user oriented (still mid-pair); a gradient-bordered card holds the
    code + a countdown. Focus defaults to "Match" (positive). A confirms,
    B rejects/cancels.
    """
    name = "bt-passkey"

    EXPIRE_S = 28

    def __init__(self, app, device="BT-MUSIC", passkey="947521"):
        self.app = app
        self.device = device
        self.passkey = passkey
        self.elapsed = 0.0
        self.focus = 1  # 0 = Reject, 1 = Match

    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()
                self.app.toast(f"Pairing {self.device}…")
            else:
                self.app.pop()
                self.app.toast("Pairing rejected")

    def update(self, dt_ms):
        self.elapsed += dt_ms

    def draw(self, surf):
        w.status_bar(surf, title="Pair new device", back=True)

        # ghost list under the card (we're still in the pair flow)
        gy = 60
        for _ in range(4):
            ghost = render.rounded_gradient((STAGE_W - SAFE * 2, 64),
                                            t.Color.SURFACE_BASE, t.Color.BG_MID,
                                            t.Radius.BUTTON, 160)
            pygame.draw.rect(ghost, t.Color.BORDER_SOFT,
                             (0, 0, STAGE_W - SAFE * 2, 64), width=1,
                             border_radius=t.Radius.BUTTON)
            ghost.set_alpha(int(255 * 0.25))
            surf.blit(ghost, (SAFE, gy))
            gy += 64 + 8

        # passkey card (460 wide, centered)
        cw, ch = 460, 232
        rect = pygame.Rect((STAGE_W - cw) // 2, 60, cw, ch)
        w.modal_card(surf, rect, radius=14)
        surf.blit(render.gradient_border((cw, ch), 14), rect.topleft)

        pad = 24
        # head: tinted BT glyph chip + title
        hy = rect.y + 20
        ibox = 28
        ibg = pygame.Surface((ibox, ibox), pygame.SRCALPHA)
        pygame.draw.rect(ibg, t.with_alpha(t.Color.ACCENT_ROSE, 0.18),
                         (0, 0, ibox, ibox), border_radius=t.Radius.BUTTON)
        ibg.blit(icons.icon("ph-bluetooth", 18, t.Color.ACCENT_ROSE),
                 ((ibox - 18) // 2, (ibox - 18) // 2))
        surf.blit(ibg, (rect.x + pad, hy))
        title = fonts.render(_TITLE_18, "Confirm passkey", t.Color.TEXT_PRIMARY)
        surf.blit(title, (rect.x + pad + ibox + 10,
                          hy + (ibox - title.get_height()) // 2))

        # target line: "Pair with <device(bold)>? Make sure this code matches…"
        ty = hy + ibox + 10
        seg_a = fonts.render(t.Type.BODY, "Pair with ", t.Color.TEXT_SECONDARY)
        seg_b = fonts.render(t.Type.BODY_STRONG, self.device, t.Color.TEXT_PRIMARY)
        seg_c = fonts.render(t.Type.BODY, "? Make sure this code", t.Color.TEXT_SECONDARY)
        x = rect.x + pad
        surf.blit(seg_a, (x, ty)); x += seg_a.get_width()
        surf.blit(seg_b, (x, ty)); x += seg_b.get_width()
        surf.blit(seg_c, (x, ty))
        ty += seg_a.get_height() + 2
        seg_d = fonts.render(t.Type.BODY, "matches on the other device.",
                             t.Color.TEXT_SECONDARY)
        surf.blit(seg_d, (rect.x + pad, ty))
        ty += seg_d.get_height() + 12

        # 6-digit code cells
        digits = (self.passkey + "------")[:6]
        gap = 8
        cell_w = (cw - pad * 2 - gap * 5) // 6
        cell_h = 56
        for i, dch in enumerate(digits):
            dx = rect.x + pad + i * (cell_w + gap)
            cell = render.rounded_gradient((cell_w, cell_h), t.Color.BG_MID,
                                           t.Color.BG_DEEP, 10, 180)
            pygame.draw.rect(cell, t.Color.BORDER_BRIGHT, (0, 0, cell_w, cell_h),
                             width=1, border_radius=10)
            d_s = fonts.render(_DIGIT, dch, t.Color.TEXT_PRIMARY)
            cell.blit(d_s, ((cell_w - d_s.get_width()) // 2,
                            (cell_h - d_s.get_height()) // 2))
            surf.blit(cell, (dx, ty))
        ty += cell_h + 8

        # expiry hint, centered, counts down
        remain = max(0, self.EXPIRE_S - int(self.elapsed / 1000))
        hint = fonts.render(t.Type.CAPTION, f"expires in {remain} s",
                            t.Color.TEXT_MUTED)
        surf.blit(hint, (rect.centerx - hint.get_width() // 2, ty))
        ty += hint.get_height() + 12

        # two buttons: Reject / Match
        bgap = 10
        bw = (cw - pad * 2 - bgap) // 2
        bh = 42
        by = ty
        # Reject
        rj = pygame.Rect(rect.x + pad, by, bw, bh)
        w.button(surf, rj, "Reject", icon_name="ph-x", focused=(self.focus == 0))
        # Match
        mt = pygame.Rect(rect.x + pad + bw + bgap, by, bw, bh)
        w.button(surf, mt, "Match", icon_name="ph-check", focused=(self.focus == 1))

        w.footer_hints(surf, [("A", "confirm"), ("B", "cancel")])


# ─── S3c — scan found nothing ─────────────────────────────────────────


class S3cEmptyScan:
    """
    S3c ⇄ S3c-pair-empty.html. After the first scan window finds 0 devices:
    a centered radar glyph (with subtle expanding rings), a reassuring head/
    sub, a 3-item checklist of tips (one already passing), and a near-full
    scan-progress bar. A re-scans; B cancels.
    """
    name = "bt-empty-scan"

    def __init__(self, app):
        self.app = app
        self.elapsed = 0.0
        self.tips = [
            ("Adapter is on and discoverable", True),
            ("Hold pair button on the other device for 3 s", False),
            ("Forget on previous host if it auto-reconnects", False),
        ]

    def on_action(self, action):
        if action == "a":
            self.elapsed = 0.0
            self.app.toast("Scanning again…")
        elif action == "b":
            self.app.pop()

    def update(self, dt_ms):
        self.elapsed += dt_ms

    def draw(self, surf):
        secs = max(0, 8 - int(self.elapsed / 1000) % 9)
        w.status_bar(surf, title="Pair new device", back=True,
                     center=w.scanning_chip(24 + secs))

        cx = STAGE_W // 2
        gy = 72
        # expanding ring decoration (radar pulse), behind the glyph
        gcx, gcy = cx, gy + 44
        pygame.draw.rect(surf, t.with_alpha(t.Color.ACCENT_CYAN, 0.25),
                         (gcx - 52, gcy - 52, 104, 104), width=1, border_radius=30)
        pygame.draw.rect(surf, t.with_alpha(t.Color.ACCENT_ROSE, 0.18),
                         (gcx - 66, gcy - 66, 132, 132), width=1, border_radius=38)

        # radar glyph card 88×88
        card = render.rounded_gradient((88, 88), t.Color.SURFACE_BASE,
                                       t.Color.BG_MID, 22, 160)
        pygame.draw.rect(card, t.Color.BORDER_SOFT, (0, 0, 88, 88), width=1,
                         border_radius=22)
        card.blit(icons.icon("ph-magnifying-glass", 48, t.Color.TEXT_MUTED),
                  ((88 - 48) // 2, (88 - 48) // 2))
        surf.blit(card, (cx - 44, gy))

        # clear the decorative radar rings (outer rose ring bottom = gy+110,
        # i.e. 22px below the 88px card) before the headline starts.
        head_gap = 30
        head = fonts.render(_HERO_22, "Nothing nearby", t.Color.TEXT_PRIMARY)
        surf.blit(head, (cx - head.get_width() // 2, gy + 88 + head_gap))

        sy = gy + 88 + head_gap + head.get_height() + 6
        sub = fonts.render(t.Type.BODY,
                           "Make sure your device is in pair mode and within one meter.",
                           t.Color.TEXT_SECONDARY)
        if sub.get_width() > STAGE_W - SAFE * 2:
            sub = sub.subsurface((0, 0, STAGE_W - SAFE * 2, sub.get_height()))
        surf.blit(sub, (cx - sub.get_width() // 2, sy))
        sy += sub.get_height() + 14

        # checklist card
        ck_w = 380
        line_h = 22
        ck_h = 12 * 2 + line_h * len(self.tips)
        ck = pygame.Surface((ck_w, ck_h), pygame.SRCALPHA)
        pygame.draw.rect(ck, t.with_alpha(t.Color.BG_DEEP, 0.55), (0, 0, ck_w, ck_h),
                         border_radius=10)
        pygame.draw.rect(ck, t.Color.BORDER_SOFT, (0, 0, ck_w, ck_h), width=1,
                         border_radius=10)
        ly = 12
        for text, ok in self.tips:
            gi = icons.icon("ph-check" if ok else "ph-circle", 14,
                            t.Color.SUCCESS if ok else t.Color.TEXT_MUTED)
            ck.blit(gi, (16, ly + (line_h - 14) // 2))
            lab = fonts.render(t.Type.CAPTION, text, t.Color.TEXT_SECONDARY)
            ck.blit(lab, (16 + 14 + 10, ly + (line_h - lab.get_height()) // 2))
            ly += line_h
        surf.blit(ck, (cx - ck_w // 2, sy))
        sy += ck_h + 12

        # scan progress (near full) + meta
        bar_w = 460
        bx = cx - bar_w // 2
        track = render.rounded_fill((bar_w, 4), t.Color.BORDER_SOFT, 999)
        surf.blit(track, (bx, sy))
        fill = render.rounded_gradient((int(bar_w * 0.95), 4), t.Color.ACCENT_ROSE,
                                       t.Color.ACCENT_CYAN, 999, 90)
        surf.blit(fill, (bx, sy))
        my = sy + 4 + 6
        ml = fonts.render(t.Type.META_SM, "extending scan window", t.Color.TEXT_MUTED)
        surf.blit(ml, (bx, my))
        mr = fonts.render(t.Type.META_SM, "95% · 1s", t.Color.TEXT_MUTED)
        surf.blit(mr, (bx + bar_w - mr.get_width(), my))

        w.footer_hints(surf, [("A", "scan again"), ("B", "cancel")])


# ─── S6 — set up Bluetooth audio (step wizard) ────────────────────────


class S6Setup:
    """
    S6 ⇄ S6-setup.html. One-time "Set up Bluetooth audio" wizard. Step rows
    progressively reveal status (done / in-progress / pending); the active
    row uses the spinning rose→cyan ring + a cyan substring. Non-interactive
    while running (footer reads "Please wait…").

    Consumer language only — substeps are audio-routing terms, never the
    underlying kernel/driver work.
    """
    name = "bt-setup"

    def __init__(self, app):
        self.app = app
        self.elapsed = 0.0
        self.job_id = None
        self.poll_ms = 0.0
        self.done = False
        self.failed = False
        self.failed_title = ""
        # (status, title, subline, pct, pct_kind)
        self.steps = [
            ("done", "Install audio engine",
             "12.4 MB · downloaded and verified", "done", "ok"),
            ("in_progress", "Set up adapter compatibility",
             "preparing your Bluetooth dongle…", "45%", "cyan"),
            ("pending", "Tune audio connection",
             "stops crackles and dropouts", "queued", "muted"),
            ("pending", "Connect audio routing",
             "so games and ports use Bluetooth", "queued", "muted"),
        ]
        self.step_no = 2
        self.total = 4
        self.frac = 0.45
        self._start_live_setup()

    def _start_live_setup(self):
        env = backend.bluetooth_prepare_apply()
        if not env:
            return
        job = (((env.get("data") or {}).get("job") or {}))
        job_id = job.get("job_id")
        if job_id:
            self.job_id = job_id
            backend.gui_log("bt_setup.job_started", job_id=job_id)
            self._apply_job(job)
            return
        if not env.get("ok"):
            self.failed = True
            self.failed_title = env.get("title") or "Bluetooth setup paused"
            self.steps = [
                ("error", "Check Bluetooth adapter",
                 env.get("summary") or "setup could not start", "err", "err"),
                ("pending", "Install audio engine", "waiting for adapter", "queued", "muted"),
                ("pending", "Tune audio connection", "waiting for adapter", "queued", "muted"),
                ("pending", "Connect audio routing", "waiting for adapter", "queued", "muted"),
            ]
            self.step_no = 1
            self.frac = 0.05
            backend.gui_log("bt_setup.start_failed", title=self.failed_title, state=env.get("state"))

    def _apply_job(self, job):
        progress = job.get("progress") or {}
        percent = int(progress.get("percent") or 0)
        step_no = int(progress.get("step") or 1)
        phase = progress.get("phase") or "Working"
        state = job.get("state")
        self.step_no = max(1, min(self.total, step_no))
        self.frac = max(0.0, min(1.0, percent / 100.0))
        titles = [
            "Install audio engine",
            "Set up adapter compatibility",
            "Tune audio connection",
            "Connect audio routing",
        ]
        rows = []
        for idx, title in enumerate(titles, start=1):
            if state == "completed" or idx < self.step_no:
                rows.append(("done", title, "checked", "done", "ok"))
            elif idx == self.step_no:
                rows.append(("in_progress", title, phase, f"{percent}%", "cyan"))
            else:
                rows.append(("pending", title, "queued", "queued", "muted"))
        if state == "failed":
            fail_idx = max(1, min(self.total, self.step_no))
            rows[fail_idx - 1] = (
                "error",
                titles[fail_idx - 1],
                job.get("summary") or "setup did not finish",
                "err",
                "err",
            )
            self.failed = True
            self.failed_title = job.get("title") or "Bluetooth setup paused"
        if state == "completed":
            self.done = True
            self.step_no = self.total
            self.frac = 1.0
        self.steps = rows

    def on_action(self, action):
        # non-interactive while patching; B lets the user back out
        if action == "a" and self.done:
            self.app.pop()
        elif action == "b":
            self.app.pop()

    def update(self, dt_ms):
        self.elapsed += dt_ms
        if not self.job_id or self.done or self.failed:
            return
        self.poll_ms += dt_ms
        if self.poll_ms < 450:
            return
        self.poll_ms = 0.0
        env = backend.job_status(self.job_id)
        if not env:
            return
        job = ((env.get("data") or {}).get("job") or {})
        self._apply_job(job)
        if self.done:
            backend.gui_log("bt_setup.job_completed", job_id=self.job_id)
            self.app.toast(job.get("title") or "Bluetooth setup ready")
            self.job_id = None
        elif self.failed:
            backend.gui_log("bt_setup.job_failed", job_id=self.job_id, title=self.failed_title)
            self.app.push(S6bSetupFailed(self.app, title=self.failed_title, summary=job.get("summary")))
            self.job_id = None

    def draw(self, surf):
        w.status_bar(surf, title="First-time setup", back=False)
        chip = _step_chip(self.step_no, self.total, halted=self.failed)
        surf.blit(chip, (STAGE_W - SAFE - 150 - chip.get_width(), 14))

        y = _setup_hero(surf, 60,
                        "Set up Bluetooth audio",
                        "This runs once. You can keep playing — "
                        "we'll let you know when it's ready.")
        y += 12

        # step rows
        row_h = 56
        ring_frac = (self.elapsed % 1100) / 1100
        for status, title, sub, pct, kind in self.steps:
            rect = pygame.Rect(SAFE, y, STAGE_W - SAFE * 2, row_h)
            pct_color = {"ok": t.Color.SUCCESS, "cyan": t.Color.ACCENT_CYAN,
                         "muted": t.Color.TEXT_MUTED,
                         "err": t.Color.ERROR}.get(kind, t.Color.TEXT_SECONDARY)
            _step_row(surf, rect, status=status, title=title, sub=sub,
                      pct=pct, pct_color=pct_color, ring_frac=ring_frac)
            y += row_h + 8

        # footer progress bar pinned near the bottom
        pct = int(round(self.frac * 100))
        footer_label = "setup paused" if self.failed else ("ready" if self.done else "preparing your adapter…")
        _setup_footer_bar(surf, STAGE_H - t.Space.FOOTER_HINT_HEIGHT - 40,
                          self.frac, footer_label,
                          f"{self.step_no} of {self.total} · {pct}%")

        # non-interactive footer hint; halted/done states let the user leave.
        hint = "A continue   B back" if self.done else ("B back" if self.failed else "Please wait…")
        wait = fonts.render(t.Type.CAPTION, hint, t.Color.TEXT_MUTED)
        pygame.draw.line(surf, t.Color.BORDER_SOFT, (0, 448), (640, 448), 1)
        surf.blit(wait, (STAGE_W // 2 - wait.get_width() // 2,
                         448 + t.Space.FOOTER_HINT_HEIGHT // 2
                         - wait.get_height() // 2))


# ─── S6b — a setup step failed (recovery) ─────────────────────────────


class S6bSetupFailed:
    """
    S6b ⇄ S6b-setup-failed.html. Mirrors S6 for spatial consistency. The
    failed step turns error-colored, later rows annotate "waiting for step
    N", and an error dock surfaces a plain-English reason + two inline
    actions (Details / Retry). Footer adds "X skip step" for advanced users.
    A retries, X skips, B aborts.

    Consumer language only — the reason mentions firmware drift in human
    terms, never kernel/driver internals.
    """
    name = "bt-setup-failed"

    def __init__(self, app, title=None, summary=None):
        self.app = app
        self.reason_title = title or "Setup paused"
        self.reason_summary = summary or "setup didn't match your firmware"
        self.steps = [
            ("done", "Install audio engine",
             "12.4 MB · downloaded and verified", "done", "ok"),
            ("error", "Set up adapter compatibility",
             self.reason_summary, "err", "err"),
            ("pending", "Tune audio connection", "waiting for step 2",
             "queued", "muted"),
            ("pending", "Connect audio routing", "waiting for step 2",
             "queued", "muted"),
        ]
        self.step_no = 2
        self.total = 4
        self.frac = 0.32
        self.focus = 1  # 0 = Details, 1 = Retry (dock buttons)

    def on_action(self, action):
        if action in ("left", "right"):
            self.focus ^= 1
        elif action == "a":
            if self.focus == 0:
                self.app.toast("Details are in the support report")
                return
            self.app.pop()
            self.app.push(S6Setup(self.app))
            self.app.toast("Retrying setup…")
        elif action == "x":
            self.app.pop()
            self.app.toast("Skipped — Bluetooth audio may not work")
        elif action == "b":
            self.app.pop()

    def update(self, dt_ms):
        pass

    def draw(self, surf):
        w.status_bar(surf, title="First-time setup", back=False)
        chip = _step_chip(self.step_no, self.total, halted=True)
        surf.blit(chip, (STAGE_W - SAFE - 150 - chip.get_width(), 14))

        y = _setup_hero(surf, 60, self.reason_title,
                        "Step 2 didn't finish. You can retry, skip, "
                        "or undo what's been done so far.")
        y += 12

        # step rows (slightly tighter to make room for the error dock)
        row_h = 50
        for status, title, sub, pct, kind in self.steps:
            rect = pygame.Rect(SAFE, y, STAGE_W - SAFE * 2, row_h)
            pct_color = {"ok": t.Color.SUCCESS, "cyan": t.Color.ACCENT_CYAN,
                         "muted": t.Color.TEXT_MUTED,
                         "err": t.Color.ERROR}.get(kind, t.Color.TEXT_SECONDARY)
            _step_row(surf, rect, status=status, title=title, sub=sub,
                      pct=pct, pct_color=pct_color)
            y += row_h + 6

        # error dock: warning glyph + reason + Details/Retry buttons
        y += 2
        dock_h = 56
        dock = pygame.Rect(SAFE, y, STAGE_W - SAFE * 2, dock_h)
        body = pygame.Surface((dock.w, dock.h), pygame.SRCALPHA)
        pygame.draw.rect(body, t.with_alpha(t.Color.ERROR, 0.06),
                         (0, 0, dock.w, dock.h), border_radius=10)
        pygame.draw.rect(body, t.with_alpha(t.Color.ERROR, 0.40),
                         (0, 0, dock.w, dock.h), width=1, border_radius=10)
        pad = 12
        wic = icons.icon("ph-warning", 20, t.Color.ERROR)
        body.blit(wic, (pad, (dock.h - 20) // 2))

        # inline buttons measured from the right
        b_h = 32
        b_gap = 6
        retry_lab = fonts.render(t.Type.CAPTION, "Retry", t.Color.TEXT_PRIMARY)
        details_lab = fonts.render(t.Type.CAPTION, "Details", t.Color.TEXT_PRIMARY)
        retry_w = retry_lab.get_width() + 24
        details_w = details_lab.get_width() + 24
        retry_x = dock.w - pad - retry_w
        details_x = retry_x - b_gap - details_w
        b_y = (dock.h - b_h) // 2
        # render buttons onto body via shared button widget (needs a surface)
        w.button(body, pygame.Rect(details_x, b_y, details_w, b_h), "Details",
                 focused=(self.focus == 0))
        w.button(body, pygame.Rect(retry_x, b_y, retry_w, b_h), "Retry",
                 focused=(self.focus == 1))

        # message between glyph and buttons (bold lead + secondary tail)
        mx = pad + 20 + 10
        msg_maxw = details_x - mx - 10
        lead = fonts.render(t.Type.CAPTION, "Adapter setup didn't match your firmware.",
                            t.Color.ERROR)
        tail = fonts.render(t.Type.CAPTION,
                            "Your dArkOSRE may have updated since the last release.",
                            t.Color.TEXT_SECONDARY)
        if lead.get_width() > msg_maxw:
            lead = lead.subsurface((0, 0, max(msg_maxw, 1), lead.get_height()))
        if tail.get_width() > msg_maxw:
            tail = tail.subsurface((0, 0, max(msg_maxw, 1), tail.get_height()))
        stack_h = lead.get_height() + 2 + tail.get_height()
        msy = (dock.h - stack_h) // 2
        body.blit(lead, (mx, msy))
        body.blit(tail, (mx, msy + lead.get_height() + 2))
        surf.blit(body, dock.topleft)
        y += dock_h + 10

        # footer progress (error-colored, only truly-completed portion)
        _setup_footer_bar(surf, STAGE_H - t.Space.FOOTER_HINT_HEIGHT - 34,
                          self.frac, "paused at adapter setup",
                          f"1 of {self.total} · 32%", error=True)

        w.footer_hints(surf, [("A", "retry"), ("X", "skip step"), ("B", "abort")])
