"""
FreeExile Open-World Wilderness Map Generator.
Generates organic, replayable wilderness tile maps for 10 canonical zones (60x45 to 120x90).
Guarantees sinuous main path (sinuosity >= 1.25), 2-3 encounter clusters, rewarded dead-ends,
isolated sealed Boss Gate, 1-3 POIs, and an 8-tile radius safe floor clearing at spawn.
"""

from __future__ import annotations
import math
import random
from typing import List, Tuple, Dict, Optional, Set, Union
from server.world.map_data_types import (
    TileType,
    TileCell,
    MapGridData,
    MapRoom,
    EncounterZone,
    ObstacleType,
    ObstacleInstance,
)
from server.world.map_biome_catalog import (
    get_biome_definition,
    get_biome_by_code,
    MapBiomeDefinition,
)

ZONE_DEFAULT_SIZES: Dict[str, Tuple[int, int]] = {
    "zone_tang_kiem_nhai": (60, 45),
    "zone_ancient_sword_barrow": (68, 52),
    "zone_boundless_sandstorm": (76, 58),
    "zone_blood_scale_ruins": (84, 64),
    "zone_five_elements_altar": (92, 70),
    "zone_abyssal_ice_pond": (100, 76),
    "zone_infinite_blood_rift": (108, 82),
    "zone_purgatory_lava_cavern": (114, 86),
    "zone_boundless_celestial_palace": (120, 90),
}

ZONE_CANONICAL_BIOMES: Dict[str, str] = {
    "zone_tang_kiem_nhai": "BLEACHED_BONE_CANYON",
    "zone_ancient_sword_barrow": "STY_01_HOANG_MANG_CO_LO",
    "zone_boundless_sandstorm": "STY_12_SA_COT_HOANG_MAC",
    "zone_blood_scale_ruins": "CRIMSON_BLOOD_FOREST",
    "zone_five_elements_altar": "STY_23_TE_DAN_MAN_HOANG",
    "zone_abyssal_ice_pond": "STY_08_VUC_DAM_BANG_DOC",
    "zone_infinite_blood_rift": "STY_27_VET_NUT_VO_HAN_HUYET_GIOI",
    "zone_purgatory_lava_cavern": "STY_19_HAM_LUA_LUYEN_NGUC",
    "zone_boundless_celestial_palace": "STY_30_TAN_TICH_THIEN_CUNG_HOANG_PHE",
}


class WildernessMapGenerator:
    """Organic procedural wilderness terrain generator adhering to PoE2 standards."""

    def __init__(self, width: int = 60, height: int = 45, biome_id: Union[str, int] = "BLEACHED_BONE_CANYON") -> None:
        self.width = max(48, width)
        self.height = max(36, height)
        if isinstance(biome_id, int) or (isinstance(biome_id, str) and str(biome_id).isdigit()):
            defn = get_biome_by_code(int(biome_id))
            self.biome_id = defn.biome_id if defn else "BLEACHED_BONE_CANYON"
        else:
            self.biome_id = str(biome_id)

    @classmethod
    def for_zone(
        cls,
        zone_id: str,
        seed: int = 0,
        biome_id: Optional[Union[str, int]] = None,
        player_level: int = 1,
        map_tier: Optional[int] = None,
    ) -> WildernessMapGenerator:
        from server.world.map_progression_scaler import calculate_map_scale_profile
        if map_tier is not None and map_tier > 0:
            profile = calculate_map_scale_profile(level=player_level, map_tier=map_tier)
            dims = (profile.grid_width, profile.grid_height)
        elif player_level > 10:
            profile = calculate_map_scale_profile(level=player_level, zone_id=zone_id)
            dims = (profile.grid_width, profile.grid_height)
        else:
            dims = ZONE_DEFAULT_SIZES.get(zone_id, (60, 45))

        if biome_id is not None:
            if isinstance(biome_id, int) or (isinstance(biome_id, str) and str(biome_id).isdigit()):
                defn = get_biome_by_code(int(biome_id))
                b_id = defn.biome_id if defn else "BLEACHED_BONE_CANYON"
            else:
                b_id = str(biome_id)
        else:
            b_id = ZONE_CANONICAL_BIOMES.get(zone_id, "BLEACHED_BONE_CANYON")
        return cls(width=dims[0], height=dims[1], biome_id=b_id)

    def generate(self, seed: int, zone_id: Optional[str] = None) -> MapGridData:
        rng = random.Random(seed)
        biome = get_biome_definition(self.biome_id)
        tiles = [
            [TileCell(x=x, y=y, tile_type=TileType.WALL, walkable=False, blocks_vision=True) for x in range(self.width)]
            for y in range(self.height)
        ]

        sx, sy = 10, min(self.height - 11, max(10, self.height - 13))
        self._carve_spawn_safe_zone(tiles, sx, sy, radius=8.0)

        bw = max(10, min(14, self.width // 4))
        bh = max(8, min(12, self.height // 3))
        bx = self.width - bw - 3
        by = 3
        boss_gate, boss_room = self._carve_boss_arena(tiles, bw, bh, bx, by)

        encounters, waypoints = self._build_encounter_zones(tiles, sx, sy, boss_gate[0], boss_gate[1], rng)
        path_nodes = [(sx, sy)] + waypoints + [(boss_gate[0] - 1, boss_gate[1])]
        self._carve_sinuous_path(tiles, path_nodes, rng)
        dead_ends = self._carve_dead_ends(tiles, waypoints, rng, (bx, by, bw, bh))
        pois = self._place_pois(tiles, waypoints, dead_ends, rng)
        self._decorate_terrain(tiles, rng, biome, (sx, sy, 8.5), (bx, by, bw, bh))
        self._seal_boss_perimeter(tiles, bx, by, bw, bh, boss_gate)

        obstacles: Dict[Tuple[int, int], ObstacleInstance] = {
            boss_gate: ObstacleInstance("obs_boss_seal", boss_gate[0], boss_gate[1], ObstacleType.BOSS_SEAL_BARRIER, hp=200, blocks_movement=True, blocks_vision=True)
        }

        return MapGridData(
            width=self.width,
            height=self.height,
            seed=seed,
            biome=self.biome_id,
            tiles=tiles,
            obstacles=obstacles,
            rooms=[boss_room],
            spawn_point=(sx, sy),
            boss_point=(bx + bw // 2, by + bh // 2),
            poi_points=pois,
            dead_end_points=dead_ends,
            boss_room=boss_room,
            boss_gate=boss_gate,
            encounter_zones=encounters,
        )

    def _carve_spawn_safe_zone(self, tiles: List[List[TileCell]], sx: int, sy: int, radius: float) -> None:
        r_sq = radius * radius
        for y in range(max(1, int(sy - radius - 1)), min(self.height - 1, int(sy + radius + 2))):
            for x in range(max(1, int(sx - radius - 1)), min(self.width - 1, int(sx + radius + 2))):
                if (x - sx) ** 2 + (y - sy) ** 2 <= r_sq:
                    c = tiles[y][x]
                    c.tile_type, c.walkable, c.blocks_vision, c.movement_cost = TileType.FLOOR, True, False, 1.0

    def _carve_boss_arena(
        self, tiles: List[List[TileCell]], bw: int, bh: int, bx: int, by: int
    ) -> Tuple[Tuple[int, int], MapRoom]:
        for y in range(by, by + bh):
            for x in range(bx, bx + bw):
                c = tiles[y][x]
                if x == bx or x == bx + bw - 1 or y == by or y == by + bh - 1:
                    c.tile_type, c.walkable, c.blocks_vision = TileType.WALL, False, True
                else:
                    c.tile_type, c.walkable, c.blocks_vision, c.movement_cost = TileType.FLOOR, True, False, 1.0

        cx, cy = bx + bw // 2, by + bh // 2
        for ry in range(by + 1, by + bh - 1):
            for rx in range(bx + 1, bx + bw - 1):
                if rx == cx and ry == cy:
                    tiles[ry][rx].tile_type = TileType.BOSS_ALTAR
                elif max(abs(rx - cx), abs(ry - cy)) <= 1:
                    tiles[ry][rx].tile_type = TileType.RUNIC_FLOOR

        gx, gy = bx, cy
        tiles[gy][gx].tile_type, tiles[gy][gx].walkable, tiles[gy][gx].blocks_vision = TileType.BOSS_GATE, False, True
        tiles[gy][gx - 1].tile_type, tiles[gy][gx - 1].walkable = TileType.FLOOR, True
        tiles[gy][gx - 1].blocks_vision = False

        boss_room = MapRoom(
            room_id=99, x=bx, y=by, w=bw, h=bh, room_type="boss", is_boss_room=True,
            boss_gate_pos=(gx, gy), boss_altar_pos=(cx, cy)
        )
        return (gx, gy), boss_room

    def _seal_boss_perimeter(
        self, tiles: List[List[TileCell]], bx: int, by: int, bw: int, bh: int, boss_gate: Tuple[int, int]
    ) -> None:
        gx, gy = boss_gate
        for x in range(bx, bx + bw):
            tiles[by][x].tile_type = tiles[by + bh - 1][x].tile_type = TileType.WALL
            tiles[by][x].walkable = tiles[by + bh - 1][x].walkable = False
            tiles[by][x].blocks_vision = tiles[by + bh - 1][x].blocks_vision = True
        for y in range(by, by + bh):
            tiles[y][bx].tile_type = tiles[y][bx + bw - 1].tile_type = TileType.WALL
            tiles[y][bx].walkable = tiles[y][bx + bw - 1].walkable = False
            tiles[y][bx].blocks_vision = tiles[y][bx + bw - 1].blocks_vision = True

        tiles[gy][gx].tile_type, tiles[gy][gx].walkable, tiles[gy][gx].blocks_vision = TileType.BOSS_GATE, False, True
        tiles[gy][gx - 1].tile_type, tiles[gy][gx - 1].walkable, tiles[gy][gx - 1].blocks_vision = TileType.FLOOR, True, False

    def _build_encounter_zones(
        self, tiles: List[List[TileCell]], sx: int, sy: int, gx: int, gy: int, rng: random.Random
    ) -> Tuple[List[EncounterZone], List[Tuple[int, int]]]:
        if self.width >= 180:
            count = 5
            fractions = [0.18, 0.36, 0.54, 0.72, 0.88]
            tiers = [1, 2, 2, 3, 3]
        elif self.width >= 110:
            count = 4
            fractions = [0.22, 0.44, 0.66, 0.85]
            tiers = [1, 2, 2, 3]
        elif self.width >= 56:
            count = 3
            fractions = [0.30, 0.58, 0.82]
            tiers = [1, 2, 3]
        else:
            count = 2
            fractions = [0.38, 0.72]
            tiers = [1, 2]
        encounters: List[EncounterZone] = []
        waypoints: List[Tuple[int, int]] = []
        tile_types = [TileType.ENCOUNTER_LOW, TileType.ENCOUNTER_MEDIUM, TileType.ENCOUNTER_HIGH]

        for i, frac in enumerate(fractions):
            base_x = int(sx + frac * (gx - sx))
            if i % 2 == 0:
                base_y = max(6, min(14, int(sy * 0.35 + rng.randint(-1, 2))))
            else:
                base_y = max(self.height - 14, min(self.height - 6, int(self.height * 0.75 + rng.randint(-2, 1))))

            r = 4 if self.width < 70 else 5
            min_x, max_x = max(2, base_x - r), min(self.width - 3, base_x + r)
            min_y, max_y = max(2, base_y - r), min(self.height - 3, base_y + r)
            tt = tile_types[i] if i < len(tile_types) else TileType.ENCOUNTER_HIGH

            for y in range(min_y, max_y + 1):
                for x in range(min_x, max_x + 1):
                    if ((x - base_x) / (r + 0.5)) ** 2 + ((y - base_y) / (r + 0.5)) ** 2 <= 1.0:
                        c = tiles[y][x]
                        c.tile_type, c.walkable, c.blocks_vision, c.movement_cost = tt, True, False, 1.0

            encounters.append(EncounterZone(min_x, min_y, max_x, max_y, tier=tiers[i]))
            waypoints.append((base_x, base_y))

        return encounters, waypoints

    def _carve_sinuous_path(
        self, tiles: List[List[TileCell]], nodes: List[Tuple[int, int]], rng: random.Random
    ) -> None:
        for i in range(len(nodes) - 1):
            x1, y1 = nodes[i]
            x2, y2 = nodes[i + 1]
            dx, dy = x2 - x1, y2 - y1
            dist = math.hypot(dx, dy)
            if dist < 2:
                continue

            perp_x, perp_y = -dy / dist, dx / dist
            bend_dist = rng.uniform(4.5, 7.5) * (1 if i % 2 == 0 else -1)
            mx = max(3, min(self.width - 4, int((x1 + x2) / 2 + perp_x * bend_dist)))
            my = max(3, min(self.height - 4, int((y1 + y2) / 2 + perp_y * bend_dist)))

            self._carve_polyline(tiles, [(x1, y1), (mx, my), (x2, y2)], width=2)

    def _carve_polyline(self, tiles: List[List[TileCell]], pts: List[Tuple[int, int]], width: int = 2) -> None:
        for k in range(len(pts) - 1):
            x1, y1 = pts[k]
            x2, y2 = pts[k + 1]
            steps = max(abs(x2 - x1), abs(y2 - y1)) * 2
            if steps == 0:
                continue
            for s in range(steps + 1):
                t = s / steps
                cx, cy = int(round(x1 + t * (x2 - x1))), int(round(y1 + t * (y2 - y1)))
                for wy in range(max(1, cy - 1), min(self.height - 1, cy + width)):
                    for wx in range(max(1, cx - 1), min(self.width - 1, cx + width)):
                        c = tiles[wy][wx]
                        if c.tile_type not in (TileType.BOSS_GATE, TileType.BOSS_ALTAR, TileType.RUNIC_FLOOR):
                            c.tile_type, c.walkable, c.blocks_vision, c.movement_cost = TileType.PATH, True, False, 0.8

    def _carve_dead_ends(
        self, tiles: List[List[TileCell]], waypoints: List[Tuple[int, int]], rng: random.Random, boss_box: Tuple[int, int, int, int]
    ) -> List[Tuple[int, int]]:
        dead_ends: List[Tuple[int, int]] = []
        bx, by, bw, bh = boss_box
        count = min(2, max(1, len(waypoints)))

        for i in range(count):
            wx, wy = waypoints[i]
            dir_y = -1 if wy > self.height // 2 else 1
            length = rng.randint(7, 11)
            ex = max(3, min(self.width - 4, wx + rng.randint(-2, 2)))
            ey = max(3, min(self.height - 4, wy + dir_y * length))

            if bx - 2 <= ex <= bx + bw + 2 and by - 2 <= ey <= by + bh + 2:
                continue

            self._carve_polyline(tiles, [(wx, wy), (ex, ey)], width=2)
            for dy in range(-2, 3):
                for dx in range(-2, 3):
                    nx, ny = ex + dx, ey + dy
                    if 1 <= nx < self.width - 1 and 1 <= ny < self.height - 1:
                        c = tiles[ny][nx]
                        c.tile_type, c.walkable, c.blocks_vision, c.movement_cost = TileType.FLOOR, True, False, 1.0
            dead_ends.append((ex, ey))

        return dead_ends

    def _place_pois(
        self, tiles: List[List[TileCell]], waypoints: List[Tuple[int, int]], dead_ends: List[Tuple[int, int]], rng: random.Random
    ) -> List[Tuple[int, int, str]]:
        pois: List[Tuple[int, int, str]] = []
        candidates: List[Tuple[int, int, str]] = []

        for de in dead_ends:
            candidates.append((de[0], de[1], "cache_shrine"))
        if waypoints:
            mid = waypoints[len(waypoints) // 2]
            candidates.append((mid[0] + 2, mid[1], "merchant_post"))
        if len(waypoints) > 1:
            candidates.append((waypoints[0][0] - 2, waypoints[0][1] + 1, "ancient_tablet"))

        for px, py, p_kind in candidates[:3]:
            if 1 <= px < self.width - 1 and 1 <= py < self.height - 1:
                tiles[py][px].tile_type = TileType.POI
                tiles[py][px].walkable = True
                tiles[py][px].blocks_vision = False
                pois.append((px, py, p_kind))

        return pois

    def _decorate_terrain(
        self,
        tiles: List[List[TileCell]],
        rng: random.Random,
        biome: MapBiomeDefinition,
        spawn_safe: Tuple[int, int, float],
        boss_box: Tuple[int, int, int, int],
    ) -> None:
        sx, sy, sr = spawn_safe
        bx, by, bw, bh = boss_box
        sr_sq = sr * sr

        for y in range(2, self.height - 2):
            for x in range(2, self.width - 2):
                if (x - sx) ** 2 + (y - sy) ** 2 <= sr_sq:
                    continue
                if bx <= x < bx + bw and by <= y < by + bh:
                    continue
                c = tiles[y][x]
                if c.tile_type == TileType.FLOOR:
                    r_val = rng.random()
                    if r_val < 0.12:
                        c.tile_type, c.movement_cost = TileType.DENSE_TERRAIN, 1.43
                    elif r_val < 0.17 and biome.primary_tile != TileType.FLOOR:
                        c.tile_type, c.movement_cost = biome.primary_tile, 2.0
