"""
game.py — Main gameplay scene for Powernet.

Layout
------
  Top-left  : HUD (level, XP bar, power balance, stats)
  Middle    : scrollable tile grid (WASD / middle-mouse to pan)
  Bottom bar: tabs (Generators | Forschung | Wires) + Bulldoze
  Picker    : slides up from the bar; scrollable building cards
  Info panel: bottom-left when hovering a building
  Level-up  : centred notification when a new level is reached

States
------
  EXPLORE   – default; hover buildings to inspect
  PLACE     – building type selected; preview follows cursor
  WIRE_FROM – wires tab; click a building to begin a wire
  WIRE_TO   – wire start chosen; click a second building to connect
  BULLDOZE  – click a building or wire to remove it
  PAUSED    – ESC overlay with Resume / Save & Quit
"""

from __future__ import annotations

import re
import time
from pathlib import Path
from typing import Optional

import pygame

from helpers.game_data import (
    BUILDING_TYPES,
    CATEGORY_LABELS,
    CATEGORY_ORDER,
    MAX_LEVEL,
    XP_THRESHOLDS,
    GameState,
    PlacedBuilding,
    Wire,
)
from helpers.gui import UITheme
from helpers.world_data import WorldMeta

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------

_TEX_BASE = Path(__file__).parent.parent / "assets" / "textures" / "other"

# ---------------------------------------------------------------------------
# Visual constants
# ---------------------------------------------------------------------------

TILE_SIZE = 64          # pixels per grid cell

TOOLBAR_H    = 52       # bottom bar height
PICKER_H     = 210      # picker panel height when fully open
PICKER_SPEED = 10.0     # open/close animation speed (fraction/s)

ANIM_FPS = 5.0          # animation fps for multi-frame building textures

GRASS_A       = (34,  62,  28)
GRASS_B       = (38,  68,  32)
GRID_LINE_COL = (26,  48,  20)

PH_PURPLE = (100, 40, 160)
PH_BLACK  = (10,  10,  10)

WIRE_COLORS = [
    (232, 137,  12),   # 0 – orange
    ( 79, 195, 247),   # 1 – cyan
    (126, 211,  33),   # 2 – green
    (255, 100, 100),   # 3 – red
    (220, 220,  50),   # 4 – yellow
    (200,  80, 220),   # 5 – purple
]
WIRE_W      = 3
SHADOW_OFF  = 5

HUD_BG    = (8,  16, 28, 210)
PANEL_BG  = (10, 18, 32, 240)
CARD_BG   = (16, 28, 48)
CARD_LOCK = (8,  12, 20)

CAM_SPEED = 320.0   # px/s keyboard pan

# Toolbar layout
TAB_W   = 120
TAB_H   = 36
TAB_GAP = 6
TOOL_W  = 108

# Picker card layout
CARD_W      = 114
CARD_GAP    = 8
CARD_STRIDE = CARD_W + CARD_GAP
ARROW_W     = 36    # scroll arrow button width
PICKER_PAD  = 10    # picker horizontal padding

# Category accent colours
CAT_COLORS: dict[str, tuple[int, int, int]] = {
    "generators": (232, 137,  12),   # orange
    "consumers":  ( 79, 195, 247),   # cyan
    "research":   (126, 211,  33),   # green
    "wires":      (220, 220,  50),   # yellow
}

# Picker tabs: only generators, research, and wires (no consumers picker)
PICKER_TABS = ["generators", "research", "wires"]


# ---------------------------------------------------------------------------
# State enum
# ---------------------------------------------------------------------------

class _S:
    EXPLORE   = "explore"
    PLACE     = "place"
    WIRE_FROM = "wire_from"
    WIRE_TO   = "wire_to"
    BULLDOZE  = "bulldoze"
    PAUSED    = "paused"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _pt_seg_dist(px: float, py: float,
                 ax: float, ay: float,
                 bx: float, by: float) -> float:
    dx, dy = bx - ax, by - ay
    if dx == 0 and dy == 0:
        return ((px - ax) ** 2 + (py - ay) ** 2) ** 0.5
    t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)))
    return ((px - ax - t * dx) ** 2 + (py - ay - t * dy) ** 2) ** 0.5


def _fmt_mw(mw: float) -> str:
    abs_v = abs(mw)
    if abs_v < 1_000:
        return f"{mw:.1f} MW"
    elif abs_v < 1_000_000:
        return f"{mw / 1_000:.2f} GW"
    else:
        return f"{mw / 1_000_000:.2f} TW"


def _fmt_xp(xp: float) -> str:
    if xp < 1_000:
        return f"{xp:.0f}"
    elif xp < 1_000_000:
        return f"{xp / 1_000:.1f}k"
    return f"{xp / 1_000_000:.2f}M"


def _scale_fit(raw: pygame.Surface, tw: int, th: int) -> pygame.Surface:
    """Scale raw to fit tw×th maintaining aspect ratio, nearest-neighbor, centered."""
    rw, rh = raw.get_size()
    if rw == 0 or rh == 0:
        return raw
    scale = min(tw / rw, th / rh)
    nw = max(1, round(rw * scale))
    nh = max(1, round(rh * scale))
    scaled = pygame.transform.scale(raw, (nw, nh))
    if nw == tw and nh == th:
        return scaled
    result = pygame.Surface((tw, th), pygame.SRCALPHA)
    result.blit(scaled, ((tw - nw) // 2, (th - nh) // 2))
    return result


# ---------------------------------------------------------------------------
# Game scene
# ---------------------------------------------------------------------------

class Game:
    """Main in-game scene; returns to main.py via update() → 'back'."""

    def __init__(self, screen: pygame.Surface, meta: WorldMeta) -> None:
        self._screen = screen
        self._meta   = meta
        self._theme  = UITheme()

        # Load or initialise game state
        raw = getattr(meta, "game_data", {}) or {}
        self._gs = GameState.from_dict(raw) if raw else GameState()

        # Run world generation if this is a new game
        if not raw:
            self._world_gen()

        # Camera
        self._cam_x: float = self._gs.cam_x
        self._cam_y: float = self._gs.cam_y

        # Middle-mouse panning
        self._panning      = False
        self._pan_origin:  tuple[int, int]     = (0, 0)
        self._cam_origin:  tuple[float, float] = (0.0, 0.0)

        # Interaction state
        self._state      = _S.EXPLORE
        self._prev_state = _S.EXPLORE
        self._active_tab: Optional[str] = None

        # Picker animation
        self._picker_t:   float = 0.0
        self._picker_tgt: float = 0.0

        # Per-tab picker scroll offset (pixels)
        self._picker_scroll: dict[str, int] = {}

        # Tool / placement
        self._sel_type:   Optional[str] = None
        self._wire_src:   Optional[str] = None
        self._wire_layer: int           = 0

        # Hover & selection
        self._hov_cell:     Optional[tuple[int, int]] = None
        self._hov_building: Optional[PlacedBuilding]  = None
        self._hov_wire:     Optional[Wire]             = None
        self._sel_building: Optional[PlacedBuilding]  = None

        # Powered buildings (computed each frame via BFS)
        self._powered: set[str] = set()

        # Alert banner
        self._alert_text  = ""
        self._alert_timer = 0.0

        # Level-up notification
        self._levelup_timer: float = 0.0
        self._levelup_level: int   = self._gs.level()

        # Previous level (to detect level-up)
        self._prev_level: int = self._gs.level()

        # Quit flag
        self._want_quit = False

        # Animation timer (global, for all building textures)
        self._anim_timer: float = 0.0

        # Texture cache: type_id → list of scaled frames
        self._tex_cache: dict[str, list[pygame.Surface]] = {}

        # Scaled-preview cache: (type_id, w, h) → Surface
        self._scaled_cache: dict[tuple[str, int, int], pygame.Surface] = {}

        # Placeholder cache
        self._ph_cache: dict[tuple[int, int], pygame.Surface] = {}

        # Picker scroll arrow rects (computed per draw)
        self._picker_left_r:  Optional[pygame.Rect] = None
        self._picker_right_r: Optional[pygame.Rect] = None

        # Layout rects (built in _rebuild)
        self._tab_rects:  dict[str, pygame.Rect] = {}
        self._tool_rects: dict[str, pygame.Rect] = {}
        self._pause_rect:     Optional[pygame.Rect] = None
        self._pause_resume_r: Optional[pygame.Rect] = None
        self._pause_quit_r:   Optional[pygame.Rect] = None

        # Debug menu
        self._debug_open: bool = False
        self._debug_btn_rects: list[tuple[str, pygame.Rect]] = []

        self._rebuild()

        # Centre camera on grid (first load only)
        if self._gs.cam_x == 0.0 and self._gs.cam_y == 0.0:
            sw, sh = screen.get_size()
            gah    = sh - TOOLBAR_H
            ww     = self._gs.grid_w * TILE_SIZE
            wh     = self._gs.grid_h * TILE_SIZE
            self._cam_x = max(0.0, (ww - sw) / 2)
            self._cam_y = max(0.0, (wh - gah) / 2)

    # ------------------------------------------------------------------
    # World generation
    # ------------------------------------------------------------------

    def _world_gen(self) -> None:
        import random
        seed = getattr(self._meta, "seed", 0) or 1
        rng = random.Random(seed)
        gs = self._gs

        # Residential clusters
        for _ in range(rng.randint(4, 7)):
            cx = rng.randint(6, gs.grid_w - 10)
            cy = rng.randint(6, gs.grid_h - 10)
            for _ in range(rng.randint(3, 7)):
                self._try_place_near(rng, "bungalow", cx, cy, 6)
            for _ in range(rng.randint(1, 3)):
                self._try_place_near(rng, "house", cx, cy, 6)
            if rng.random() > 0.45:
                self._try_place_near(rng, "multifamily", cx, cy, 5)

        # Industrial clusters
        for _ in range(rng.randint(2, 4)):
            cx = rng.randint(6, gs.grid_w - 10)
            cy = rng.randint(6, gs.grid_h - 10)
            for _ in range(rng.randint(2, 4)):
                self._try_place_near(rng, "small_factory", cx, cy, 5)
            for _ in range(rng.randint(1, 2)):
                self._try_place_near(rng, "warehouse", cx, cy, 5)
            if rng.random() > 0.4:
                self._try_place_near(rng, "factory", cx, cy, 4)
            if rng.random() > 0.65:
                self._try_place_near(rng, "big_factory", cx, cy, 4)

    def _try_place_near(self, rng: object, type_id: str,
                        cx: int, cy: int, radius: int,
                        attempts: int = 25) -> None:
        import random
        td = BUILDING_TYPES.get(type_id)
        if td is None:
            return
        _rng: random.Random = rng  # type: ignore[assignment]
        for _ in range(attempts):
            gx = max(0, min(cx + _rng.randint(-radius, radius),
                            self._gs.grid_w - td.size[0]))
            gy = max(0, min(cy + _rng.randint(-radius, radius),
                            self._gs.grid_h - td.size[1]))
            b = self._gs.place_building(type_id, gx, gy)
            if b is not None:
                return

    # ------------------------------------------------------------------
    # Powered BFS
    # ------------------------------------------------------------------

    def _compute_powered(self) -> set[str]:
        """BFS from all generators through wires. Returns set of powered build_ids."""
        gs = self._gs

        # Build adjacency map
        adj: dict[str, set[str]] = {}
        for b in gs.buildings:
            adj[b.build_id] = set()
        for w in gs.wires:
            if w.from_id in adj and w.to_id in adj:
                adj[w.from_id].add(w.to_id)
                adj[w.to_id].add(w.from_id)

        # Seed BFS from all generators
        powered: set[str] = set()
        queue: list[str] = []
        for b in gs.buildings:
            td = BUILDING_TYPES.get(b.type_id)
            if td and td.power_mw > 0:
                powered.add(b.build_id)
                queue.append(b.build_id)

        head = 0
        while head < len(queue):
            bid = queue[head]
            head += 1
            for nid in adj.get(bid, set()):
                if nid not in powered:
                    powered.add(nid)
                    queue.append(nid)

        return powered

    # ------------------------------------------------------------------
    # Layout rebuild
    # ------------------------------------------------------------------

    def _rebuild(self) -> None:
        sw, sh = self._screen.get_size()
        self._sw = sw
        self._sh = sh

        # Toolbar
        ty = sh - TOOLBAR_H
        self._tab_rects = {}
        for i, tab in enumerate(PICKER_TABS):
            x = PICKER_PAD + i * (TAB_W + TAB_GAP)
            self._tab_rects[tab] = pygame.Rect(x, ty + 8, TAB_W, TAB_H)

        sep_x = PICKER_PAD + len(PICKER_TABS) * (TAB_W + TAB_GAP) + 4
        self._tool_rects = {
            "bulldoze": pygame.Rect(sep_x + 8, ty + 8, TOOL_W, TAB_H),
        }

        # Pause menu
        pw, ph = 360, 230
        px = (sw - pw) // 2
        py = (sh - ph) // 2
        self._pause_rect     = pygame.Rect(px, py, pw, ph)
        self._pause_resume_r = pygame.Rect(px + 40, py + 75,  pw - 80, 44)
        self._pause_quit_r   = pygame.Rect(px + 40, py + 129, pw - 80, 44)

    # ------------------------------------------------------------------
    # Texture loading
    # ------------------------------------------------------------------

    def _load_tex_frames(self, type_id: str) -> list[pygame.Surface]:
        td = BUILDING_TYPES.get(type_id)
        if td is None or not td.texture_name:
            return []

        base_path = _TEX_BASE / td.texture_name
        if not base_path.exists():
            return []

        pw = td.size[0] * TILE_SIZE
        ph = td.size[1] * TILE_SIZE

        stem   = base_path.stem
        suffix = base_path.suffix
        parent = base_path.parent

        m = re.match(r"^(.*?)-(\d+)$", stem)
        if m:
            base_stem  = m.group(1)
            frame_paths: list[Path] = []
            i = 1
            while True:
                candidate = parent / f"{base_stem}-{i}{suffix}"
                if candidate.exists():
                    frame_paths.append(candidate)
                    i += 1
                else:
                    break
            if len(frame_paths) < 2:
                frame_paths = [base_path]
        else:
            frame_paths = [base_path]

        result: list[pygame.Surface] = []
        for p in frame_paths:
            try:
                raw = pygame.image.load(str(p)).convert_alpha()
                scaled = _scale_fit(raw, pw, ph)
                result.append(scaled)
            except Exception:
                pass

        return result

    def _get_tex(self, type_id: str) -> Optional[pygame.Surface]:
        """Return the current animation frame surface for a building type."""
        if type_id not in self._tex_cache:
            self._tex_cache[type_id] = self._load_tex_frames(type_id)
        frames = self._tex_cache[type_id]
        if not frames:
            return None
        idx = int(self._anim_timer * ANIM_FPS) % len(frames)
        return frames[idx]

    def _get_tex_scaled(self, type_id: str, w: int, h: int) -> Optional[pygame.Surface]:
        """Return a texture scaled to fit (w, h) — used for picker previews."""
        key = (type_id, w, h)
        if key in self._scaled_cache:
            return self._scaled_cache[key]
        td = BUILDING_TYPES.get(type_id)
        if td is None or not td.texture_name:
            return None
        base_path = _TEX_BASE / td.texture_name
        if not base_path.exists():
            return None
        try:
            raw = pygame.image.load(str(base_path)).convert_alpha()
            result = _scale_fit(raw, w, h)
            self._scaled_cache[key] = result
            return result
        except Exception:
            return None

    # ------------------------------------------------------------------
    # Placeholder texture
    # ------------------------------------------------------------------

    def _placeholder(self, w: int, h: int) -> pygame.Surface:
        key = (w, h)
        if key not in self._ph_cache:
            surf = pygame.Surface((w, h))
            hw, hh = max(1, w // 2), max(1, h // 2)
            surf.fill(PH_PURPLE, (0,  0,  hw,     hh))
            surf.fill(PH_BLACK,  (hw, 0,  w - hw, hh))
            surf.fill(PH_BLACK,  (0,  hh, hw,     h - hh))
            surf.fill(PH_PURPLE, (hw, hh, w - hw, h - hh))
            pygame.draw.rect(surf, (60, 10, 100), surf.get_rect(), 1)
            self._ph_cache[key] = surf
        return self._ph_cache[key]

    # ------------------------------------------------------------------
    # Coordinate helpers
    # ------------------------------------------------------------------

    def _game_h(self) -> int:
        return self._sh - TOOLBAR_H - int(self._picker_t * PICKER_H)

    def _picker_y(self) -> int:
        return self._sh - TOOLBAR_H - int(self._picker_t * PICKER_H)

    def _w2s(self, gx: float, gy: float) -> tuple[float, float]:
        return gx * TILE_SIZE - self._cam_x, gy * TILE_SIZE - self._cam_y

    def _s2g(self, sx: int, sy: int) -> tuple[int, int]:
        return int((sx + self._cam_x) // TILE_SIZE), int((sy + self._cam_y) // TILE_SIZE)

    def _building_rect(self, b: PlacedBuilding) -> pygame.Rect:
        sw, sh = b.size
        bx, by = self._w2s(b.gx, b.gy)
        return pygame.Rect(int(bx), int(by), sw * TILE_SIZE, sh * TILE_SIZE)

    def _building_center(self, b: PlacedBuilding) -> tuple[int, int]:
        r = self._building_rect(b)
        return r.centerx, r.centery

    def _clamp_cam(self) -> None:
        gah = self._game_h()
        ww  = self._gs.grid_w * TILE_SIZE
        wh  = self._gs.grid_h * TILE_SIZE
        self._cam_x = max(0.0, min(self._cam_x, max(0.0, ww - self._sw)))
        self._cam_y = max(0.0, min(self._cam_y, max(0.0, wh - gah)))

    # ------------------------------------------------------------------
    # Picker helpers
    # ------------------------------------------------------------------

    def _cards_for_tab(self, tab: str) -> list[str]:
        if tab == "wires":
            return []
        # Only player-buildable buildings appear in picker
        return [tid for tid, td in BUILDING_TYPES.items()
                if td.category == tab and td.player_buildable]

    def _picker_content_x(self) -> int:
        return PICKER_PAD + ARROW_W + 4

    def _picker_visible_w(self) -> int:
        return self._sw - self._picker_content_x() - ARROW_W - PICKER_PAD - 4

    def _scroll_for_tab(self, tab: str) -> int:
        return self._picker_scroll.get(tab, 0)

    def _clamp_scroll(self, tab: str) -> None:
        cards       = self._cards_for_tab(tab)
        total_w     = max(0, len(cards) * CARD_STRIDE - CARD_GAP)
        visible_w   = self._picker_visible_w()
        max_scroll  = max(0, total_w - visible_w)
        cur         = self._picker_scroll.get(tab, 0)
        self._picker_scroll[tab] = max(0, min(cur, max_scroll))

    # ------------------------------------------------------------------
    # Update
    # ------------------------------------------------------------------

    def update(self, dt: float, events: list[pygame.event.Event]) -> Optional[str]:
        # Resize check
        nsw, nsh = self._screen.get_size()
        if (nsw, nsh) != (self._sw, self._sh):
            self._rebuild()

        # Animation timer
        self._anim_timer += dt

        # Alert countdown
        if self._alert_timer > 0:
            self._alert_timer = max(0.0, self._alert_timer - dt)

        # Level-up notification countdown
        if self._levelup_timer > 0:
            self._levelup_timer = max(0.0, self._levelup_timer - dt)

        # Picker animation
        self._picker_t += (self._picker_tgt - self._picker_t) * min(1.0, PICKER_SPEED * dt)

        # Compute powered buildings each frame
        self._powered = self._compute_powered()

        # Keyboard camera pan, XP and economy update
        if self._state != _S.PAUSED:
            self._pan_keys(dt)
            self._upd_xp(dt)
            self._upd_economy(dt)

        if self._state == _S.PAUSED:
            return self._upd_pause(events)

        return self._upd_play(dt, events)

    def _pan_keys(self, dt: float) -> None:
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]  or keys[pygame.K_a]: self._cam_x -= CAM_SPEED * dt
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]: self._cam_x += CAM_SPEED * dt
        if keys[pygame.K_UP]    or keys[pygame.K_w]: self._cam_y -= CAM_SPEED * dt
        if keys[pygame.K_DOWN]  or keys[pygame.K_s]: self._cam_y += CAM_SPEED * dt
        self._clamp_cam()

    def _upd_xp(self, dt: float) -> None:
        xps = self._gs.xp_per_second(self._powered)
        if xps > 0:
            prev_level = self._gs.level()
            self._gs.total_xp += xps * dt
            new_level = self._gs.level()
            if new_level > prev_level:
                self._on_level_up(new_level)

    def _upd_economy(self, dt: float) -> None:
        net_per_min = self._gs.net_income_per_minute(self._powered)
        self._gs.money += net_per_min / 60.0 * dt

    # ---- Pause ----

    def _upd_pause(self, events: list[pygame.event.Event]) -> Optional[str]:
        for e in events:
            if e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE:
                self._resume()
            elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
                if self._pause_resume_r and self._pause_resume_r.collidepoint(e.pos):
                    self._resume()
                elif self._pause_quit_r and self._pause_quit_r.collidepoint(e.pos):
                    self._save()
                    self._want_quit = True
        if self._want_quit:
            return "back"
        return None

    # ---- Normal play ----

    def _upd_play(self, _dt: float, events: list[pygame.event.Event]) -> Optional[str]:
        mx, my = pygame.mouse.get_pos()
        gah    = self._game_h()
        in_game = my < gah

        if in_game and not self._panning:
            gx, gy = self._s2g(mx, my)
            self._hov_cell     = (gx, gy)
            self._hov_building = self._gs.get_building_at(gx, gy)
        else:
            self._hov_cell     = None
            self._hov_building = None

        self._hov_wire = (
            self._nearest_wire(mx, my)
            if self._state == _S.BULLDOZE and in_game
            else None
        )

        # Debug menu — handle separately first
        if self._debug_open:
            for e in events:
                if e.type == pygame.KEYDOWN and e.key in (pygame.K_ESCAPE, pygame.K_F5):
                    self._debug_open = False
                elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
                    self._handle_debug_click(e.pos)
            return None

        for e in events:
            if e.type == pygame.MOUSEBUTTONDOWN and e.button == 2:
                self._panning    = True
                self._pan_origin = e.pos
                self._cam_origin = (self._cam_x, self._cam_y)
            elif e.type == pygame.MOUSEBUTTONUP and e.button == 2:
                self._panning = False
            elif e.type == pygame.MOUSEMOTION and self._panning:
                dx = e.pos[0] - self._pan_origin[0]
                dy = e.pos[1] - self._pan_origin[1]
                self._cam_x = self._cam_origin[0] - dx
                self._cam_y = self._cam_origin[1] - dy
                self._clamp_cam()

            elif e.type == pygame.KEYDOWN:
                if e.key == pygame.K_F5:
                    self._debug_open = True
                elif e.key == pygame.K_ESCAPE:
                    if self._state == _S.WIRE_TO:
                        # Back to wire_from, not explore
                        self._wire_src = None
                        self._state    = _S.WIRE_FROM
                    elif self._state == _S.WIRE_FROM:
                        # Exit wire mode entirely
                        self._close_picker()
                    elif self._state not in (_S.EXPLORE,):
                        self._cancel()
                    else:
                        self._prev_state = self._state
                        self._state      = _S.PAUSED
                elif e.key == pygame.K_w:
                    # W key activates wire mode from EXPLORE
                    if self._state == _S.EXPLORE:
                        self._active_tab = "wires"
                        self._picker_tgt = 0.0  # no picker for wires tab
                        self._state      = _S.WIRE_FROM
                        self._wire_src   = None
                        self._sel_type   = None

            elif e.type == pygame.MOUSEWHEEL:
                if self._state in (_S.WIRE_FROM, _S.WIRE_TO):
                    # Scroll wheel changes wire layer in wire mode
                    self._wire_layer = max(0, min(
                        self._gs.wire_layers - 1,
                        self._wire_layer - e.y,
                    ))
                elif (self._picker_t > 0.1
                        and self._active_tab is not None
                        and self._active_tab != "wires"):
                    tab = self._active_tab
                    self._picker_scroll[tab] = (
                        self._scroll_for_tab(tab) - e.x * CARD_STRIDE
                    )
                    self._clamp_scroll(tab)
                else:
                    if in_game:
                        self._cam_y -= e.y * TILE_SIZE * 0.5
                        self._clamp_cam()

            elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
                self._on_lmb(mx, my, in_game)

            elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 3:
                self._on_rmb()

        return None

    # ---- Input ----

    def _on_lmb(self, mx: int, my: int, in_game: bool) -> None:
        # Toolbar tabs
        for name, rect in self._tab_rects.items():
            if rect.collidepoint(mx, my):
                self._toggle_tab(name)
                return

        # Toolbar tools
        for name, rect in self._tool_rects.items():
            if rect.collidepoint(mx, my):
                self._toggle_tool(name)
                return

        # Picker area
        py2   = self._picker_y()
        ph2   = int(self._picker_t * PICKER_H)
        in_pic = py2 <= my < py2 + ph2 and self._picker_t > 0.2

        if in_pic:
            tab = self._active_tab
            if tab is not None and tab != "wires":
                # Scroll arrows
                if self._picker_left_r and self._picker_left_r.collidepoint(mx, my):
                    self._picker_scroll[tab] = max(0, self._scroll_for_tab(tab) - CARD_STRIDE)
                    return
                if self._picker_right_r and self._picker_right_r.collidepoint(mx, my):
                    self._picker_scroll[tab] = self._scroll_for_tab(tab) + CARD_STRIDE
                    self._clamp_scroll(tab)
                    return

                # Card click
                cx_start = self._picker_content_x()
                cx_end   = cx_start + self._picker_visible_w()
                if cx_start <= mx < cx_end:
                    scroll   = self._scroll_for_tab(tab)
                    rel_x    = mx - cx_start + scroll
                    card_idx = rel_x // CARD_STRIDE
                    cards    = self._cards_for_tab(tab)
                    if 0 <= card_idx < len(cards):
                        tid = cards[card_idx]
                        td  = BUILDING_TYPES.get(tid)
                        if td:
                            if td.unlock_level > self._gs.level():
                                self._flash(f"Requires Level {td.unlock_level}!")
                            else:
                                self._select_type(tid)
            return  # consumed by picker

        if not in_game:
            return

        gx, gy = self._s2g(mx, my)

        if self._state == _S.PLACE and self._sel_type:
            td = BUILDING_TYPES.get(self._sel_type)
            if td and td.max_count > 0 and self._gs.count_of_type(self._sel_type) >= td.max_count:
                self._flash(f"Max 1 allowed: {td.name}")
                return
            if td and td.build_cost > 0 and self._gs.money < td.build_cost:
                self._flash(f"Can't afford! Need €{td.build_cost:,.0f}")
                return
            b = self._gs.place_building(self._sel_type, gx, gy)
            if b:
                if td and td.build_cost > 0:
                    self._gs.money -= td.build_cost
                self._flash(f"Placed {BUILDING_TYPES[self._sel_type].name}")
            else:
                self._flash("Cannot place here!")

        elif self._state == _S.WIRE_FROM:
            b = self._gs.get_building_at(gx, gy)
            if b:
                self._wire_src = b.build_id
                self._state    = _S.WIRE_TO
                self._flash(f"From {b.typedef.name} — click target")

        elif self._state == _S.WIRE_TO:
            b = self._gs.get_building_at(gx, gy)
            if b and self._wire_src and b.build_id != self._wire_src:
                w = self._gs.add_wire(self._wire_src, b.build_id, self._wire_layer)
                self._wire_src = None
                self._state    = _S.WIRE_FROM
                self._flash("Wire connected!" if w else "Already connected!")
            elif b:
                self._flash("Cannot connect to itself!")

        elif self._state == _S.BULLDOZE:
            if self._hov_wire:
                self._gs.remove_wire(self._hov_wire)
                self._hov_wire = None
                self._flash("Wire removed")
            elif self._hov_building:
                name = self._hov_building.typedef.name
                bid  = self._hov_building.build_id
                self._gs.remove_building(bid)
                if self._sel_building and self._sel_building.build_id == bid:
                    self._sel_building = None
                self._hov_building = None
                self._flash(f"{name} demolished")

    def _on_rmb(self) -> None:
        if self._state in (_S.PLACE, _S.WIRE_FROM, _S.WIRE_TO):
            self._cancel()
        else:
            self._sel_building = None

    # ---- Tab / tool toggles ----

    def _toggle_tab(self, name: str) -> None:
        if self._active_tab == name:
            self._close_picker()
        else:
            self._active_tab = name
            if name == "wires":
                # Wires tab does NOT open picker
                self._picker_tgt = 0.0
                self._state    = _S.WIRE_FROM
                self._wire_src = None
                self._sel_type = None
            else:
                self._picker_tgt = 1.0
                self._state    = _S.EXPLORE
                self._sel_type = None
                self._wire_src = None

    def _close_picker(self) -> None:
        self._active_tab = None
        self._picker_tgt = 0.0
        self._state      = _S.EXPLORE
        self._sel_type   = None
        self._wire_src   = None

    def _toggle_tool(self, name: str) -> None:
        if name == "bulldoze":
            if self._state == _S.BULLDOZE:
                self._close_picker()
                self._state = _S.EXPLORE
            else:
                self._close_picker()
                self._state = _S.BULLDOZE

    def _select_type(self, type_id: str) -> None:
        if self._sel_type == type_id and self._state == _S.PLACE:
            self._sel_type = None
            self._state    = _S.EXPLORE
        else:
            self._sel_type = type_id
            self._state    = _S.PLACE

    def _cancel(self) -> None:
        if self._state == _S.WIRE_TO:
            self._wire_src = None
            self._state    = _S.WIRE_FROM
        elif self._active_tab == "wires":
            self._wire_src = None
            self._state    = _S.WIRE_FROM
        else:
            self._sel_type = None
            self._state    = _S.EXPLORE

    def _resume(self) -> None:
        self._state = (
            self._prev_state if self._prev_state != _S.PAUSED else _S.EXPLORE
        )

    # ---- Wire hover ----

    def _nearest_wire(self, sx: int, sy: int, threshold: float = 10.0) -> Optional[Wire]:
        best:   Optional[Wire] = None
        best_d: float          = threshold
        for w in self._gs.wires:
            src = self._gs.get_building_by_id(w.from_id)
            dst = self._gs.get_building_by_id(w.to_id)
            if src is None or dst is None:
                continue
            ax, ay = self._building_center(src)
            bx, by = self._building_center(dst)
            d = _pt_seg_dist(sx, sy, ax, ay, bx, by)
            if d < best_d:
                best_d = d
                best   = w
        return best

    # ---- Alerts / notifications ----

    def _flash(self, msg: str) -> None:
        self._alert_text  = msg
        self._alert_timer = 2.5

    def _on_level_up(self, new_level: int) -> None:
        self._levelup_timer = 4.0
        self._levelup_level = new_level
        self._prev_level    = new_level

    # ---- Save ----

    def _save(self) -> None:
        self._gs.cam_x         = self._cam_x
        self._gs.cam_y         = self._cam_y
        self._meta.game_data   = self._gs.to_dict()
        self._meta.last_played = time.time()
        self._meta.save()

    # ---- Debug ----

    def _handle_debug_click(self, pos: tuple[int, int]) -> None:
        for label, rect in self._debug_btn_rects:
            if rect.collidepoint(pos):
                if label == "Reset XP":
                    self._gs.total_xp = 0.0
                elif label == "+€10k":
                    self._gs.money += 10_000.0
                elif label == "Reset €":
                    self._gs.money = 5_000.0
                elif label.startswith("LVL ") or label == "MAX":
                    if label == "MAX":
                        target_lv = MAX_LEVEL
                    else:
                        target_lv = int(label[4:])
                    self._gs.total_xp = XP_THRESHOLDS[target_lv - 1]
                elif label == "Close":
                    self._debug_open = False

    # ------------------------------------------------------------------
    # Draw
    # ------------------------------------------------------------------

    def draw(self, screen: pygame.Surface) -> None:
        screen.fill(self._theme.BG)

        gah       = self._game_h()
        game_rect = pygame.Rect(0, 0, self._sw, gah)

        screen.set_clip(game_rect)
        self._draw_grid(screen, gah)
        self._draw_wires(screen)
        self._draw_buildings(screen)
        self._draw_overlay(screen, gah)
        screen.set_clip(None)

        self._draw_hud(screen)
        self._draw_picker(screen)
        self._draw_toolbar(screen)

        # Info panel: show on hover, not on click
        if self._hov_building is not None:
            self._draw_info_panel(screen, gah)

        if self._alert_timer > 0:
            self._draw_alert(screen)

        if self._levelup_timer > 0:
            self._draw_levelup(screen)

        if self._state == _S.PAUSED:
            self._draw_pause(screen)

        if self._debug_open:
            self._draw_debug(screen)

    # ---- Grid ----

    def _draw_grid(self, screen: pygame.Surface, gah: int) -> None:
        gs = self._gs
        ts = TILE_SIZE
        cx = int(self._cam_x)
        cy = int(self._cam_y)
        x0 = max(0, cx // ts)
        y0 = max(0, cy // ts)
        x1 = min(gs.grid_w, (cx + self._sw) // ts + 2)
        y1 = min(gs.grid_h, (cy + gah)       // ts + 2)

        for gy in range(y0, y1):
            for gx in range(x0, x1):
                sx  = gx * ts - cx
                sy  = gy * ts - cy
                col = GRASS_A if (gx + gy) % 2 == 0 else GRASS_B
                screen.fill(col, (sx, sy, ts, ts))

        lc = GRID_LINE_COL
        for gx in range(x0, x1 + 1):
            sx = gx * ts - cx
            if 0 <= sx <= self._sw:
                pygame.draw.line(screen, lc, (sx, 0), (sx, gah))
        for gy in range(y0, y1 + 1):
            sy = gy * ts - cy
            if 0 <= sy <= gah:
                pygame.draw.line(screen, lc, (0, sy), (self._sw, sy))

        # Grid border
        bx0 = -cx
        by0 = -cy
        bx1 = gs.grid_w * ts - cx
        by1 = gs.grid_h * ts - cy
        pygame.draw.rect(screen, (60, 120, 50), (bx0, by0, bx1 - bx0, by1 - by0), 2)

    # ---- Wires ----

    def _draw_wires(self, screen: pygame.Surface) -> None:
        for w in self._gs.wires:
            src = self._gs.get_building_by_id(w.from_id)
            dst = self._gs.get_building_by_id(w.to_id)
            if src is None or dst is None:
                continue
            ax, ay = self._building_center(src)
            bx, by = self._building_center(dst)
            col    = WIRE_COLORS[w.layer % len(WIRE_COLORS)]
            is_hov = w is self._hov_wire
            if is_hov:
                pygame.draw.line(screen, (255, 240, 60), (ax, ay), (bx, by), WIRE_W + 4)
            pygame.draw.line(screen, col, (ax, ay), (bx, by), WIRE_W + (2 if is_hov else 0))
            pygame.draw.circle(screen, col, (ax, ay), 5)
            pygame.draw.circle(screen, col, (bx, by), 5)

    # ---- Buildings ----

    def _draw_buildings(self, screen: pygame.Surface) -> None:
        th   = self._theme
        clip = screen.get_clip()
        for b in self._gs.buildings:
            r = self._building_rect(b)
            if not clip.colliderect(r):
                continue
            pw, ph = b.size[0] * TILE_SIZE, b.size[1] * TILE_SIZE

            # Drop shadow
            sh_surf = pygame.Surface((pw, ph), pygame.SRCALPHA)
            sh_surf.fill((0, 0, 0, 70))
            screen.blit(sh_surf, (r.x + SHADOW_OFF, r.y + SHADOW_OFF))

            # Body texture (or placeholder)
            tex = self._get_tex(b.type_id)
            if tex:
                screen.blit(tex, r.topleft)
            else:
                screen.blit(self._placeholder(pw, ph), r.topleft)

            # Category border colour
            cat  = b.typedef.category
            ccol = CAT_COLORS.get(cat, th.BORDER)

            # Highlight / hover
            if (b is self._hov_building
                    and self._state in (_S.EXPLORE, _S.BULLDOZE, _S.WIRE_FROM)):
                hl  = pygame.Surface((pw, ph), pygame.SRCALPHA)
                col = (255, 50, 50, 60) if self._state == _S.BULLDOZE else (255, 255, 255, 30)
                hl.fill(col)
                screen.blit(hl, r.topleft)
                pygame.draw.rect(screen, th.ACCENT, r, 2)
            else:
                pygame.draw.rect(screen, ccol, r, 1)

            # Powered status dot: green = powered, red = not powered
            is_powered = b.build_id in self._powered
            dot_col = (0, 200, 80) if is_powered else (200, 50, 50)
            pygame.draw.circle(screen, (0, 0, 0, 180), (r.right - 8, r.top + 8), 6)
            pygame.draw.circle(screen, dot_col, (r.right - 8, r.top + 8), 4)

            # Name tag
            name_s = th.font(11).render(b.typedef.name, True, (220, 225, 235))
            nx = r.centerx - name_s.get_width() // 2
            ny = r.bottom - name_s.get_height() - 3
            if ny > r.top + 2:
                bg = pygame.Surface((name_s.get_width() + 4, name_s.get_height()), pygame.SRCALPHA)
                bg.fill((0, 0, 0, 140))
                screen.blit(bg,     (nx - 2, ny))
                screen.blit(name_s, (nx,     ny))

            # XP rate badge for powered research buildings only
            td = b.typedef
            if td.xp_rate > 0 and is_powered:
                xp_s = th.font(11).render(f"+{td.xp_rate:.0f} XP/s", True, (126, 211, 33))
                screen.blit(xp_s, (r.x + 2, r.y + 2))

    # ---- Placement / wire overlay ----

    def _draw_overlay(self, screen: pygame.Surface, gah: int) -> None:
        mx, my = pygame.mouse.get_pos()

        if self._state == _S.PLACE and self._sel_type and my < gah:
            td = BUILDING_TYPES.get(self._sel_type)
            if td:
                gx, gy = self._s2g(mx, my)
                sw2, sh2 = td.size
                px_w = sw2 * TILE_SIZE
                px_h = sh2 * TILE_SIZE
                sx   = gx * TILE_SIZE - int(self._cam_x)
                sy   = gy * TILE_SIZE - int(self._cam_y)
                can  = (self._gs.in_bounds(gx, gy, (sw2, sh2))
                        and not self._gs.is_occupied(gx, gy, (sw2, sh2)))

                tex = self._get_tex(self._sel_type)
                if tex:
                    prev = tex.copy()
                else:
                    prev = self._placeholder(px_w, px_h).copy()
                prev.set_alpha(180)
                screen.blit(prev, (sx, sy))
                pygame.draw.rect(screen,
                                 (0, 200, 70) if can else (220, 50, 50),
                                 (sx, sy, px_w, px_h), 2)

        elif self._state == _S.WIRE_TO and self._wire_src and my < gah:
            src = self._gs.get_building_by_id(self._wire_src)
            if src:
                ax, ay = self._building_center(src)
                col    = WIRE_COLORS[self._wire_layer % len(WIRE_COLORS)]
                b      = self._gs.get_building_at(*self._s2g(mx, my))
                if b and b.build_id != self._wire_src:
                    bx, by = self._building_center(b)
                    pygame.draw.line(screen, col, (ax, ay), (bx, by), WIRE_W)
                    pygame.draw.circle(screen, col, (bx, by), 5)
                else:
                    pygame.draw.line(screen, col, (ax, ay), (mx, my), WIRE_W)
                pygame.draw.circle(screen, col, (ax, ay), 5)

    # ---- HUD ----

    def _draw_hud(self, screen: pygame.Surface) -> None:
        th      = self._theme
        gs      = self._gs
        lv      = gs.level()
        prog    = gs.xp_progress()
        xps     = gs.xp_per_second(self._powered)
        gen     = gs.total_generation_mw()
        con     = gs.total_consumption_mw(self._powered)
        net     = gs.net_power_mw(self._powered)
        happy   = gs.happiness(self._powered)
        net_inc = gs.net_income_per_minute(self._powered)

        hud_w = 300
        hud_h = 220
        hud   = pygame.Surface((hud_w, hud_h), pygame.SRCALPHA)
        hud.fill(HUD_BG)

        pygame.draw.rect(hud, th.ACCENT, hud.get_rect(), 1)

        y2 = 8

        # Level header
        lv_lbl = th.font(12).render("LEVEL", True, th.TEXT_DIM)
        lv_num = th.font(24).render(str(lv), True, th.ACCENT)
        hud.blit(lv_lbl, (10, y2 + 6))
        hud.blit(lv_num, (10 + lv_lbl.get_width() + 6, y2))

        if lv < MAX_LEVEL:
            nxt_xp  = gs.xp_for_next_level()
            cur_xp  = gs.xp_for_current_level()
            xp_lbl  = th.font(12).render(
                f"{_fmt_xp(gs.total_xp - cur_xp)} / {_fmt_xp(nxt_xp - cur_xp)} XP",
                True, th.TEXT_DIM,
            )
            hud.blit(xp_lbl, (hud_w - xp_lbl.get_width() - 8, y2 + 8))
        else:
            mx_lbl = th.font(12).render("MAX LEVEL", True, th.ACCENT)
            hud.blit(mx_lbl, (hud_w - mx_lbl.get_width() - 8, y2 + 8))

        y2 += 32

        # XP progress bar
        bar_x, bar_w, bar_h = 10, hud_w - 20, 10
        pygame.draw.rect(hud, (20, 35, 55), (bar_x, y2, bar_w, bar_h), border_radius=4)
        fill_w = int(bar_w * prog)
        if fill_w > 0:
            bar_col = (
                int(232 * (1 - prog) + 126 * prog),
                int(137 * (1 - prog) + 211 * prog),
                int(12  * (1 - prog) + 33  * prog),
            )
            pygame.draw.rect(hud, bar_col, (bar_x, y2, fill_w, bar_h), border_radius=4)
        pygame.draw.rect(hud, th.BORDER, (bar_x, y2, bar_w, bar_h), 1, border_radius=4)

        y2 += bar_h + 4

        if xps > 0:
            xp_rate_s = th.font(12).render(f"+{xps:.1f} XP/s", True, (126, 211, 33))
            hud.blit(xp_rate_s, (10, y2))
        y2 += 16

        pygame.draw.line(hud, th.BORDER, (10, y2), (hud_w - 10, y2))
        y2 += 6

        # Money row
        money_s = th.font(14).render(f"€{gs.money:,.0f}", True,
                                     (80, 220, 120) if gs.money >= 0 else (230, 60, 60))
        hud.blit(money_s, (10, y2))

        # Happiness bar (right side)
        hap_pct = int(happy * 100)
        hap_col = (
            int(230 * (1 - happy) + 40 * happy),
            int(60  * (1 - happy) + 220 * happy),
            int(60  * (1 - happy) + 80 * happy),
        )
        hap_lbl = th.font(12).render(f"Happiness: {hap_pct}%", True, hap_col)
        hud.blit(hap_lbl, (hud_w - hap_lbl.get_width() - 8, y2 + 2))
        y2 += 20

        # Income/expense
        inc_sign = "+" if net_inc >= 0 else "-"
        inc_col  = (80, 220, 120) if net_inc >= 0 else (230, 100, 60)
        inc_s = th.font(11).render(
            f"{inc_sign}€{abs(net_inc):,.1f}/min", True, inc_col
        )
        hud.blit(inc_s, (10, y2))
        y2 += 16

        pygame.draw.line(hud, th.BORDER, (10, y2), (hud_w - 10, y2))
        y2 += 5

        net_col = (0, 200, 80) if net >= 0 else (230, 60, 60)
        net_pfx = "▲" if net >= 0 else "▼"

        rows = [
            ("Output",      _fmt_mw(gen),        th.ACCENT),
            ("Consumption", _fmt_mw(con),         (79, 195, 247)),
            (f"{net_pfx} Net", _fmt_mw(abs(net)), net_col),
        ]
        fk = th.font(12)
        fv = th.font(12)
        for key, val, vcol in rows:
            ks = fk.render(key + ":", True, th.TEXT_DIM)
            vs = fv.render(val, True, vcol)
            hud.blit(ks, (10, y2))
            hud.blit(vs, (hud_w - vs.get_width() - 8, y2))
            y2 += ks.get_height() + 3

        screen.blit(hud, (10, 10))

    # ---- Picker panel ----

    def _draw_picker(self, screen: pygame.Surface) -> None:
        if self._picker_t < 0.01:
            return
        th  = self._theme
        py  = self._picker_y()
        ph  = int(self._picker_t * PICKER_H)
        tab = self._active_tab

        surf = pygame.Surface((self._sw, ph), pygame.SRCALPHA)
        surf.fill(PANEL_BG)

        line_col = CAT_COLORS.get(tab, th.ACCENT) if tab else th.ACCENT
        pygame.draw.line(surf, line_col, (0, 0), (self._sw, 0), 2)
        screen.blit(surf, (0, py))

        if tab is not None and tab != "wires":
            self._draw_building_picker(screen, py, ph, tab)

    def _draw_building_picker(self, screen: pygame.Surface,
                               py: int, ph: int, tab: str) -> None:
        th      = self._theme
        cards   = self._cards_for_tab(tab)
        scroll  = self._scroll_for_tab(tab)
        lv      = self._gs.level()
        cx_start = self._picker_content_x()
        vis_w    = self._picker_visible_w()
        card_h   = ph - 20

        ar_y = py + (ph - ARROW_W) // 2
        lr   = pygame.Rect(PICKER_PAD, ar_y, ARROW_W, ARROW_W)
        pygame.draw.rect(screen, (18, 30, 50), lr, border_radius=5)
        pygame.draw.rect(screen, th.BORDER,    lr, 1, border_radius=5)
        ls   = th.font(20).render("<", True, th.TEXT if scroll > 0 else th.TEXT_DIM)
        screen.blit(ls, (lr.centerx - ls.get_width() // 2, lr.centery - ls.get_height() // 2))
        self._picker_left_r = lr

        total_w  = max(0, len(cards) * CARD_STRIDE - CARD_GAP)
        max_sc   = max(0, total_w - vis_w)
        rr       = pygame.Rect(cx_start + vis_w + 4, ar_y, ARROW_W, ARROW_W)
        pygame.draw.rect(screen, (18, 30, 50), rr, border_radius=5)
        pygame.draw.rect(screen, th.BORDER,    rr, 1, border_radius=5)
        rs       = th.font(20).render(">", True, th.TEXT if scroll < max_sc else th.TEXT_DIM)
        screen.blit(rs, (rr.centerx - rs.get_width() // 2, rr.centery - rs.get_height() // 2))
        self._picker_right_r = rr

        clip = pygame.Rect(cx_start, py, vis_w, ph)
        screen.set_clip(clip)

        for i, tid in enumerate(cards):
            x = cx_start + i * CARD_STRIDE - scroll
            if x + CARD_W < cx_start or x > cx_start + vis_w:
                continue
            self._draw_building_card(screen, x, py + 10, CARD_W, card_h,
                                     tid, self._sel_type == tid, lv)

        screen.set_clip(None)

    def _draw_building_card(self, screen: pygame.Surface,
                             x: int, y: int, w: int, h: int,
                             type_id: str, selected: bool, player_level: int) -> None:
        th     = self._theme
        td     = BUILDING_TYPES.get(type_id)
        if td is None:
            return

        locked = td.unlock_level > player_level
        cat    = td.category
        ccol   = CAT_COLORS.get(cat, th.ACCENT)

        if locked:
            bg   = CARD_LOCK
            bord = (30, 40, 60)
        elif selected:
            bg   = (35, 18, 5)
            bord = th.ACCENT
        else:
            bg   = CARD_BG
            bord = ccol

        rect = pygame.Rect(x, y, w, h)
        pygame.draw.rect(screen, bg,   rect, border_radius=6)
        pygame.draw.rect(screen, bord, rect, 1 if not selected else 2, border_radius=6)

        # Texture preview
        img_h    = h - 68
        img_rect = pygame.Rect(x + 5, y + 5, w - 10, max(1, img_h))
        tex      = self._get_tex_scaled(type_id, img_rect.w, img_rect.h)
        if tex:
            if locked:
                dim = tex.copy()
                dim.set_alpha(60)
                screen.blit(dim, img_rect.topleft)
            else:
                screen.blit(tex, img_rect.topleft)
        else:
            ph_surf = self._placeholder(img_rect.w, img_rect.h)
            if locked:
                ph_surf = ph_surf.copy()
                ph_surf.set_alpha(60)
            screen.blit(ph_surf, img_rect.topleft)

        bottom_y = y + h - 60

        # Power / XP label
        mw    = td.power_mw
        abs_v = abs(mw)
        if abs_v >= 1000:
            pwr_str = f"{mw / 1000:.1f} GW"
        elif abs_v < 1:
            pwr_str = f"{mw * 1000:.2f} kW"
        else:
            pwr_str = f"{mw:.0f} MW"

        if td.xp_rate > 0:
            pwr_str = f"+{td.xp_rate:.0f} XP/s"
            pwr_col = (126, 211, 33)
        elif mw > 0:
            pwr_col = th.ACCENT
        elif mw < 0:
            pwr_col = (79, 195, 247)
        else:
            pwr_col = th.TEXT_DIM

        pwr_s = th.font(11).render(pwr_str, True,
                                   th.TEXT_DIM if locked else pwr_col)
        screen.blit(pwr_s, (x + w // 2 - pwr_s.get_width() // 2, bottom_y))
        bottom_y += 14

        # Name
        nm_col = (50, 70, 100) if locked else (th.TEXT if selected else (160, 175, 200))
        name_s = th.font(11).render(td.name, True, nm_col)
        if name_s.get_width() > w - 6:
            font = th.font(11)
            trunc = td.name
            while font.size(trunc + "…")[0] > w - 6 and trunc:
                trunc = trunc[:-1]
            name_s = font.render(trunc + "…", True, nm_col)
        screen.blit(name_s, (x + w // 2 - name_s.get_width() // 2, bottom_y))
        bottom_y += 14

        # Build cost
        if td.build_cost > 0:
            can_afford = self._gs.money >= td.build_cost
            cost_col = (80, 220, 120) if (not locked and can_afford) else (200, 80, 60)
            cost_s = th.font(10).render(f"€{td.build_cost:,.0f}", True,
                                        (60, 80, 100) if locked else cost_col)
            screen.blit(cost_s, (x + w // 2 - cost_s.get_width() // 2, bottom_y))
            bottom_y += 13

        # Level badge
        if locked:
            badge_col = (60, 80, 120)
        else:
            badge_col = ccol

        badge_txt = f"LVL {td.unlock_level}"
        badge_s   = th.font(10).render(badge_txt, True,
                                        (80, 100, 140) if locked else th.BG)
        badge_w   = badge_s.get_width() + 8
        badge_h   = badge_s.get_height() + 4
        badge_x   = x + w // 2 - badge_w // 2
        pygame.draw.rect(screen, badge_col,
                         (badge_x, bottom_y, badge_w, badge_h), border_radius=3)
        screen.blit(badge_s, (badge_x + 4, bottom_y + 2))

        # Lock overlay icon if locked
        if locked:
            lx = x + w // 2 - 5
            ly = y + 8
            pygame.draw.rect(screen, (40, 60, 90), (lx, ly + 5, 10, 8), border_radius=2)
            pygame.draw.arc(screen, (40, 60, 90),
                            pygame.Rect(lx, ly, 10, 10), 0, 3.14159, 2)

    # ---- Toolbar ----

    def _draw_toolbar(self, screen: pygame.Surface) -> None:
        th = self._theme
        ty = self._sh - TOOLBAR_H

        bar = pygame.Surface((self._sw, TOOLBAR_H), pygame.SRCALPHA)
        bar.fill((8, 16, 28, 252))
        pygame.draw.line(bar, th.ACCENT, (0, 0), (self._sw, 0), 2)
        screen.blit(bar, (0, ty))

        tab_labels_map = {
            "generators": "Generators",
            "research":   "Forschung",
            "wires":      "Wires",
        }
        for tab, rect in self._tab_rects.items():
            active  = self._active_tab == tab
            ccol    = CAT_COLORS.get(tab, th.ACCENT)
            bg      = (25, 18, 8) if active else th.BTN_NORMAL_BG
            bord    = ccol        if active else th.BORDER
            pygame.draw.rect(screen, bg,   rect, border_radius=5)
            pygame.draw.rect(screen, bord, rect, 1 if not active else 2, border_radius=5)

            if active:
                tb = pygame.Rect(rect.x + 2, rect.y, rect.w - 4, 2)
                pygame.draw.rect(screen, ccol, tb, border_radius=1)

            lbl   = tab_labels_map.get(tab, tab)
            ls    = th.font(13).render(lbl, True, ccol if active else th.TEXT_DIM)
            screen.blit(ls, (rect.centerx - ls.get_width() // 2,
                              rect.centery - ls.get_height() // 2))

        # Separator
        if self._tab_rects:
            sep_x = max(r.right for r in self._tab_rects.values()) + 8
            pygame.draw.line(screen, th.BORDER, (sep_x, ty + 6), (sep_x, self._sh - 6))

        # Bulldoze button
        for name, rect in self._tool_rects.items():
            active  = (name == "bulldoze" and self._state == _S.BULLDOZE)
            bg      = (40, 12, 12)  if active else th.BTN_NORMAL_BG
            bord    = (220, 60, 60) if active else th.BORDER
            pygame.draw.rect(screen, bg,   rect, border_radius=5)
            pygame.draw.rect(screen, bord, rect, 1, border_radius=5)
            lbl_col = (220, 80, 80) if active else th.TEXT
            ls = th.font(13).render("Bulldoze", True, lbl_col)
            screen.blit(ls, (rect.centerx - ls.get_width() // 2,
                              rect.centery - ls.get_height() // 2))

        # Context hint (right side)
        if self._state == _S.WIRE_FROM:
            hint = f"Wires – Layer {self._wire_layer} (scroll wheel to change) | Click source building"
        elif self._state == _S.WIRE_TO:
            hint = f"Wires – Layer {self._wire_layer} | Click target building | ESC to cancel"
        else:
            hints = {
                _S.EXPLORE:  "Click a building to inspect  |  WASD / MMB to pan  |  W = wire mode",
                _S.PLACE:    "Left-click to place  |  RMB / ESC to cancel",
                _S.BULLDOZE: "Click building or wire to demolish  |  ESC to cancel",
            }
            hint = hints.get(self._state, "")

        if hint:
            hs = th.font(13).render(hint, True, th.TEXT_DIM)
            hx = self._sw - hs.get_width() - 12
            hy = ty + TOOLBAR_H // 2 - hs.get_height() // 2
            tool_right = max((r.right for r in self._tool_rects.values()), default=0)
            if hx > tool_right + 8:
                screen.blit(hs, (hx, hy))

    # ---- Building info panel ----

    def _draw_info_panel(self, screen: pygame.Surface, gah: int) -> None:
        b = self._hov_building
        if b is None:
            return
        th    = self._theme
        td    = b.typedef
        wires = self._gs.wires_for_building(b.build_id)
        cat   = td.category
        ccol  = CAT_COLORS.get(cat, th.ACCENT)

        pw, ph = 280, 240
        px     = 10
        py2    = gah - ph - 10

        panel = pygame.Surface((pw, ph), pygame.SRCALPHA)
        panel.fill(PANEL_BG)
        pygame.draw.rect(panel, ccol, panel.get_rect(), 1)
        pygame.draw.rect(panel, ccol, (0, 0, pw, 3))

        # Title
        title = th.font(16).render(td.name, True, ccol)
        panel.blit(title, (10, 10))

        # Category badge
        cat_s = th.font(10).render(CATEGORY_LABELS.get(cat, cat).upper(), True, th.BG)
        cat_bw = cat_s.get_width() + 8
        cat_bh = cat_s.get_height() + 4
        cat_bx = pw - cat_bw - 8
        pygame.draw.rect(panel, ccol, (cat_bx, 10, cat_bw, cat_bh), border_radius=3)
        panel.blit(cat_s, (cat_bx + 4, 12))

        y2 = 36
        fk = th.font(12)
        fv = th.font(12)

        is_powered = b.build_id in self._powered

        rows: list[tuple[str, str, tuple[int, int, int]]] = []
        if td.power_mw > 0:
            rows.append(("Output",    _fmt_mw(td.power_mw),  th.ACCENT))
        elif td.power_mw < 0:
            rows.append(("Consumes",  _fmt_mw(abs(td.power_mw)), (79, 195, 247)))
        if td.xp_rate > 0:
            rows.append(("XP/s",      f"+{td.xp_rate:.1f}", (126, 211, 33)))
        if td.tax_rate > 0:
            rows.append(("Tax/min",   f"€{td.tax_rate:.1f}", (80, 220, 120)))
        if td.operating_cost != 0:
            sign = "-" if td.operating_cost > 0 else "+"
            opc_col = (200, 100, 60) if td.operating_cost > 0 else (80, 220, 120)
            rows.append(("Op. Cost",  f"{sign}€{abs(td.operating_cost):.1f}/min", opc_col))
        if td.build_cost > 0:
            rows.append(("Build Cost", f"€{td.build_cost:,.0f}", th.TEXT))
        rows += [
            ("Unlock Lvl",  str(td.unlock_level),   th.TEXT),
            ("Powered",     "Yes" if is_powered else "No",
             (0, 200, 80) if is_powered else (200, 50, 50)),
            ("Connections", str(len(wires)),          th.TEXT),
            ("Grid Pos",    f"({b.gx}, {b.gy})",     th.TEXT_DIM),
        ]

        for key, val, vcol in rows:
            ks = fk.render(key + ":", True, th.TEXT_DIM)
            vs = fv.render(val, True, vcol)
            panel.blit(ks, (10, y2))
            panel.blit(vs, (pw - vs.get_width() - 8, y2))
            y2 += ks.get_height() + 4

        screen.blit(panel, (px, py2))

    # ---- Alert banner ----

    def _draw_alert(self, screen: pygame.Surface) -> None:
        th    = self._theme
        s     = th.font(15).render(self._alert_text, True, th.TEXT)
        w, h  = s.get_width() + 24, s.get_height() + 14
        x     = (self._sw - w) // 2
        y     = 72
        alpha = min(255, int(min(self._alert_timer, 0.8) / 0.8 * 255))
        bg    = pygame.Surface((w, h), pygame.SRCALPHA)
        bg.fill((10, 18, 32, alpha))
        pygame.draw.rect(bg, (*th.ACCENT, alpha), bg.get_rect(), 1)
        screen.blit(bg, (x, y))
        ts = s.copy()
        ts.set_alpha(alpha)
        screen.blit(ts, (x + 12, y + 7))

    # ---- Level-up notification ----

    def _draw_levelup(self, screen: pygame.Surface) -> None:
        th    = self._theme
        timer = self._levelup_timer
        lv    = self._levelup_level

        total   = 4.0
        fade_in = 0.5
        fade_out = 1.0
        elapsed  = total - timer

        if elapsed < fade_in:
            alpha = int(elapsed / fade_in * 255)
        elif timer < fade_out:
            alpha = int(timer / fade_out * 255)
        else:
            alpha = 255

        pw, ph = 420, 100
        px     = (self._sw - pw) // 2
        py2    = (self._sh - ph) // 2 - 30

        surf = pygame.Surface((pw, ph), pygame.SRCALPHA)
        surf.fill((8, 20, 12, int(alpha * 0.92)))
        pygame.draw.rect(surf, (126, 211, 33, alpha), surf.get_rect(), 2)

        t1 = th.font(30).render("LEVEL UP!", True, (126, 211, 33))
        t1.set_alpha(alpha)
        surf.blit(t1, (pw // 2 - t1.get_width() // 2, 10))

        lv_txt = th.font(18).render(f"Now at Level {lv}", True, th.TEXT)
        lv_txt.set_alpha(alpha)
        surf.blit(lv_txt, (pw // 2 - lv_txt.get_width() // 2, 50))

        newly_unlocked = [
            td.name for td in BUILDING_TYPES.values()
            if td.unlock_level == lv and td.player_buildable
        ]
        if newly_unlocked:
            hint = "Unlocked: " + ", ".join(newly_unlocked[:3])
            if len(newly_unlocked) > 3:
                hint += f" +{len(newly_unlocked) - 3}"
            hs = th.font(12).render(hint, True, (160, 200, 160))
            hs.set_alpha(alpha)
            surf.blit(hs, (pw // 2 - hs.get_width() // 2, 78))

        screen.blit(surf, (px, py2))

    # ---- Pause overlay ----

    def _draw_pause(self, screen: pygame.Surface) -> None:
        th = self._theme

        overlay = pygame.Surface((self._sw, self._sh), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 170))
        screen.blit(overlay, (0, 0))

        if self._pause_rect is None:
            return
        r     = self._pause_rect
        panel = pygame.Surface((r.w, r.h), pygame.SRCALPHA)
        panel.fill((10, 18, 32, 252))
        pygame.draw.rect(panel, th.ACCENT, panel.get_rect(), 2)
        screen.blit(panel, r.topleft)

        title = th.font(30).render("PAUSED", True, th.ACCENT)
        screen.blit(title, (r.centerx - title.get_width() // 2, r.y + 18))

        mx2, my2 = pygame.mouse.get_pos()
        for rect, label, base_bg in [
            (self._pause_resume_r, "Resume",              th.BTN_NORMAL_BG),
            (self._pause_quit_r,   "Save & Quit to Menu", (40, 15, 15)),
        ]:
            if rect is None:
                continue
            hov = rect.collidepoint(mx2, my2)
            bg  = th.BTN_HOVER_BG if hov else base_bg
            pygame.draw.rect(screen, bg,        rect, border_radius=6)
            pygame.draw.rect(screen, th.BORDER, rect, 1, border_radius=6)
            ls = th.font(17).render(label, True, th.TEXT)
            screen.blit(ls, (rect.centerx - ls.get_width() // 2,
                              rect.centery - ls.get_height() // 2))

    # ---- Debug overlay ----

    def _draw_debug(self, screen: pygame.Surface) -> None:
        th = self._theme
        lv = self._gs.level()

        # Semi-transparent overlay
        overlay = pygame.Surface((self._sw, self._sh), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 120))
        screen.blit(overlay, (0, 0))

        pw, ph = 540, 260
        px = (self._sw - pw) // 2
        py = (self._sh - ph) // 2

        panel = pygame.Surface((pw, ph), pygame.SRCALPHA)
        panel.fill((10, 18, 32, 252))
        pygame.draw.rect(panel, th.ACCENT, panel.get_rect(), 2)
        screen.blit(panel, (px, py))

        # Title
        title_s = th.font(22).render("DEBUG", True, th.ACCENT)
        screen.blit(title_s, (px + pw // 2 - title_s.get_width() // 2, py + 10))

        # Level / XP info
        info_s = th.font(14).render(
            f"Level {lv}   XP: {_fmt_xp(self._gs.total_xp)}   Money: €{self._gs.money:,.0f}",
            True, th.TEXT,
        )
        screen.blit(info_s, (px + 16, py + 40))

        btn_w  = 64
        btn_h  = 34
        btn_gap = 8
        mx2, my2 = pygame.mouse.get_pos()

        self._debug_btn_rects = []

        # Level buttons
        level_labels = ["LVL 1", "LVL 3", "LVL 5", "LVL 7", "LVL 10", "LVL 15", "MAX"]
        bx = px + 16
        by = py + 72
        for label in level_labels:
            r = pygame.Rect(bx, by, btn_w, btn_h)
            hov = r.collidepoint(mx2, my2)
            bg  = th.BTN_HOVER_BG if hov else th.BTN_NORMAL_BG
            pygame.draw.rect(screen, bg,        r, border_radius=4)
            pygame.draw.rect(screen, th.ACCENT if hov else th.BORDER,
                             r, 1, border_radius=4)
            ls = th.font(13).render(label, True, th.TEXT)
            screen.blit(ls, (r.centerx - ls.get_width() // 2,
                              r.centery - ls.get_height() // 2))
            self._debug_btn_rects.append((label, r))
            bx += btn_w + btn_gap

        # Second row: Reset XP + money buttons
        ry = by + btn_h + 14
        money_labels = [("Reset XP", 100, (40, 15, 15), (200, 60, 60), (220, 80, 80)),
                        ("+€10k",    80,  (15, 40, 20), (60, 200, 80), (80, 220, 120)),
                        ("Reset €",  80,  (30, 30, 15), (160, 160, 40), (200, 200, 60))]
        mx3 = px + 16
        for mlabel, mw, mbg, mbord, mtxt in money_labels:
            r = pygame.Rect(mx3, ry, mw, btn_h)
            hov = r.collidepoint(mx2, my2)
            bg2 = (min(255, mbg[0] + 20), min(255, mbg[1] + 20), min(255, mbg[2] + 20)) if hov else mbg
            pygame.draw.rect(screen, bg2,   r, border_radius=4)
            pygame.draw.rect(screen, mbord, r, 1, border_radius=4)
            ls = th.font(13).render(mlabel, True, mtxt)
            screen.blit(ls, (r.centerx - ls.get_width() // 2,
                              r.centery - ls.get_height() // 2))
            self._debug_btn_rects.append((mlabel, r))
            mx3 += mw + btn_gap

        # Hint
        hint_s = th.font(13).render("F5 or ESC to close", True, th.TEXT_DIM)
        screen.blit(hint_s, (px + pw // 2 - hint_s.get_width() // 2, py + ph - 30))
