"""
B1 — BIOS files scanner.

Recreates the Claude Design handoff (B1-bios.html) in pygame. A
consumer-first "are my BIOS files in place?" console that groups the
required firmware files by system (PlayStation 1, 3DO, Sega Saturn,
Neo Geo …) and tells the owner, per file, whether it is Ready, Missing,
Optional, or has the Wrong checksum — using human words and never a
download link. The product only ever says *where to put a file*
(/roms/bios), never "go download X".

  status bar : back chevron + "BIOS files" · battery · clock
  hero       : warning glyph + "N BIOS files missing" + ready/missing tally
  list       : per-system cards (icon + name + short tag + status badge),
               each holding a two-column grid of file rows with a status
               glyph (mint check / rose x / peach warning / muted circle)
  help strip : cyan note — "Put missing BIOS files in /roms/bios …"
  footer     : A show path · X copy report · B back

Class ⇄ handoff:
  B1Bios ⇄ B1-bios.html

Screen shape mirrors the other modules: 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 the
screen + local helpers; it never edits main.py or widgets.py.
"""

from __future__ import annotations

import pygame

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

SAFE = t.Space.SAFE_AREA
STAGE_W = t.VIEWPORT_WIDTH
STAGE_H = t.VIEWPORT_HEIGHT
BODY_W = STAGE_W - SAFE * 2  # 608

# fixed regions (status bar 48, footer 32)
BODY_TOP = t.Space.STATUS_BAR_HEIGHT          # 48
BODY_BOTTOM = STAGE_H - t.Space.FOOTER_HINT_HEIGHT  # 448

# ── per-file status vocabulary ───────────────────────────────────────
# Maps a file's state to (glyph, color, dim). Consumer language only; the
# label text comes from the data so we never bake a download verb in here.
_FILE_STATE = {
    "ok":   ("ph-check",          t.Color.SUCCESS,      False),
    "err":  ("ph-x",             t.Color.ERROR,        False),
    "warn": ("ph-warning-circle", t.Color.ACCENT_PEACH, False),
    "opt":  ("ph-circle",         t.Color.TEXT_MUTED,   True),
}

# Badge color triad for the per-system summary pill.
_BADGE = {
    "ok":   t.Color.SUCCESS,
    "warn": t.Color.ACCENT_PEACH,
    "err":  t.Color.ERROR,
}


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


def _badge_pill(text: str, level: str) -> pygame.Surface:
    """
    Tiny mono status pill (ready / N missing / wrong checksum) tinted by
    level ∈ {ok, warn, err}. Matches .sys-card .top .badge.
    """
    col = _BADGE.get(level, t.Color.TEXT_SECONDARY)
    lab = fonts.render(t.Type.META_SM, text, col)
    pw, ph = lab.get_width() + 16, 17
    pill = pygame.Surface((pw, ph), pygame.SRCALPHA)
    pygame.draw.rect(pill, t.with_alpha(col, 0.14), (0, 0, pw, ph),
                     border_radius=999)
    pygame.draw.rect(pill, t.with_alpha(col, 0.38), (0, 0, pw, ph),
                     width=1, border_radius=999)
    pill.blit(lab, ((pw - lab.get_width()) // 2, (ph - lab.get_height()) // 2))
    return pill


def _file_row(surf, x, y, max_w, name: str, state: str):
    """
    One file entry: status glyph + name (ellipsised). Mirrors .files .file —
    a mono line whose glyph + name color encode the state. Optional files
    render dimmed. `name` already carries its human suffix ("· missing",
    "· optional", "· re-download", "· ok") from the data.
    """
    glyph, col, dim = _FILE_STATE.get(state, _FILE_STATE["opt"])
    gh = 11
    ic = icons.icon(glyph, gh, col)
    name_col = (t.Color.ERROR if state == "err" else t.Color.TEXT_SECONDARY)
    ns = fonts.render(t.Type.META_SM, name, name_col)
    avail = max_w - gh - 8
    if ns.get_width() > avail:
        ns = ns.subsurface((0, 0, max(avail, 1), ns.get_height()))
    row_h = max(gh, ns.get_height())
    chunk = pygame.Surface((max_w, row_h), pygame.SRCALPHA)
    chunk.blit(ic, (0, (row_h - gh) // 2))
    chunk.blit(ns, (gh + 8, (row_h - ns.get_height()) // 2))
    if dim:
        chunk.set_alpha(int(255 * 0.6))
    surf.blit(chunk, (x, y))
    return row_h


def _sys_card_height(card) -> int:
    """
    Resolve a system card's pixel height from its file count. Header band
    (26px chip + padding) + a two-column file grid (ceil(n/2) rows).
    """
    pad_t, pad_b = 10, 10
    head_h = 26
    rows = (len(card["files"]) + 1) // 2
    grid_h = rows * _FILE_LINE_H - _FILE_ROW_GAP if rows else 0
    grid_top_gap = 8 if rows else 0
    return pad_t + head_h + grid_top_gap + grid_h + pad_b


_FILE_LINE_H = 16          # per file row stride (line + gap)
_FILE_ROW_GAP = 4
_FILE_COL_GAP = 10


def _sys_card(surf, rect, card, *, focused=False):
    """
    A per-system group card: icon chip + system name + short tag + status
    badge across the top, then a two-column grid of file rows. Mirrors
    .sys-card. Focused gets the elevated gradient + signature glow border.
    """
    radius = t.Radius.BUTTON
    if focused:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_ELEVATED,
                                       t.Color.SURFACE_BASE, radius, 160)
    else:
        body = render.rounded_gradient((rect.w, rect.h), t.Color.SURFACE_BASE,
                                       t.Color.BG_MID, radius, 160)
        pygame.draw.rect(body, t.Color.BORDER_SOFT, (0, 0, rect.w, rect.h),
                         width=1, border_radius=radius)

    pad_x, pad_t = 12, 10
    # ── header: icon chip + label(+ tag) + badge ──
    box = 26
    chip = pygame.Surface((box, box), pygame.SRCALPHA)
    pygame.draw.rect(chip, t.with_alpha((255, 255, 255), 0.04), (0, 0, box, box),
                     border_radius=7)
    pygame.draw.rect(chip, t.Color.BORDER_SOFT, (0, 0, box, box),
                     width=1, border_radius=7)
    gic = icons.icon(card.get("icon", "ph-game-controller"), 14,
                     t.Color.TEXT_SECONDARY)
    chip.blit(gic, ((box - 14) // 2, (box - 14) // 2))
    body.blit(chip, (pad_x, pad_t))

    head_cy = pad_t + box // 2
    # badge pinned right (measured first so the label can clip before it)
    pill = _badge_pill(card["badge"], card["badge_level"])
    pill_x = rect.w - pad_x - pill.get_width()
    body.blit(pill, (pill_x, head_cy - pill.get_height() // 2))

    # label + small mono tag (e.g. "PSX") sitting to its right
    lx = pad_x + box + 10
    lab = fonts.render(t.TextStyle(t.INTER_SEMIBOLD, 13, "SemiBold", -0.1),
                       card["label"], t.Color.TEXT_PRIMARY)
    tag = card.get("tag")
    tag_s = fonts.render(t.Type.META_SM, tag, t.Color.TEXT_MUTED) if tag else None
    label_limit = pill_x - 8 - lx
    lab_w = lab.get_width()
    tag_w = (tag_s.get_width() + 8) if tag_s else 0
    if lab_w + tag_w > label_limit:
        lab_w = max(1, label_limit - tag_w)
        lab = lab.subsurface((0, 0, lab_w, lab.get_height()))
    body.blit(lab, (lx, head_cy - lab.get_height() // 2))
    if tag_s:
        body.blit(tag_s, (lx + lab.get_width() + 8,
                          head_cy - tag_s.get_height() // 2 + 1))

    # ── file grid (two columns) ──
    grid_y = pad_t + box + 8
    col_w = (rect.w - pad_x * 2 - _FILE_COL_GAP) // 2
    for i, (name, state) in enumerate(card["files"]):
        col, row = i % 2, i // 2
        fx = pad_x + col * (col_w + _FILE_COL_GAP)
        fy = grid_y + row * _FILE_LINE_H
        _file_row(body, fx, fy, col_w, name, state)

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


def _hero(surf, y, *, title, sub_segments, ready, missing):
    """
    B1 hero band: rose(error)-tinted warning glyph + title + colored subline,
    and a right-aligned ready/missing tally. Returns the divider baseline y.
    Mirrors .b-hero.
    """
    box = 44
    chip = pygame.Surface((box, box), pygame.SRCALPHA)
    pygame.draw.rect(chip, t.with_alpha(t.Color.ERROR, 0.16), (0, 0, box, box),
                     border_radius=11)
    pygame.draw.rect(chip, t.with_alpha(t.Color.ERROR, 0.40), (0, 0, box, box),
                     width=1, border_radius=11)
    gic = icons.icon("ph-warning", 24, t.Color.ERROR)
    chip.blit(gic, ((box - 24) // 2, (box - 24) // 2))
    surf.blit(chip, (SAFE, y))

    # right tally: big "ready / missing", small caption under it
    rx = STAGE_W - SAFE
    big_style = t.TextStyle(t.SPACE_GROTESK_BOLD, 22, "Bold", -0.4)
    ready_s = fonts.render(big_style, str(ready), t.Color.SUCCESS)
    slash_s = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 14, "Bold", 0.0),
                           "/", t.Color.TEXT_MUTED)
    miss_s = fonts.render(big_style, str(missing), t.Color.ERROR)
    big_w = ready_s.get_width() + 4 + slash_s.get_width() + 4 + miss_s.get_width()
    bx = rx - big_w
    by = y + 2
    surf.blit(ready_s, (bx, by))
    bx2 = bx + ready_s.get_width() + 4
    surf.blit(slash_s, (bx2, by + (ready_s.get_height() - slash_s.get_height())))
    bx2 += slash_s.get_width() + 4
    surf.blit(miss_s, (bx2, by))
    cap = fonts.render(t.Type.META_SM, "ready / missing", t.Color.TEXT_MUTED)
    surf.blit(cap, (rx - cap.get_width(), by + ready_s.get_height() + 3))
    tally_right = rx - big_w

    # meta (title + subline) fills the middle between the chip and the tally
    tx = SAFE + box + 14
    title_s = fonts.render(t.TextStyle(t.SPACE_GROTESK_BOLD, 20, "Bold", -0.5),
                           title, t.Color.TEXT_PRIMARY)
    tmax = tally_right - 14 - tx
    if title_s.get_width() > tmax:
        title_s = title_s.subsurface((0, 0, max(tmax, 1), title_s.get_height()))
    surf.blit(title_s, (tx, y + 1))

    sy = y + 1 + title_s.get_height() + 5
    sx = tx
    for text, color in sub_segments:
        seg = fonts.render(t.Type.BODY_SM, text, color)
        if sx + seg.get_width() > tally_right - 14:
            seg = seg.subsurface(
                (0, 0, max(1, tally_right - 14 - sx), seg.get_height()))
            surf.blit(seg, (sx, sy))
            break
        surf.blit(seg, (sx, sy))
        sx += seg.get_width()

    div_y = y + box + 8
    pygame.draw.line(surf, t.Color.BORDER_SOFT, (SAFE, div_y),
                     (STAGE_W - SAFE, div_y), 1)
    return div_y


def _help_strip(surf, rect, *, path):
    """
    Cyan-tinted "where to put it" note. The path is shown as an inline mono
    code chip; the product never tells the owner to download anything.
    Mirrors .help-strip.
    """
    body = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
    pygame.draw.rect(body, t.with_alpha(t.Color.ACCENT_CYAN, 0.06),
                     (0, 0, rect.w, rect.h), border_radius=t.Radius.BUTTON)
    pygame.draw.rect(body, t.with_alpha(t.Color.ACCENT_CYAN, 0.30),
                     (0, 0, rect.w, rect.h), width=1, border_radius=t.Radius.BUTTON)
    pad = 12
    cy = rect.h // 2
    ic = icons.icon("ph-info", 13, t.Color.ACCENT_CYAN)
    body.blit(ic, (pad, cy - ic.get_height() // 2))
    x = pad + ic.get_width() + 10

    lead = fonts.render(t.TextStyle(t.INTER_SEMIBOLD, 11, "SemiBold", 0.0),
                        "Put missing BIOS files in ", t.Color.TEXT_PRIMARY)
    body.blit(lead, (x, cy - lead.get_height() // 2))
    x += lead.get_width()
    # mono code chip for the path
    code = fonts.render(t.Type.META_SM, path, t.Color.ACCENT_CYAN)
    chip_w, chip_h = code.get_width() + 10, code.get_height() + 4
    chip = pygame.Surface((chip_w, chip_h), pygame.SRCALPHA)
    pygame.draw.rect(chip, t.with_alpha(t.Color.ACCENT_CYAN, 0.10),
                     (0, 0, chip_w, chip_h), border_radius=4)
    chip.blit(code, (5, (chip_h - code.get_height()) // 2))
    body.blit(chip, (x, cy - chip_h // 2))
    x += chip_w + 6
    tail = fonts.render(t.Type.BODY_SM, "· checked automatically",
                        t.Color.TEXT_SECONDARY)
    if x + tail.get_width() <= rect.w - pad:
        body.blit(tail, (x, cy - tail.get_height() // 2))
    surf.blit(body, rect.topleft)


# ═══════════════════════════════════════════════════════════════════════
# B1 — BIOS files scanner
# ═══════════════════════════════════════════════════════════════════════


class B1Bios:
    """Per-system BIOS scanner grouped by console; consumer-first verdicts."""
    name = "bios"

    def __init__(self, app):
        self.app = app
        self.focus = 0
        # scroll offset (px) into the system list, recomputed each draw to
        # keep the focused card on screen.
        self._scroll = 0

        # Hero tally + line.
        self.ready_count = 15
        self.missing_count = 3
        self.hero_title = "3 BIOS files missing"
        self.hero_sub = [
            ("PS1, 3DO and Saturn won't run yet · ", t.Color.TEXT_SECONDARY),
            ("15 of 18 ready", t.Color.SUCCESS),
        ]
        self.bios_path = "/roms/bios"

        # Per-system groups. Each file is (human_name, state) where state ∈
        # {ok, err, warn, opt}. Names already carry consumer suffixes and no
        # download verbs — the help strip says where to put a file.
        self.systems = [
            dict(label="PlayStation 1", tag="PSX", icon="ph-game-controller",
                 badge="1 missing", badge_level="err", files=[
                     ("scph5500.bin · JP · missing", "err"),
                     ("scph5501.bin · US · missing", "err"),
                     ("scph5502.bin · EU · missing", "err"),
                     ("scph7001.bin · optional", "opt"),
                 ]),
            dict(label="3DO Interactive", tag="3DO", icon="ph-game-controller",
                 badge="2 missing", badge_level="err", files=[
                     ("panafz1.bin · missing", "err"),
                     ("panafz10.bin · missing", "err"),
                 ]),
            dict(label="Sega Saturn", tag="SAT", icon="ph-game-controller",
                 badge="wrong checksum", badge_level="warn", files=[
                     ("saturn_bios.bin · re-download", "warn"),
                     ("mpr-17933.bin · ok", "ok"),
                 ]),
            dict(label="Neo Geo", tag="NEOGEO", icon="ph-game-controller",
                 badge="ready", badge_level="ok", files=[
                     ("neogeo.zip · ok", "ok"),
                 ]),
        ]

    # ── input ─────────────────────────────────────────────────────────
    def on_action(self, action):
        n = len(self.systems)
        if action in ("down", "right"):
            self.focus = min(n - 1, self.focus + 1)
        elif action in ("up", "left"):
            self.focus = max(0, self.focus - 1)
        elif action == "a":
            sysm = self.systems[self.focus]
            self.app.toast(f"{sysm['label']} — put files in {self.bios_path}")
        elif action == "x":
            self.app.toast("BIOS report copied")
        elif action == "b":
            self.app.pop()

    def update(self, dt_ms):
        pass

    # ── draw ──────────────────────────────────────────────────────────
    def draw(self, surf):
        w.status_bar(surf, title="BIOS files", back=True)

        div_y = _hero(surf, 56, title=self.hero_title, sub_segments=self.hero_sub,
                      ready=self.ready_count, missing=self.missing_count)

        # help strip is pinned to the bottom of the body; the list scrolls in
        # the space between the hero divider and the strip.
        help_h = 38
        help_y = BODY_BOTTOM - 8 - help_h
        list_top = div_y + 10
        list_bottom = help_y - 8
        view_h = list_bottom - list_top

        gap = 6
        heights = [_sys_card_height(s) for s in self.systems]
        # y-offset of each card within the (un-scrolled) content column
        offsets = []
        acc = 0
        for h in heights:
            offsets.append(acc)
            acc += h + gap
        content_h = acc - gap if heights else 0

        # keep focused card fully visible
        f_top = offsets[self.focus]
        f_bot = f_top + heights[self.focus]
        if f_top - self._scroll < 0:
            self._scroll = f_top
        elif f_bot - self._scroll > view_h:
            self._scroll = f_bot - view_h
        max_scroll = max(0, content_h - view_h)
        self._scroll = max(0, min(self._scroll, max_scroll))

        prev_clip = surf.get_clip()
        surf.set_clip(pygame.Rect(SAFE, list_top, BODY_W, view_h))
        for i, sysm in enumerate(self.systems):
            cy = list_top + offsets[i] - self._scroll
            if cy + heights[i] < list_top or cy > list_bottom:
                continue
            rect = pygame.Rect(SAFE, cy, BODY_W, heights[i])
            _sys_card(surf, rect, sysm, focused=(self.focus == i))
        surf.set_clip(prev_clip)

        # scroll affordance: a faint caret when more cards lie below
        if self._scroll < max_scroll:
            car = icons.icon("ph-caret-down", 12, t.Color.TEXT_MUTED)
            surf.blit(car, (STAGE_W // 2 - car.get_width() // 2,
                            list_bottom - car.get_height()))

        _help_strip(surf, pygame.Rect(SAFE, help_y, BODY_W, help_h),
                    path=self.bios_path)

        w.footer_hints(surf, [("A", "show path"), ("X", "copy report"),
                              ("B", "back")])
