"""
Reusable UI widgets shared across screens.

Each draw_* function renders onto a target surface at given coords using
design tokens. Focusable widgets take a `focused` flag and apply the
signature rose→cyan glow + gradient border + 1.03 scale via render.py.

Coordinates follow the designer spec tables (640×480 stage, top-left).
"""

from __future__ import annotations

import pygame

from ..design import tokens as t
from . import fonts, icons, render

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


# ─── focus decoration ────────────────────────────────────────────────


def draw_focused_card(
    surf: pygame.Surface,
    rect: pygame.Rect,
    radius: int,
    *,
    scale: float = 1.0,
    base_surface: pygame.Surface | None = None,
):
    """
    Draw the focus treatment for a card/row whose body is `base_surface`
    (already rendered at rect size). Glow behind, optional scale, gradient
    border on top. If base_surface is None caller draws body separately.
    """
    glow, gpos = render.focus_glow(rect, radius)
    surf.blit(glow, gpos)
    if base_surface is not None:
        if abs(scale - 1.0) > 1e-3:
            sw, sh = int(rect.w * scale), int(rect.h * scale)
            scaled = pygame.transform.smoothscale(base_surface, (sw, sh))
            pos = (rect.centerx - sw // 2, rect.centery - sh // 2)
            surf.blit(scaled, pos)
            border = pygame.transform.smoothscale(
                render.gradient_border((rect.w, rect.h), radius), (sw, sh))
            surf.blit(border, pos)
        else:
            surf.blit(base_surface, rect.topleft)
            surf.blit(render.gradient_border((rect.w, rect.h), radius), rect.topleft)


# ─── status bar (top 48px) ───────────────────────────────────────────


def battery_chip(level_pct: int = 87) -> pygame.Surface:
    """Phosphor battery glyph + percent label, right-aligned cluster."""
    # pick the phosphor battery by level
    name = ("ph-battery-full" if level_pct >= 80 else
            "ph-battery-high" if level_pct >= 55 else
            "ph-battery-medium" if level_pct >= 30 else
            "ph-battery-low")
    ic = icons.icon(name, 18, t.Color.TEXT_SECONDARY)
    lbl = fonts.render(t.Type.MONO_TECH, f"{level_pct}%", t.Color.TEXT_SECONDARY)
    w = ic.get_width() + 6 + lbl.get_width()
    h = max(ic.get_height(), lbl.get_height())
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    s.blit(ic, (0, (h - ic.get_height()) // 2))
    s.blit(lbl, (ic.get_width() + 6, (h - lbl.get_height()) // 2))
    return s


def status_bar(surf, title=None, *, eyebrow=None, back=False,
               battery=87, clock="14:32", center=None):
    """
    Draw the top status bar.
    - eyebrow: small uppercase brand (home screen)
    - title + back: section screens (back chevron + title)
    - center: optional (surface, ) tuple drawn centered (e.g. scanning chip)
    """
    y_text = 14
    x = SAFE
    if back:
        chev = icons.icon("ph-caret-left", 22, t.Color.TEXT_SECONDARY)
        surf.blit(chev, (x, y_text))
        x += chev.get_width() + 6
    if eyebrow:
        eb = fonts.render(t.Type.EYEBROW, eyebrow, t.Color.TEXT_SECONDARY, upper=True)
        surf.blit(eb, (x, 18))
    elif title:
        tt = fonts.render(t.Type.TITLE, title, t.Color.TEXT_PRIMARY)
        surf.blit(tt, (x, y_text))

    # right cluster: battery + clock
    rx = STAGE_W - SAFE
    clk = fonts.render(t.Type.MONO_TECH, clock, t.Color.TEXT_PRIMARY)
    rx -= clk.get_width()
    surf.blit(clk, (rx, 18))
    rx -= t.Space.L
    batt = battery_chip(battery)
    rx -= batt.get_width()
    surf.blit(batt, (rx, 16))

    # optional centered element (e.g. "scanning · 8s")
    if center is not None:
        cs = center
        surf.blit(cs, ((STAGE_W - cs.get_width()) // 2, 18))


def scanning_chip(seconds: int) -> pygame.Surface:
    ic = icons.icon("ph-broadcast", 16, t.Color.ACCENT_CYAN)
    lbl = fonts.render(t.Type.CAPTION, f"scanning · {seconds}s", t.Color.ACCENT_CYAN)
    w = ic.get_width() + 6 + lbl.get_width()
    h = max(ic.get_height(), lbl.get_height())
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    s.blit(ic, (0, (h - ic.get_height()) // 2))
    s.blit(lbl, (ic.get_width() + 6, (h - lbl.get_height()) // 2))
    return s


# ─── footer hints (bottom 32px) ──────────────────────────────────────


def _btn_glyph(ch: str) -> pygame.Surface:
    d = 22
    s = pygame.Surface((d, d), pygame.SRCALPHA)
    pygame.draw.circle(s, t.Color.TEXT_MUTED, (d // 2, d // 2), d // 2, 2)
    lbl = fonts.render(t.Type.CAPTION, ch, t.Color.TEXT_SECONDARY)
    s.blit(lbl, (d // 2 - lbl.get_width() // 2, d // 2 - lbl.get_height() // 2))
    return s


def footer_hints(surf, hints: list[tuple[str, str]]):
    """hints = [("A","select"), ("B","back"), ...] centered with divider."""
    pygame.draw.line(surf, t.Color.BORDER_SOFT, (0, 448), (640, 448), 1)
    parts = [(_btn_glyph(g), fonts.render(t.Type.CAPTION, l, t.Color.TEXT_SECONDARY))
             for g, l in hints]
    gap_inner, gap_outer = 8, t.Space.XL
    total = sum(g.get_width() + gap_inner + l.get_width() for g, l in parts)
    total += gap_outer * (len(parts) - 1)
    x = (640 - total) // 2
    cy = 448 + t.Space.FOOTER_HINT_HEIGHT // 2
    for g, l in parts:
        surf.blit(g, (x, cy - g.get_height() // 2))
        x += g.get_width() + gap_inner
        surf.blit(l, (x, cy - l.get_height() // 2))
        x += l.get_width() + gap_outer


# ─── hero zone ───────────────────────────────────────────────────────


def hero_zone(surf, y, icon_name, title, subline_segments, *,
              icon_grad=True, height=None, divider=True):
    """
    icon (rounded gradient bg + dark glyph) + title + subline.
    subline_segments: list of (text, color) tuples joined with " · ".
    """
    icon_box = 56
    x = SAFE
    # gradient rounded icon
    box = render.rounded_gradient((icon_box, icon_box),
                                  t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, 14, 135)
    glyph = icons.icon(icon_name, 30, t.Color.BG_MID)
    box.blit(glyph, ((icon_box - 30) // 2, (icon_box - 30) // 2))
    surf.blit(box, (x, y))

    tx = x + icon_box + 16
    title_s = fonts.render(t.Type.HERO, title, t.Color.TEXT_PRIMARY)
    surf.blit(title_s, (tx, y + 6))

    # subline
    sy = y + 6 + title_s.get_height() + 6
    sx = tx
    for i, (text, color) in enumerate(subline_segments):
        if i > 0:
            dot = fonts.render(t.Type.BODY, " · ", t.Color.TEXT_MUTED)
            surf.blit(dot, (sx, sy)); sx += dot.get_width()
        seg = fonts.render(t.Type.BODY, text, color)
        surf.blit(seg, (sx, sy)); sx += seg.get_width()

    if divider:
        dy = y + (height or 100) - 4
        pygame.draw.line(surf, t.Color.BORDER_SOFT, (SAFE, dy), (640 - SAFE, dy), 1)


# ─── list row (icon + label + chevron) ───────────────────────────────


def list_row(surf, rect, label, icon_name, *, focused=False, dim=False,
             icon_color=None, chevron=True):
    body = render.rounded_gradient((rect.w, rect.h),
                                   t.Color.SURFACE_BASE, t.Color.BG_MID,
                                   t.Radius.BUTTON, 160)
    if focused:
        body = render.rounded_gradient((rect.w, rect.h),
                                       t.Color.SURFACE_ELEVATED, t.Color.SURFACE_BASE,
                                       t.Radius.BUTTON, 160)
    else:
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=t.Radius.BUTTON)

    icol = icon_color or (t.Color.ACCENT_ROSE if focused else t.Color.TEXT_SECONDARY)
    ic = icons.icon(icon_name, 22, icol)
    body.blit(ic, (16, (rect.h - 22) // 2))
    lab = fonts.render(t.Type.BODY_STRONG, label, t.Color.TEXT_PRIMARY)
    body.blit(lab, (16 + 22 + 16, (rect.h - lab.get_height()) // 2))
    if chevron:
        ch = icons.icon("ph-caret-right", 14,
                        t.Color.TEXT_PRIMARY if focused else t.Color.TEXT_MUTED)
        body.blit(ch, (rect.w - 16 - 14, (rect.h - 14) // 2))

    if dim and not focused:
        body.set_alpha(int(255 * 0.6))

    if focused:
        draw_focused_card(surf, rect, t.Radius.BUTTON, scale=1.03, base_surface=body)
    else:
        surf.blit(body, rect.topleft)


# ─── gradient text + brand wordmark ──────────────────────────────────


def gradient_text(style, text, *, c0=t.Color.ACCENT_ROSE, c1=t.Color.ACCENT_CYAN,
                  angle=135.0):
    """Render `text` filled with a rose→cyan linear gradient (the brand mark)."""
    base = fonts.render(style, text, (255, 255, 255))
    bw, bh = base.get_size()
    if bw == 0 or bh == 0:
        return base
    grad = render.linear_gradient((bw, bh), c0, c1, angle).convert_alpha()
    out = base.copy()
    out.blit(grad, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
    return out


def brand_wordmark() -> pygame.Surface:
    """`AAhead Control` lockup: gradient AA + 'head' primary + 'Control' muted."""
    big = t.TextStyle(t.SPACE_GROTESK_BOLD, 14, "Bold", -0.3)
    small = t.TextStyle(t.SPACE_GROTESK_SEMIBOLD, 11, "SemiBold", 0.2)
    aa = gradient_text(big, "AA")
    head = fonts.render(big, "head", t.Color.TEXT_PRIMARY)
    ctrl = fonts.render(small, "Control", t.Color.TEXT_SECONDARY)
    gap = 5
    h = max(aa.get_height(), head.get_height(), ctrl.get_height())
    w = aa.get_width() + head.get_width() + gap + ctrl.get_width()
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    x = 0
    s.blit(aa, (x, h - aa.get_height())); x += aa.get_width()
    s.blit(head, (x, h - head.get_height())); x += head.get_width() + gap
    s.blit(ctrl, (x, h - ctrl.get_height() - 1))
    return s


# ─── action-queue strip (H1 home) ────────────────────────────────────


def action_queue(surf, rect, message, count):
    """Peach-tinted attention strip: warn glyph + message + count pill + chevron."""
    peach = t.Color.ACCENT_PEACH
    body = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
    pygame.draw.rect(body, t.with_alpha(peach, 0.08), (0, 0, rect.w, rect.h), border_radius=10)
    pygame.draw.rect(body, t.with_alpha(peach, 0.35), (0, 0, rect.w, rect.h), width=1, border_radius=10)
    pad = 12
    box = 22
    ibg = pygame.Surface((box, box), pygame.SRCALPHA)
    pygame.draw.rect(ibg, t.with_alpha(peach, 0.18), (0, 0, box, box), border_radius=6)
    ic = icons.icon("ph-warning", 13, peach)
    ibg.blit(ic, ((box - 13) // 2, (box - 13) // 2))
    body.blit(ibg, (pad, (rect.h - box) // 2))

    # count pill + chevron (right side, measured first)
    chev = icons.icon("ph-caret-right", 12, t.Color.TEXT_MUTED)
    chev_x = rect.w - pad - chev.get_width()
    body.blit(chev, (chev_x, (rect.h - chev.get_height()) // 2))
    cnt = fonts.render(t.Type.META_SM, str(count), peach)
    pill_w, pill_h = cnt.get_width() + 14, 16
    pill = pygame.Surface((pill_w, pill_h), pygame.SRCALPHA)
    pygame.draw.rect(pill, t.with_alpha(peach, 0.20), (0, 0, pill_w, pill_h), border_radius=999)
    pill.blit(cnt, ((pill_w - cnt.get_width()) // 2, (pill_h - cnt.get_height()) // 2))
    pill_x = chev_x - 8 - pill_w
    body.blit(pill, (pill_x, (rect.h - pill_h) // 2))

    # message: bold lead before " — ", muted remainder
    mx = pad + box + 10
    my_cy = rect.h // 2
    lead, sep, rest = message.partition(" — ")
    lead_s = fonts.render(t.Type.LABEL_SM, lead, t.Color.TEXT_PRIMARY)
    body.blit(lead_s, (mx, my_cy - lead_s.get_height() // 2))
    mx += lead_s.get_width()
    if rest:
        rest_s = fonts.render(t.Type.BODY_SM, sep + rest, t.Color.TEXT_SECONDARY)
        body.blit(rest_s, (mx, my_cy - rest_s.get_height() // 2))
    surf.blit(body, rect.topleft)


# ─── status card (H1 console grid) ───────────────────────────────────

_DOT = {"ok": t.Color.SUCCESS, "warn": t.Color.WARNING, "err": t.Color.ERROR,
        "off": t.Color.TEXT_DISABLED, "muted": t.Color.TEXT_DISABLED}
_STATE = {"ok": t.Color.SUCCESS, "warn": t.Color.ACCENT_PEACH, "err": t.Color.ERROR,
          "muted": t.Color.TEXT_MUTED, "off": t.Color.TEXT_MUTED}
_TINT = {"cyan": t.Color.ACCENT_CYAN, "mint": t.Color.SUCCESS,
         "rose": t.Color.ACCENT_ROSE, "peach": t.Color.ACCENT_PEACH}


def status_card(surf, rect, *, icon_name, label, state, detail,
                dot="muted", state_level=None, tint="cyan", focused=False, dim=False):
    """
    H1 status card: tinted glyph + label + status dot (top), one-line
    state verdict (color-coded), small mono detail pinned to the bottom.
    """
    radius = 10
    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 = 11
    tint_rgb = _TINT.get(tint, t.Color.ACCENT_CYAN)
    box = 26
    ibg = pygame.Surface((box, box), pygame.SRCALPHA)
    pygame.draw.rect(ibg, t.with_alpha(tint_rgb, 0.12), (0, 0, box, box), border_radius=7)
    pygame.draw.rect(ibg, t.with_alpha(tint_rgb, 0.35), (0, 0, box, box), width=1, border_radius=7)
    ic = icons.icon(icon_name, 14, tint_rgb)
    ibg.blit(ic, ((box - 14) // 2, (box - 14) // 2))
    body.blit(ibg, (pad, 10))

    lab = fonts.render(t.Type.LABEL_SM, label, t.Color.TEXT_PRIMARY)
    body.blit(lab, (pad + box + 8, 10 + (box - lab.get_height()) // 2))

    dr = 4
    pygame.draw.circle(body, _DOT.get(dot, t.Color.TEXT_DISABLED),
                       (rect.w - pad - dr, 10 + box // 2), dr)

    st = fonts.render(t.Type.STATE, state, _STATE.get(state_level or dot, t.Color.TEXT_PRIMARY))
    body.blit(st, (pad, 10 + box + 9))

    if detail:
        dt = fonts.render(t.Type.META_SM, detail, t.Color.TEXT_SECONDARY)
        body.blit(dt, (pad, rect.h - 12 - dt.get_height()))

    if dim and not focused:
        body.set_alpha(int(255 * 0.6))

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


# ─── confidence meter (D1 hero) ──────────────────────────────────────


def confidence_meter(filled: int, total: int = 5, *,
                     on_color=t.Color.SUCCESS) -> pygame.Surface:
    """Row of small bars (D1 "match confidence"): `filled` of `total` lit."""
    bw, bh, gap = 16, 6, 3
    w = bw * total + gap * (total - 1)
    s = pygame.Surface((w, bh), pygame.SRCALPHA)
    for i in range(total):
        col = on_color if i < filled else t.Color.BORDER_SOFT
        pygame.draw.rect(s, col, (i * (bw + gap), 0, bw, bh), border_radius=1)
    return s


# ─── info card (D1 detail grid: header + key/value rows) ─────────────

_KV_VALUE = {"ok": t.Color.SUCCESS, "warn": t.Color.ACCENT_PEACH,
             "cyan": t.Color.ACCENT_CYAN, None: t.Color.TEXT_SECONDARY}


def info_card(surf, rect, *, title, icon_name, icon_color, rows, key_w=88):
    """
    D1 detail card: uppercase tinted header (icon + title) over a key/value
    grid. `rows` = list of (key, value, value_state) where value_state is
    one of None/"ok"/"warn"/"cyan" (maps to a token color).
    """
    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_x, pad_t = 12, 10
    # header: small icon + uppercase muted title
    hic = icons.icon(icon_name, 13, icon_color)
    body.blit(hic, (pad_x, pad_t + 1))
    htitle = fonts.render(t.Type.EYEBROW, title, t.Color.TEXT_MUTED, upper=True)
    body.blit(htitle, (pad_x + hic.get_width() + 6,
                       pad_t + (hic.get_height() - htitle.get_height()) // 2))

    # key/value grid (mono), rows evenly stacked under the header
    gy = pad_t + max(hic.get_height(), htitle.get_height()) + 8
    line_h = 16
    for i, (key, value, state) in enumerate(rows):
        ry = gy + i * line_h
        k = fonts.render(t.Type.META_SM, key, t.Color.TEXT_MUTED)
        body.blit(k, (pad_x, ry))
        vcol = _KV_VALUE.get(state, t.Color.TEXT_SECONDARY)
        v = fonts.render(t.Type.META_SM, value, vcol)
        # value column, clipped to card width
        vx = pad_x + key_w
        maxw = rect.w - pad_x - vx
        if v.get_width() > maxw:
            v = v.subsurface((0, 0, max(maxw, 1), v.get_height()))
        body.blit(v, (vx, ry))

    surf.blit(body, rect.topleft)


# ─── device row (pair list: icon + name + subline + RSSI) ────────────


def rssi_bars(level: int) -> pygame.Surface:
    """4 vertical bars, `level` (0-4) filled in secondary, rest disabled."""
    bw, gap, maxh = 4, 3, 18
    w = bw * 4 + gap * 3
    s = pygame.Surface((w, maxh), pygame.SRCALPHA)
    for i in range(4):
        bh = int(maxh * (i + 1) / 4)
        col = t.Color.TEXT_SECONDARY if i < level else t.Color.TEXT_DISABLED
        x = i * (bw + gap)
        pygame.draw.rect(s, col, (x, maxh - bh, bw, bh), border_radius=1)
    return s


def device_row(surf, rect, name, subline, icon_name, rssi, *, focused=False):
    body = render.rounded_gradient((rect.w, rect.h),
                                   t.Color.SURFACE_BASE, t.Color.BG_MID,
                                   t.Radius.BUTTON, 160)
    if focused:
        body = render.rounded_gradient((rect.w, rect.h),
                                       t.Color.SURFACE_ELEVATED, t.Color.SURFACE_BASE,
                                       t.Radius.BUTTON, 160)
    else:
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=t.Radius.BUTTON)

    icol = t.Color.ACCENT_ROSE if focused else t.Color.TEXT_SECONDARY
    ic = icons.icon(icon_name, 28, icol)
    body.blit(ic, (16, (rect.h - 28) // 2))
    # name + subline stack
    name_s = fonts.render(t.Type.TITLE, name, t.Color.TEXT_PRIMARY)
    sub_s = fonts.render(t.Type.CAPTION, subline, t.Color.ACCENT_CYAN)
    stack_h = name_s.get_height() + 2 + sub_s.get_height()
    sy = (rect.h - stack_h) // 2
    sx = 16 + 28 + 16
    body.blit(name_s, (sx, sy))
    body.blit(sub_s, (sx, sy + name_s.get_height() + 2))
    # rssi right
    bars = rssi_bars(rssi)
    body.blit(bars, (rect.w - 16 - bars.get_width(), (rect.h - bars.get_height()) // 2))

    if focused:
        draw_focused_card(surf, rect, t.Radius.BUTTON, scale=1.02, base_surface=body)
    else:
        surf.blit(body, rect.topleft)


# ─── segmented control (Speakers / Bluetooth) ────────────────────────


def segmented(surf, rect, segments, selected_idx, *, focused_idx=None):
    """Two-segment pill. segments=[(icon,label),...]."""
    body = render.rounded_fill((rect.w, rect.h), t.Color.SURFACE_BASE, t.Radius.BUTTON)
    pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                     width=1, border_radius=t.Radius.BUTTON)
    n = len(segments)
    seg_w = rect.w // n
    for i, (icon_name, label) in enumerate(segments):
        sx = i * seg_w
        sel = (i == selected_idx)
        seg_rect = pygame.Rect(sx + 4, 4, seg_w - 8, rect.h - 8)
        if sel:
            seg_surf = render.rounded_gradient((seg_rect.w, seg_rect.h),
                                               t.Color.SURFACE_ELEVATED,
                                               t.Color.SURFACE_BASE, t.Radius.CHIP, 160)
            body.blit(seg_surf, seg_rect.topleft)
        col = t.Color.TEXT_PRIMARY if sel else t.Color.TEXT_MUTED
        ic = icons.icon(icon_name, 20, col)
        lab = fonts.render(t.Type.BODY, label, col)
        cw = ic.get_width() + 8 + lab.get_width()
        cx = sx + (seg_w - cw) // 2
        cy = rect.h // 2
        body.blit(ic, (cx, cy - ic.get_height() // 2))
        body.blit(lab, (cx + ic.get_width() + 8, cy - lab.get_height() // 2))
    surf.blit(body, rect.topleft)
    if focused_idx is not None:
        sx = rect.x + focused_idx * seg_w + 4
        fr = pygame.Rect(sx, rect.y + 4, seg_w - 8, rect.h - 8)
        surf.blit(render.gradient_border((fr.w, fr.h), t.Radius.CHIP), fr.topleft)


# ─── volume slider ───────────────────────────────────────────────────


def slider(surf, x, y, w, value_frac):
    """Track + gradient fill + knob. value_frac 0..1."""
    h = 6
    track = render.rounded_fill((w, h), t.Color.BORDER_SOFT, 999)
    surf.blit(track, (x, y))
    fw = max(int(w * value_frac), h)
    fill = render.rounded_gradient((fw, h), t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, 999, 90)
    surf.blit(fill, (x, y))
    # knob
    kx = x + int(w * value_frac)
    pygame.draw.circle(surf, t.Color.TEXT_PRIMARY, (kx, y + h // 2), 7)
    pygame.draw.circle(surf, t.Color.ACCENT_ROSE, (kx, y + h // 2), 7, 2)


# ─── button ──────────────────────────────────────────────────────────


def button(surf, rect, label, *, icon_name=None, focused=False, primary=False):
    body = render.rounded_gradient((rect.w, rect.h),
                                   t.Color.SURFACE_ELEVATED if focused else t.Color.SURFACE_BASE,
                                   t.Color.SURFACE_BASE if focused else t.Color.BG_MID,
                                   t.Radius.BUTTON, 160)
    if not focused:
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=t.Radius.BUTTON)
    style = t.Type.BODY_STRONG
    col = t.Color.TEXT_PRIMARY
    lab = fonts.render(style, label, col)
    content_w = lab.get_width()
    ic = None
    if icon_name:
        ic = icons.icon(icon_name, 20, col)
        content_w += ic.get_width() + 8
    cx = (rect.w - content_w) // 2
    cy = rect.h // 2
    if ic:
        body.blit(ic, (cx, cy - ic.get_height() // 2))
        cx += ic.get_width() + 8
    body.blit(lab, (cx, cy - lab.get_height() // 2))
    if focused:
        draw_focused_card(surf, rect, t.Radius.BUTTON, scale=1.0, base_surface=body)
    else:
        surf.blit(body, rect.topleft)


# ─── progress bar ────────────────────────────────────────────────────


def progress_bar(surf, x, y, w, frac, *, h=6):
    track = render.rounded_fill((w, h), t.Color.BORDER_SOFT, 999)
    surf.blit(track, (x, y))
    fw = max(int(w * frac), h)
    fill = render.rounded_gradient((fw, h), t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, 999, 90)
    surf.blit(fill, (x, y))


# ─── modal primitives (dialogs) ──────────────────────────────────────


def scrim(surf, alpha=0.62):
    """Dim the whole stage behind a modal."""
    s = pygame.Surface((STAGE_W, t.VIEWPORT_HEIGHT), pygame.SRCALPHA)
    s.fill((6, 4, 14, int(alpha * 255)))
    surf.blit(s, (0, 0))


def modal_card(surf, rect, *, radius=t.Radius.CARD):
    """Elevated card with a faint gradient hairline border, centered usage."""
    body = render.rounded_gradient((rect.w, rect.h),
                                   t.Color.SURFACE_ELEVATED, t.Color.SURFACE_BASE,
                                   radius, 160)
    surf.blit(body, rect.topleft)
    # faint gradient border
    border = render.gradient_border((rect.w, rect.h), radius, width=1)
    border.set_alpha(140)
    surf.blit(border, rect.topleft)


def conic_ring(surf, cx, cy, r, frac, *, thickness=10):
    """
    Progress ring: rose→cyan arc from top (-90°) clockwise by frac*360.
    Drawn as short arc segments with color interpolated along the sweep.
    """
    import math
    # track
    pygame.draw.circle(surf, t.Color.BORDER_SOFT, (cx, cy), r, thickness)
    if frac <= 0:
        return
    start = -90.0
    sweep = 360.0 * max(0.0, min(frac, 1.0))
    steps = max(2, int(sweep / 4))
    for i in range(steps):
        a0 = start + sweep * i / steps
        a1 = start + sweep * (i + 1) / steps
        col = render._lerp(t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, i / steps)
        # draw a thick segment via two arcs is hard; approximate with a
        # filled polygon ring slice
        rad0, rad1 = math.radians(a0), math.radians(a1)
        ro, ri = r + thickness / 2, r - thickness / 2
        pts = [
            (cx + ro * math.cos(rad0), cy + ro * math.sin(rad0)),
            (cx + ro * math.cos(rad1), cy + ro * math.sin(rad1)),
            (cx + ri * math.cos(rad1), cy + ri * math.sin(rad1)),
            (cx + ri * math.cos(rad0), cy + ri * math.sin(rad0)),
        ]
        pygame.draw.polygon(surf, col, pts)


# ─── dashed strokes ──────────────────────────────────────────────────


def dashed_line(surf, p0, p1, color, *, dash=4, gap=4, width=1):
    """Draw a dashed segment from point p0 to point p1 (any orientation).

    The final dash is clipped to the endpoint so the stroke never overshoots.
    """
    import math
    x0, y0 = p0
    x1, y1 = p1
    dx, dy = x1 - x0, y1 - y0
    dist = math.hypot(dx, dy) or 1.0
    ux, uy = dx / dist, dy / dist
    step = dash + gap
    n = int(dist // step) + 1
    for i in range(n):
        s = i * step
        e = min(s + dash, dist)
        if s >= dist:
            break
        sx, sy = x0 + ux * s, y0 + uy * s
        ex, ey = x0 + ux * e, y0 + uy * e
        pygame.draw.line(surf, color, (sx, sy), (ex, ey), width)


def dashed_hline(surf, x0, x1, y, color, *, dash=4, gap=4, width=1):
    """Draw a horizontal dashed rule from x0 to x1 at height y."""
    x = x0
    while x < x1:
        pygame.draw.line(surf, color, (x, y), (min(x + dash, x1), y), width)
        x += dash + gap


# ─── text chip (status-bar style) ────────────────────────────────────


def chip(text, color, *, mono: bool = True, dot: bool = False) -> pygame.Surface:
    """
    Status-bar style text chip (mono by default). `dot=True` prepends a small
    filled status dot drawn with pygame (the bundled fonts lack the ● glyph,
    so we never put it in a string).
    """
    style = t.Type.META_SM if mono else t.Type.CAPTION
    lbl = fonts.render(style, text, color)
    if not dot:
        return lbl
    dr, gap = 3, 6
    s = pygame.Surface((dr * 2 + gap + lbl.get_width(), lbl.get_height()),
                       pygame.SRCALPHA)
    pygame.draw.circle(s, color, (dr, s.get_height() // 2), dr)
    s.blit(lbl, (dr * 2 + gap, 0))
    return s


# ─── overlay underlay ────────────────────────────────────────────────


def draw_underlay(surf, underlay):
    """Draw the underlay screen if present (no scrim — callers add their own)."""
    if underlay is not None:
        underlay.draw(surf)


# ─── text wrap (character-count) ─────────────────────────────────────


def wrap_chars(text, width):
    """Naive word-wrap to `width` chars per line. Returns a list of lines."""
    words = text.split()
    lines, cur = [], ""
    for word in words:
        if len(cur) + len(word) + 1 <= width:
            cur = (cur + " " + word).strip()
        else:
            lines.append(cur)
            cur = word
    if cur:
        lines.append(cur)
    return lines
