"""
game_data.py — In-world game-state structures for Powernet.

Defines building types (generators, consumers, research),
the XP / level-up research system, placed buildings, wires, and the
full GameState that is serialised into WorldMeta.game_data (gzip JSON).
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field
from typing import Optional


# ---------------------------------------------------------------------------
# XP / Level system
# ---------------------------------------------------------------------------

# XP needed to *reach* each level (index 0 = level 1 start = 0 XP).
# Level 1 is free; level 16 requires 100 000 XP.
XP_THRESHOLDS: list[float] = [
    0,          # level 1
    100,        # level 2
    300,        # level 3
    700,        # level 4
    1_500,      # level 5
    2_800,      # level 6
    4_500,      # level 7
    7_000,      # level 8
    10_500,     # level 9
    15_000,     # level 10
    21_000,     # level 11
    29_000,     # level 12
    40_000,     # level 13
    55_000,     # level 14
    75_000,     # level 15
    100_000,    # level 16
]

MAX_LEVEL = len(XP_THRESHOLDS)


def level_from_xp(xp: float) -> int:
    """Return the player level (1-based) for a given total XP value."""
    lv = 1
    for i, thresh in enumerate(XP_THRESHOLDS):
        if xp >= thresh:
            lv = i + 1
        else:
            break
    return lv


# ---------------------------------------------------------------------------
# Building type definitions
# ---------------------------------------------------------------------------


@dataclass
class BuildingTypeDef:
    type_id: str
    name: str
    category: str           # "generators" | "consumers" | "research"
    power_mw: float         # MW produced (positive = generator, negative = consumer)
    size: tuple[int, int] = (1, 1)   # (cols, rows) in grid cells
    description: str = ""
    texture_name: str = ""  # path relative to assets/textures/other/
    unlock_level: int = 1   # minimum player level required to place
    xp_rate: float = 0.0         # XP/second produced (research buildings only)
    max_count: int = 0           # 0 = unlimited; >0 = maximum placeable instances
    player_buildable: bool = True  # False = spawned by world gen only (consumers)
    build_cost: float = 0.0      # one-time € cost to place
    operating_cost: float = 0.0  # €/minute (can be negative = earns money)
    tax_rate: float = 0.0        # €/minute paid by consumers when powered × happiness
    pollution_radius: int = 0    # grid-unit radius of pollution zone (0 = none)


BUILDING_TYPES: dict[str, BuildingTypeDef] = {

    # ── Generators ───────────────────────────────────────────────────────────

    "solar_panel": BuildingTypeDef(
        "solar_panel", "Solarplatte", "generators", 2.0, (1, 1),
        "Einzelne Solarplatte. 2 MW sauberer Solarstrom.",
        "gen/solar-panels.png", unlock_level=1,
        build_cost=600.0, operating_cost=0.2,
    ),
    "biogas_small": BuildingTypeDef(
        "biogas_small", "Biogasanlage", "generators", 5.0, (1, 1),
        "Kleine Biogasanlage. 5 MW aus organischen Abfällen.",
        "gen/bio-1.png", unlock_level=1,
        build_cost=800.0, operating_cost=1.0, pollution_radius=2,
    ),
    "diesel_gen": BuildingTypeDef(
        "diesel_gen", "Dieselgenerator", "generators", 12.0, (1, 1),
        "12 MW Dieselgenerator. Günstig, aber teuer im Betrieb.",
        "gen/gas-gen.png", unlock_level=1,
        build_cost=2_000.0, operating_cost=15.0, pollution_radius=3,
    ),
    "solar_thermal": BuildingTypeDef(
        "solar_thermal", "Solarthermie", "generators", 100.0, (2, 2),
        "100 MW Solarthermisches Kraftwerk.",
        "gen/soalrthermie.png", unlock_level=3,
        build_cost=15_000.0, operating_cost=3.0,
    ),
    "geothermal": BuildingTypeDef(
        "geothermal", "Geothermie", "generators", 150.0, (2, 2),
        "150 MW Geothermisches Kraftwerk. Nutzt Erdwärme.",
        "gen/geothermie-1.png", unlock_level=4,
        build_cost=20_000.0, operating_cost=4.0,
    ),
    "waste_plant": BuildingTypeDef(
        "waste_plant", "Müllverbrennung", "generators", 25.0, (2, 2),
        "25 MW Müllverbrennungsanlage. Erhält Geld für Müllentsorgung.",
        "gen/bio-2.png", unlock_level=4,
        build_cost=8_000.0, operating_cost=-8.0, pollution_radius=4,
    ),
    "gas_plant": BuildingTypeDef(
        "gas_plant", "Gaskraftwerk", "generators", 1_000.0, (3, 3),
        "1 GW Gaskraftwerk. Hocheffizient.",
        "gen/gas-gen.png", unlock_level=6,
        build_cost=60_000.0, operating_cost=80.0, pollution_radius=5,
    ),
    "fusion": BuildingTypeDef(
        "fusion", "Kernfusion", "generators", 10_000.0, (4, 4),
        "10 GW Kernfusionskraftwerk. Endgame-Technologie.",
        "gen/fusion.png", unlock_level=15,
        build_cost=2_000_000.0, operating_cost=300.0,
    ),

    # ── Consumers ────────────────────────────────────────────────────────────

    "bungalow": BuildingTypeDef(
        "bungalow", "Bungalow", "consumers", -1.0, (1, 1),
        "1 MW Wohngebäude. Zahlt Steuern wenn versorgt.",
        "build/bungalow.png", unlock_level=1,
        player_buildable=False, tax_rate=18.0,
    ),
    "house": BuildingTypeDef(
        "house", "Einfamilienhaus", "consumers", -2.0, (1, 1),
        "2 MW Einfamilienhaus. Zahlt Steuern wenn versorgt.",
        "build/house.png", unlock_level=1,
        player_buildable=False, tax_rate=36.0,
    ),
    "small_factory": BuildingTypeDef(
        "small_factory", "Kleine Fabrik", "consumers", -10.0, (1, 1),
        "10 MW Kleine Fabrik. Zahlt Steuern wenn versorgt.",
        "build/small-factory.png", unlock_level=2,
        player_buildable=False, tax_rate=100.0,
    ),
    "multifamily": BuildingTypeDef(
        "multifamily", "Mehrfamilienhaus", "consumers", -25.0, (2, 2),
        "25 MW Mehrfamilienhaus. Zahlt Steuern wenn versorgt.",
        "build/multifamily-house.png", unlock_level=3,
        player_buildable=False, tax_rate=90.0,
    ),
    "warehouse": BuildingTypeDef(
        "warehouse", "Industriehalle", "consumers", -20.0, (2, 2),
        "20 MW Industrielagerhalle. Zahlt Steuern wenn versorgt.",
        "build/industriehalle.png", unlock_level=3,
        player_buildable=False, tax_rate=75.0,
    ),
    "factory": BuildingTypeDef(
        "factory", "Fabrik", "consumers", -200.0, (2, 2),
        "200 MW Fabrik. Zahlt Steuern wenn versorgt.",
        "build/normal-factory.png", unlock_level=5,
        player_buildable=False, tax_rate=450.0,
    ),
    "big_factory": BuildingTypeDef(
        "big_factory", "Große Fabrik", "consumers", -500.0, (3, 3),
        "500 MW Großfabrik. Zahlt Steuern wenn versorgt.",
        "build/big-factory-1.png", unlock_level=6,
        player_buildable=False, tax_rate=1_200.0,
    ),

    # ── Research ─────────────────────────────────────────────────────────────

    "research_small": BuildingTypeDef(
        "research_small", "Kl. Forschung", "research", -2.0, (1, 1),
        "Produziert 1 XP/s.",
        "misc/small-forschung.png", unlock_level=1, xp_rate=1.0,
        build_cost=500.0, operating_cost=1.0,
    ),
    "research": BuildingTypeDef(
        "research", "Forschung", "research", -100.0, (2, 2),
        "Produziert 10 XP/s.",
        "misc/forschung-on.png", unlock_level=4, xp_rate=10.0,
        build_cost=5_000.0, operating_cost=8.0,
    ),
    "research_large": BuildingTypeDef(
        "research_large", "Gr. Forschung", "research", -500.0, (3, 3),
        "Produziert 50 XP/s.",
        "misc/big-forschung.png", unlock_level=7, xp_rate=50.0,
        build_cost=20_000.0, operating_cost=30.0,
    ),
    "research_secret": BuildingTypeDef(
        "research_secret", "Geh. Forschung", "research", -500.0, (2, 2),
        "100 XP/s — Streng geheim. Max. 1 Gebäude.",
        "misc/secret-forschung.png", unlock_level=5, xp_rate=100.0, max_count=1,
        build_cost=100_000.0, operating_cost=50.0,
    ),

    # ── Logistics ─────────────────────────────────────────────────────────────

    "wire_node": BuildingTypeDef(
        "wire_node", "Knotenpunkt", "logistics", 0.0, (1, 1),
        "Drahtknoten. Kostenlos. Keine Energie. Für Leitungsorganisation.",
        "", unlock_level=1,
        build_cost=0.0, operating_cost=0.0,
        player_buildable=True,
    ),
}

# Cost to demolish a consumer building (not player-built)
BULLDOZE_CONSUMER_COST: float = 1_000.0

# Ordered category tab list
CATEGORY_ORDER: list[str] = ["generators", "consumers", "research", "logistics"]

# Category display labels
CATEGORY_LABELS: dict[str, str] = {
    "generators": "Generators",
    "consumers":  "Consumers",
    "research":   "Forschung",
    "logistics":  "Logistik",
}


# ---------------------------------------------------------------------------
# Placed building
# ---------------------------------------------------------------------------


@dataclass
class PlacedBuilding:
    type_id: str
    gx: int                 # top-left grid column
    gy: int                 # top-left grid row
    active: bool = True
    build_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])

    @property
    def typedef(self) -> BuildingTypeDef:
        return BUILDING_TYPES.get(
            self.type_id,
            BuildingTypeDef(self.type_id, "Unknown", "generators", 0.0),
        )

    @property
    def size(self) -> tuple[int, int]:
        return self.typedef.size

    def occupies(self, gx: int, gy: int) -> bool:
        sw, sh = self.size
        return self.gx <= gx < self.gx + sw and self.gy <= gy < self.gy + sh

    # ---- serialisation ----

    def to_dict(self) -> dict:
        return {
            "type_id":  self.type_id,
            "gx":       self.gx,
            "gy":       self.gy,
            "active":   self.active,
            "build_id": self.build_id,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "PlacedBuilding":
        return cls(
            type_id=d["type_id"],
            gx=d["gx"],
            gy=d["gy"],
            active=d.get("active", True),
            build_id=d.get("build_id", uuid.uuid4().hex[:8]),
        )


# ---------------------------------------------------------------------------
# Wire
# ---------------------------------------------------------------------------


@dataclass
class Wire:
    from_id: str    # build_id of source building
    to_id: str      # build_id of target building
    layer: int = 0

    def to_dict(self) -> dict:
        return {"from_id": self.from_id, "to_id": self.to_id, "layer": self.layer}

    @classmethod
    def from_dict(cls, d: dict) -> "Wire":
        return cls(from_id=d["from_id"], to_id=d["to_id"], layer=d.get("layer", 0))


# ---------------------------------------------------------------------------
# Full game state
# ---------------------------------------------------------------------------


@dataclass
class GameState:
    wire_layers: int = 2
    grid_w: int = 64
    grid_h: int = 64
    buildings: list[PlacedBuilding] = field(default_factory=list)
    wires: list[Wire] = field(default_factory=list)
    cam_x: float = 0.0
    cam_y: float = 0.0
    total_xp: float = 0.0   # accumulated research XP
    money: float = 15_000.0  # starting cash

    # ---- serialisation ----

    def to_dict(self) -> dict:
        return {
            "wire_layers": self.wire_layers,
            "grid_w":      self.grid_w,
            "grid_h":      self.grid_h,
            "buildings":   [b.to_dict() for b in self.buildings],
            "wires":       [w.to_dict() for w in self.wires],
            "cam_x":       self.cam_x,
            "cam_y":       self.cam_y,
            "total_xp":    self.total_xp,
            "money":       self.money,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "GameState":
        gs = cls(
            wire_layers=d.get("wire_layers", 2),
            grid_w=d.get("grid_w", 64),
            grid_h=d.get("grid_h", 64),
            cam_x=d.get("cam_x", 0.0),
            cam_y=d.get("cam_y", 0.0),
            total_xp=d.get("total_xp", 0.0),
            money=d.get("money", 15_000.0),
        )
        gs.buildings = [PlacedBuilding.from_dict(b) for b in d.get("buildings", [])]
        gs.wires     = [Wire.from_dict(w)            for w in d.get("wires", [])]
        return gs

    # ---- level / XP queries ----

    def level(self) -> int:
        return level_from_xp(self.total_xp)

    def xp_for_current_level(self) -> float:
        lv = self.level()
        return XP_THRESHOLDS[lv - 1]

    def xp_for_next_level(self) -> float:
        lv = self.level()
        if lv >= MAX_LEVEL:
            return XP_THRESHOLDS[-1]
        return XP_THRESHOLDS[lv]

    def xp_progress(self) -> float:
        """0.0 … 1.0 progress towards the next level."""
        lv = self.level()
        if lv >= MAX_LEVEL:
            return 1.0
        cur = XP_THRESHOLDS[lv - 1]
        nxt = XP_THRESHOLDS[lv]
        return (self.total_xp - cur) / max(1, nxt - cur)

    def xp_per_second(self, powered: Optional[set[str]] = None) -> float:
        """Total XP/s from active research buildings that are powered."""
        total = 0.0
        for b in self.buildings:
            if b.active:
                td = BUILDING_TYPES.get(b.type_id)
                if td and td.xp_rate > 0:
                    # If powered set provided, only count powered buildings
                    if powered is None or b.build_id in powered:
                        total += td.xp_rate
        return total

    # ---- power queries ----

    def total_generation_mw(self) -> float:
        total = 0.0
        for b in self.buildings:
            if b.active:
                td = BUILDING_TYPES.get(b.type_id)
                if td and td.power_mw > 0:
                    total += td.power_mw
        return total

    def total_consumption_mw(self, powered: Optional[set[str]] = None) -> float:
        """Returns a positive value (absolute consumption of powered consumers/research)."""
        total = 0.0
        for b in self.buildings:
            if b.active:
                td = BUILDING_TYPES.get(b.type_id)
                if td and td.power_mw < 0:
                    if powered is None or b.build_id in powered:
                        total += abs(td.power_mw)
        return total

    def net_power_mw(self, powered: Optional[set[str]] = None) -> float:
        return self.total_generation_mw() - self.total_consumption_mw(powered)

    def total_power_mw(self) -> float:
        """Legacy helper (net power)."""
        return self.net_power_mw()

    # ---- economy queries ----

    def polluted_tiles(self) -> set[tuple[int, int]]:
        """Return all grid tiles within any generator's pollution radius."""
        result: set[tuple[int, int]] = set()
        for b in self.buildings:
            td = BUILDING_TYPES.get(b.type_id)
            if not td or td.pollution_radius <= 0:
                continue
            cx = b.gx + td.size[0] * 0.5
            cy = b.gy + td.size[1] * 0.5
            r  = td.pollution_radius
            r2 = r * r
            for tx in range(int(cx - r) - 1, int(cx + r) + 2):
                for ty in range(int(cy - r) - 1, int(cy + r) + 2):
                    if (tx + 0.5 - cx) ** 2 + (ty + 0.5 - cy) ** 2 <= r2:
                        result.add((tx, ty))
        return result

    def happiness(self, powered: set[str]) -> float:
        """0.0–1.0.  Fraction of consumer buildings that are powered."""
        consumers = [b for b in self.buildings
                     if BUILDING_TYPES.get(b.type_id) and
                     BUILDING_TYPES[b.type_id].category == "consumers"]
        if not consumers:
            return 1.0
        powered_count = sum(1 for b in consumers if b.build_id in powered)
        return powered_count / len(consumers)

    def tax_income_per_minute(
        self,
        powered: set[str],
        polluted: Optional[set[tuple[int, int]]] = None,
    ) -> float:
        """€/min from all powered consumer buildings, scaled by happiness.
        Buildings in a polluted zone pay only 50% tax."""
        h = self.happiness(powered)
        total = 0.0
        for b in self.buildings:
            td = BUILDING_TYPES.get(b.type_id)
            if td and td.tax_rate > 0 and b.build_id in powered:
                rate = td.tax_rate * h
                if polluted:
                    sw, sh = td.size
                    if any((b.gx + dx, b.gy + dy) in polluted
                           for dx in range(sw) for dy in range(sh)):
                        rate *= 0.5
                total += rate
        return total

    def operating_costs_per_minute(self) -> float:
        """€/min total operating costs (positive = cost, negative = income)
        for all active player-buildable buildings."""
        total = 0.0
        for b in self.buildings:
            if not b.active:
                continue
            td = BUILDING_TYPES.get(b.type_id)
            if td and td.player_buildable:
                total += td.operating_cost
        return total

    def net_income_per_minute(
        self,
        powered: set[str],
        polluted: Optional[set[tuple[int, int]]] = None,
    ) -> float:
        """€/min = tax income – operating costs."""
        return self.tax_income_per_minute(powered, polluted) - self.operating_costs_per_minute()

    # ---- queries ----

    def get_building_by_id(self, build_id: str) -> Optional[PlacedBuilding]:
        for b in self.buildings:
            if b.build_id == build_id:
                return b
        return None

    def get_building_at(self, gx: int, gy: int) -> Optional[PlacedBuilding]:
        for b in self.buildings:
            if b.occupies(gx, gy):
                return b
        return None

    def is_occupied(self, gx: int, gy: int, size: tuple[int, int] = (1, 1)) -> bool:
        sw, sh = size
        for cx in range(gx, gx + sw):
            for cy in range(gy, gy + sh):
                if self.get_building_at(cx, cy) is not None:
                    return True
        return False

    def in_bounds(self, gx: int, gy: int, size: tuple[int, int] = (1, 1)) -> bool:
        sw, sh = size
        return (0 <= gx and gx + sw <= self.grid_w
                and 0 <= gy and gy + sh <= self.grid_h)

    def count_of_type(self, type_id: str) -> int:
        return sum(1 for b in self.buildings if b.type_id == type_id)

    # ---- mutations ----

    def place_building(self, type_id: str, gx: int, gy: int) -> Optional[PlacedBuilding]:
        td = BUILDING_TYPES.get(type_id)
        if td is None:
            return None
        # Check max_count constraint
        if td.max_count > 0 and self.count_of_type(type_id) >= td.max_count:
            return None
        if not self.in_bounds(gx, gy, td.size) or self.is_occupied(gx, gy, td.size):
            return None
        b = PlacedBuilding(type_id=type_id, gx=gx, gy=gy)
        self.buildings.append(b)
        return b

    def remove_building(self, build_id: str) -> bool:
        b = self.get_building_by_id(build_id)
        if b is None:
            return False
        self.buildings.remove(b)
        self.wires = [w for w in self.wires
                      if w.from_id != build_id and w.to_id != build_id]
        return True

    def add_wire(self, from_id: str, to_id: str, layer: int = 0) -> Optional[Wire]:
        if from_id == to_id:
            return None
        if (self.get_building_by_id(from_id) is None
                or self.get_building_by_id(to_id) is None):
            return None
        for w in self.wires:
            if w.layer == layer and (
                (w.from_id == from_id and w.to_id == to_id)
                or (w.from_id == to_id and w.to_id == from_id)
            ):
                return None
        wire = Wire(from_id=from_id, to_id=to_id, layer=layer)
        self.wires.append(wire)
        return wire

    def remove_wire(self, wire: Wire) -> bool:
        if wire in self.wires:
            self.wires.remove(wire)
            return True
        return False

    def wires_for_building(self, build_id: str) -> list[Wire]:
        return [w for w in self.wires
                if w.from_id == build_id or w.to_id == build_id]
