"""
Discovery-shell components on the Ink palette: tiles, rails, the Clerk's
eyebrow/quote, box art, meta lines and the primary/ghost CTAs. The art is the
hero; the only chrome color is the rose→cyan focus signature.
"""

from __future__ import annotations

from functools import lru_cache

import pygame

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

SAFE = t.Space.SAFE_AREA


@lru_cache(maxsize=4)
def _bottom_fade(w: int, h: int) -> pygame.Surface:
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    bg = t.Color.BG_DEEP
    for y in range(h):
        s.fill((bg[0], bg[1], bg[2], int(255 * y / max(1, h - 1))), (0, y, w, 1))
    return s


def scroll_fade(surf, *, bottom, h=22, w=None):
    """Fade scrolling content into the background just above the footer, so a
    half-row peeking under the fold reads as 'more below', not a hard cut."""
    w = w or t.VIEWPORT_WIDTH
    surf.blit(_bottom_fade(w, h), (0, bottom - h))


@lru_cache(maxsize=32)
def _scrim(w: int, h: int, a_top: int, a_bot: int) -> pygame.Surface:
    """Vertical BG_DEEP alpha gradient (a_top→a_bot) — a legibility scrim so
    overlay text reads over CRISP art (no blur needed)."""
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    bg = t.Color.BG_DEEP
    span = max(1, h - 1)
    for y in range(h):
        a = int(a_top + (a_bot - a_top) * y / span)
        if a > 0:
            s.fill((bg[0], bg[1], bg[2], a), (0, y, w, 1))
    return s


def draw_focused_card(surf, rect, radius, *, scale=1.0, base_surface=None):
    """Focus treatment: rose→cyan outer glow + gradient border (the signature)."""
    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)
            surf.blit(pygame.transform.smoothscale(
                render.gradient_border((rect.w, rect.h), radius), (sw, sh)), pos)
        else:
            surf.blit(base_surface, rect.topleft)
            surf.blit(render.gradient_border((rect.w, rect.h), radius), rect.topleft)
    else:
        surf.blit(render.gradient_border((rect.w, rect.h), radius), rect.topleft)


# ── text helpers ──────────────────────────────────────────────────────


def first_sentence(text: str, *, cap: int = 120) -> str:
    if not text:
        return ""
    s = text.strip()
    for end in (". ", "! ", "? "):
        i = s.find(end)
        if 0 < i <= cap:
            return s[: i + 1].strip()
    return (s[:cap].rstrip() + "…") if len(s) > cap else s


def wrap_text(style, text: str, max_w: int) -> list[str]:
    """Greedy word-wrap to pixel width using the real font metrics."""
    words = (text or "").split()
    lines, cur = [], ""
    for word in words:
        trial = (cur + " " + word).strip()
        if fonts.measure(style, trial)[0] <= max_w or not cur:
            cur = trial
        else:
            lines.append(cur)
            cur = word
    if cur:
        lines.append(cur)
    return lines


def draw_paragraph(surf, style, text, x, y, max_w, color, *, line_h, max_lines=None):
    lines = wrap_text(style, text, max_w)
    if max_lines:
        lines = lines[:max_lines]
    for i, ln in enumerate(lines):
        surf.blit(fonts.render(style, ln, color), (x, y + i * line_h))
    return y + len(lines) * line_h


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


def clerk_eyebrow(surf, x, y, text, *, pill=False, color=t.Color.ACCENT_ROSE):
    """
    Rose all-caps eyebrow with a dot — the Clerk's signature. `pill=True` lays a
    light near-black rounded chip behind it so it stays readable over bright art.
    `color` overrides the accent (e.g. peach on the source screen).
    """
    dot_d = 4
    lab = fonts.render(t.Type.EYEBROW, text, color, upper=True)
    h = lab.get_height()
    content_w = dot_d + 6 + lab.get_width()
    if pill:
        px, py = 8, 4
        pr = pygame.Rect(x - px, y - py, content_w + px * 2, h + py * 2)
        chip = pygame.Surface((pr.w, pr.h), pygame.SRCALPHA)
        pygame.draw.rect(chip, t.with_alpha(t.Color.BG_DEEP, 0.58),
                         (0, 0, pr.w, pr.h), border_radius=t.Radius.PILL)
        pygame.draw.rect(chip, t.with_alpha(t.Color.TEXT_PRIMARY, 0.10),
                         (0, 0, pr.w, pr.h), width=1, border_radius=t.Radius.PILL)
        surf.blit(chip, pr.topleft)
    cy = y + h // 2
    pygame.draw.circle(surf, color, (x + dot_d // 2, cy), dot_d // 2 + 1)
    surf.blit(lab, (x + dot_d + 6, y))
    return h


def fit_meta(segments, max_w):
    """Drop trailing meta segments until 'a · b · c' fits `max_w` px (always
    keeps the first, e.g. the system). Stops long system+genre combos from
    spilling the meta/rating past the hero text panel onto the art."""
    segs = [s for s in segments if s]
    sep = fonts.measure(t.Type.CAPTION, " · ")[0]

    def w(ss):
        return (sum(fonts.measure(t.Type.CAPTION, str(s))[0] for s in ss)
                + sep * max(0, len(ss) - 1))

    while len(segs) > 1 and w(segs) > max_w:
        segs = segs[:-1]
    return segs


def meta_line(surf, x, y, segments, *, color=t.Color.TEXT_MUTED):
    """'SNES · 1995 · Squaresoft' with muted dot separators."""
    sx = x
    for i, seg in enumerate(segments):
        if not seg:
            continue
        if i > 0:
            d = fonts.render(t.Type.CAPTION, " · ", t.Color.TEXT_DISABLED)
            surf.blit(d, (sx, y)); sx += d.get_width()
        s = fonts.render(t.Type.CAPTION, str(seg), color)
        surf.blit(s, (sx, y)); sx += s.get_width()
    return sx


def gradient_bar(h: int, w: int = 2) -> pygame.Surface:
    return render.linear_gradient((w, h), t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, 0.0)


def clerk_quote(surf, x, y, text, max_w, *, max_lines=3, bar=True,
                style=t.Type.CLERK, color=t.Color.TEXT_SECONDARY, line_h=18):
    """Wrapped quote. `bar=True` draws the rose→cyan left bar (game card);
    `bar=False` is a plain paragraph (v3 hero)."""
    pad = 12 if bar else 0
    lines = wrap_text(style, text, max_w - pad)[:max_lines]
    h = max(line_h * len(lines), line_h)
    if bar:
        surf.blit(gradient_bar(h), (x, y + 2))
    for i, ln in enumerate(lines):
        surf.blit(fonts.render(style, ln, color), (x + pad, y + i * line_h))
    return h


# ── the ONE poster tile — used by every grid/rail so tiles behave identically ──


def rating_badge(rating):
    """Rating number for the star pill (e.g. '4.6'), or None. The star itself is
    drawn as an icon by poster_tile (the ★ glyph isn't in the font)."""
    return f"{rating:.1f}" if rating else None


# long system names that would overflow the poster pill → compact forms
_SYS_SHORT = {"Master System": "Master Sys", "Game Boy Color": "GB Color",
              "Game Boy Advance": "GBA"}


def sys_year(g):
    """'SNES · 1995' from a game dict (skips missing parts); long system names
    are shortened so the pill never spills past the poster edge."""
    s = g.get("system")
    s = _SYS_SHORT.get(s, s)
    return " · ".join(str(v) for v in [s, g.get("year")] if v)


def _pill(text, style, fg, *, accent=None, icon=None, icon_color=None):
    """Rounded chip: optional leading icon + text on translucent near-black."""
    lab = fonts.render(style, text, fg)
    ic = icons.icon(icon, lab.get_height(), icon_color or fg) if icon else None
    pad = 6
    iw = (ic.get_width() + 4) if ic else 0
    w, h = pad * 2 + iw + lab.get_width(), lab.get_height() + 4
    s = pygame.Surface((w, h), pygame.SRCALPHA)
    pygame.draw.rect(s, t.with_alpha(t.Color.BG_DEEP, 0.62), (0, 0, w, h),
                     border_radius=t.Radius.PILL)
    if accent:
        pygame.draw.rect(s, t.with_alpha(accent, 0.45), (0, 0, w, h), width=1,
                         border_radius=t.Radius.PILL)
    x = pad
    if ic:
        s.blit(ic, (x, (h - ic.get_height()) // 2)); x += ic.get_width() + 4
    s.blit(lab, (x, 2))
    return s


def poster_tile(surf, rect, *, path, name, badge=None, badge_r=None, sub=None,
                chips=None, focused=False, dim=False, name_below=True):
    """
    The single, consistent tile for every screen. On focus the cover LIFTS, stays
    bright/crisp with the rose→cyan frame, and meta pills ride the art: ★rating
    (`badge`) + system·year (`badge_r`) top-left, and the `chips` — (text, color)
    pairs like genre / playtime — stacked over a scrim at the BOTTOM of the
    poster, each on its own line so nothing is cut. The NAME stays in its slot
    BELOW the card (outside the art), marqueeing if long; an optional `sub` (e.g.
    a collection's game count) sits under it. Neighbours dim. Everything but
    path/name is optional, so collection / series cards reuse the same look.
    """
    # ---- the art (resting flat; focused lifts with the overlaid meta pills) ----
    if not focused:
        img = images.cover(path, rect.w, rect.h, radius=3, mode="cover")
        if dim:
            img = img.copy(); img.set_alpha(110)        # neighbours clearly recede
        surf.blit(img, rect.topleft)
    else:
        grow = 1.07
        gw, gh = int(rect.w * grow), int(rect.h * grow)
        gx, gy = rect.centerx - gw // 2, rect.y - 6 - (gh - rect.h) // 2
        fr = pygame.Rect(gx, gy, gw, gh)
        sh = pygame.Surface((gw + 18, gh + 18), pygame.SRCALPHA)
        pygame.draw.rect(sh, (0, 0, 0, 150), (9, 13, gw, gh), border_radius=6)
        surf.blit(sh, (gx - 9, gy - 9))
        surf.blit(images.cover(path, gw, gh, radius=3, mode="cover"), fr.topleft)
        surf.blit(render.gradient_border((gw, gh), 3), fr.topleft)
        if gw >= 100:                                   # tiny tiles skip the pills
            py = fr.y + 7
            if badge_r:                                  # system·year on top…
                p = _pill(_ellipsize(t.Type.TILE_SUB, badge_r, fr.w - 26),
                          t.Type.TILE_SUB, t.Color.TEXT_PRIMARY)
                surf.blit(p, (fr.x + 7, py)); py += p.get_height() + 4
            if badge:                                    # …rating under it
                p = _pill(badge, t.Type.TILE_SUB, t.Color.ACCENT_PEACH, icon="ph-star")
                surf.blit(p, (fr.x + 7, py)); py += p.get_height() + 4
            # genre / playtime chips sit at the BOTTOM of the poster, on a scrim
            cs = [_pill(str(x), t.Type.TILE_SUB, col) for x, col in (chips or ()) if x]
            if cs:
                tot = sum(c.get_height() for c in cs) + 2 * (len(cs) - 1)
                surf.blit(_scrim(gw, tot + 18, 0, 205), (fr.x, fr.bottom - tot - 18))
                cy = fr.bottom - 9 - tot
                for c in cs:
                    surf.blit(c, (fr.x + 7, cy)); cy += c.get_height() + 2

    # ---- below the card (outside the art): name, then an optional sub ----
    if name_below and name:
        by = rect.bottom + 4
        if focused:
            marquee(surf, t.Type.TILE_NAME, name, rect.x + 2, by, rect.w, t.Color.TEXT_PRIMARY)
        else:
            ncol = t.Color.TEXT_MUTED if dim else t.Color.TEXT_SECONDARY
            surf.blit(fonts.render(t.Type.TILE_NAME, _ellipsize(t.Type.TILE_NAME, name, rect.w),
                      ncol), (rect.x + 2, by))
        if sub:
            scol = t.Color.TEXT_DISABLED if dim else t.Color.TEXT_MUTED
            surf.blit(fonts.render(t.Type.TILE_SUB, _ellipsize(t.Type.TILE_SUB, sub, rect.w),
                      scol), (rect.x + 2, by + 18))


# ── right-edge A–Z fast-jump index ──


def az_index(surf, *, top, bottom, current=None):
    """Thin A–Z column flush to the right edge; `current` letter highlighted."""
    import string
    x = t.VIEWPORT_WIDTH - 18
    pygame.draw.line(surf, t.Color.BORDER_SOFT, (x, top), (x, bottom), 1)
    letters = ["#"] + list(string.ascii_uppercase)
    n = len(letters)
    avail = bottom - top - 12
    step = avail / (n - 1)
    cur = (current or "").upper()[:1]
    for i, ch in enumerate(letters):
        ly = top + 6 + int(i * step)
        on = (ch == cur)
        col = t.Color.TEXT_PRIMARY if on else t.Color.TEXT_DISABLED
        s = fonts.render(t.Type.TILE_SUB, ch, col)
        surf.blit(s, (x + 9 - s.get_width() // 2, ly - s.get_height() // 2))
        if on:
            surf.blit(render.linear_gradient((3, 12), t.Color.ACCENT_ROSE, t.Color.ACCENT_CYAN, 0.0),
                      (x - 4, ly - 6))


def star_rating(surf, x, y, rating, *, size=12):
    """Up to 5 stars (peach filled, muted empty) for a 0–5 rating."""
    if rating is None:
        return 0
    full = int(round(rating))
    for i in range(5):
        col = t.Color.ACCENT_PEACH if i < full else t.Color.TEXT_DISABLED
        s = icons.icon("ph-star", size, col)
        surf.blit(s, (x + i * (size + 2), y))
    return 5 * (size + 2)


# ── box art ───────────────────────────────────────────────────────────


def box_art(surf, rect, path, system, *, radius=t.Radius.TILE):
    """Draw the cover whole (contain) at native aspect inside rect."""
    img = images.cover(path, rect.w, rect.h, radius=radius, mode="contain")
    surf.blit(img, rect.topleft)


# ── tile (rail item) ──────────────────────────────────────────────────


def tile(surf, rect, *, path, system, name, sub, focused=False, art_h=None):
    """
    A rail tile: art (native aspect, contain) + name + sub. The art box is
    `art_h` tall (defaults to most of rect, leaving room for two text lines).
    """
    art_h = art_h or (rect.h - 28)
    art_rect = pygame.Rect(rect.x, rect.y, rect.w, art_h)
    img = images.cover(path, art_rect.w, art_rect.h, radius=t.Radius.TILE, mode="cover")
    if focused:
        glow, gpos = render.focus_glow(art_rect, t.Radius.TILE)
        surf.blit(glow, gpos)
        surf.blit(img, art_rect.topleft)
        surf.blit(render.gradient_border((art_rect.w, art_rect.h), t.Radius.TILE),
                  art_rect.topleft)
    else:
        surf.blit(img, art_rect.topleft)
    name_col = t.Color.TEXT_PRIMARY
    nm = fonts.render(t.Type.TILE_NAME, _ellipsize(t.Type.TILE_NAME, name, rect.w), name_col)
    surf.blit(nm, (rect.x, art_rect.bottom + 5))
    sb = fonts.render(t.Type.TILE_SUB, sub, t.Color.TEXT_MUTED)
    surf.blit(sb, (rect.x, art_rect.bottom + 5 + nm.get_height() + 2))


def _ellipsize(style, text, max_w):
    if fonts.measure(style, text)[0] <= max_w:
        return text
    s = text
    while s and fonts.measure(style, s + "…")[0] > max_w:
        s = s[:-1]
    return (s.rstrip() + "…") if s else text


def marquee(surf, style, text, x, y, max_w, color, *, speed=0.032, gap=28):
    """Draw `text` at (x,y); if it's wider than `max_w`, scroll it as a ticker
    (clipped to max_w). Used for the FOCUSED item's name so long titles read in
    full instead of truncating. Phase comes from the global clock — no per-tile
    state, and only one item is focused at a time."""
    glyph = fonts.render(style, text, color)
    if glyph.get_width() <= max_w:
        surf.blit(glyph, (x, y))
        return
    period = glyph.get_width() + gap
    off = int(pygame.time.get_ticks() * speed) % period
    prev = surf.get_clip()
    region = pygame.Rect(x, y, max_w, glyph.get_height())
    surf.set_clip(region.clip(prev) if prev else region)
    surf.blit(glyph, (x - off, y))
    surf.blit(glyph, (x - off + period, y))      # wrap-around copy
    surf.set_clip(prev)


# ── buttons ───────────────────────────────────────────────────────────


def button_primary(surf, rect, label, *, focused=True):
    """White pill with an 'A' chip — the primary action. Glows when focused."""
    body = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
    pygame.draw.rect(body, t.Color.TEXT_PRIMARY, (0, 0, rect.w, rect.h),
                     border_radius=t.Radius.BUTTON)
    # 'A' chip
    chip_d = rect.h - 12
    cx, cyy = 6, (rect.h - chip_d) // 2
    pygame.draw.circle(body, t.Color.BG_DEEP,
                       (cx + chip_d // 2, cyy + chip_d // 2), chip_d // 2)
    a = fonts.render(t.Type.GP, "A", t.Color.TEXT_PRIMARY)
    body.blit(a, (cx + chip_d // 2 - a.get_width() // 2,
                  cyy + chip_d // 2 - a.get_height() // 2))
    lab = fonts.render(t.Type.BTN, label, t.Color.BG_DEEP)
    body.blit(lab, (cx + chip_d + 8, (rect.h - lab.get_height()) // 2))
    if focused:
        glow, gpos = render.focus_glow(rect, t.Radius.BUTTON)
        surf.blit(glow, gpos)
    surf.blit(body, rect.topleft)
    if focused:
        surf.blit(render.gradient_border((rect.w, rect.h), t.Radius.BUTTON), rect.topleft)


def button_ghost(surf, rect, label, *, icon_name=None, icon_color=None, focused=False):
    # dark glassy chip — reads on top of key-art without a translucent rectangle
    body = pygame.Surface((rect.w, rect.h), pygame.SRCALPHA)
    pygame.draw.rect(body, t.with_alpha(t.Color.BG_DEEP, 0.55),
                     (0, 0, rect.w, rect.h), border_radius=t.Radius.BUTTON)
    pygame.draw.rect(body, t.with_alpha(t.Color.TEXT_PRIMARY, 0.12),
                     (0, 0, rect.w, rect.h), width=1, border_radius=t.Radius.BUTTON)
    x = 11
    if icon_name:
        ic = icons.icon(icon_name, 12, icon_color or t.Color.TEXT_MUTED)
        body.blit(ic, (x, (rect.h - ic.get_height()) // 2))
        x += ic.get_width() + 7
    lab = fonts.render(t.Type.CAPTION, label, t.Color.TEXT_SECONDARY)
    body.blit(lab, (x, (rect.h - 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)


# ── rail header ───────────────────────────────────────────────────────


def rail_header(surf, x, y, title, aside=None, max_w=None):
    ts = fonts.render(t.Type.ROW_TITLE, title, t.Color.TEXT_PRIMARY)
    surf.blit(ts, (x, y))
    if aside:
        a = fonts.render(t.Type.TILE_SUB, aside, t.Color.TEXT_MUTED)
        ax = (x + max_w - a.get_width()) if max_w else (x + ts.get_width() + 10)
        surf.blit(a, (ax, y + ts.get_height() - a.get_height() - 1))
    return ts.get_height()
