"""
FreeExile Procedural Map & Anti-Bot Dynamic Obstacle Generation Engine.
Implements scalable organic layouts, isolated Boss Arena, and anti-bot obstacles.
"""

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


class ProceduralMapEngine:
    """High-performance procedural dungeon & anti-bot maze synthesizer."""

    def __init__(
        self, width: int = 48, height: int = 48, min_room_size: int = 6, max_rooms: Optional[int] = None
    ):
        self.width = width
        self.height = height
        self.min_room_size = min_room_size
        self.max_rooms = max_rooms if max_rooms is not None else max(5, int((width * height) / 280))

    @classmethod
    def for_progression(
        cls,
        level: int = 1,
        act_id: Optional[str] = None,
        map_tier: Optional[int] = None,
        zone_id: Optional[str] = None,
    ) -> ProceduralMapEngine:
        from server.world.map_progression_scaler import calculate_map_scale_profile
        profile = calculate_map_scale_profile(level=level, act_id=act_id, map_tier=map_tier, zone_id=zone_id)
        return cls(
            width=profile.grid_width,
            height=profile.grid_height,
            min_room_size=6,
            max_rooms=profile.max_rooms,
        )

    def generate_map(self, seed: int, biome_id: str = "BLEACHED_BONE_CANYON") -> MapGridData:
        rng = random.Random(seed)
        biome = get_biome_definition(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)
        ]

        rooms = self._generate_rooms(rng)
        spawn_room = rooms[0]
        boss_cand = max(
            rooms[1:],
            key=lambda r: math.hypot(r.center[0] - spawn_room.center[0], r.center[1] - spawn_room.center[1]),
        )
        normal_rooms = [r for r in rooms if r.room_id != boss_cand.room_id]
        boss_room = self._build_grand_boss_room(boss_cand, normal_rooms)

        self._carve_rooms(tiles, normal_rooms)
        self._carve_boss_room_interior(tiles, boss_room)
        self._connect_normal_rooms(tiles, normal_rooms, boss_room)
        boss_gate_coord = self._connect_and_gate_boss_room(tiles, normal_rooms, boss_room)
        dead_ends = self._carve_dead_ends(tiles, normal_rooms, rng, boss_room)
        self._embellish_boss_arena(tiles, boss_room)
        obstacles = self._inject_anti_bot_obstacles(tiles, normal_rooms, boss_room, boss_gate_coord, rng, biome)

        boss_room = MapRoom(
            room_id=boss_room.room_id,
            x=boss_room.x,
            y=boss_room.y,
            w=boss_room.w,
            h=boss_room.h,
            room_type="boss",
            is_boss_room=True,
            boss_gate_pos=boss_gate_coord,
            boss_altar_pos=boss_room.center,
        )

        return MapGridData(
            width=self.width,
            height=self.height,
            seed=seed,
            biome=biome_id,
            tiles=tiles,
            obstacles=obstacles,
            rooms=normal_rooms + [boss_room],
            spawn_point=spawn_room.center,
            boss_point=boss_room.center,
            dead_end_points=dead_ends,
            boss_room=boss_room,
            boss_gate=boss_gate_coord,
            boss_gate_breached=False,
        )

    def _generate_rooms(self, rng: random.Random) -> List[MapRoom]:
        rooms: List[MapRoom] = []
        eff_min = min(self.min_room_size, max(4, self.width // 7))
        attempts = 0
        multiplier = 45 if self.max_rooms >= 20 else 25
        while len(rooms) < self.max_rooms and attempts < self.max_rooms * multiplier:
            attempts += 1
            w, h = rng.randint(eff_min, eff_min + 3), rng.randint(eff_min, eff_min + 3)
            x, y = rng.randint(2, self.width - w - 3), rng.randint(2, self.height - h - 3)
            new_room = MapRoom(room_id=len(rooms), x=x, y=y, w=w, h=h)
            if not any(new_room.intersects(r, padding=2) for r in rooms):
                rooms.append(new_room)
        if len(rooms) < 2:
            rooms = [MapRoom(0, 3, 3, 6, 6), MapRoom(1, self.width - 10, self.height - 10, 7, 7)]
        return rooms

    def _build_grand_boss_room(self, cand: MapRoom, normal_rooms: List[MapRoom]) -> MapRoom:
        eff_min = min(self.min_room_size, max(4, self.width // 7))
        target_size = min(self.width - 6, max(cand.w, eff_min + 2))
        bw, bh = min(self.width - 6, target_size), min(self.height - 6, target_size)
        bx = max(2, min(self.width - bw - 2, cand.x))
        by = max(2, min(self.height - bh - 2, cand.y))
        cand_br = MapRoom(cand.room_id, bx, by, bw, bh, room_type="boss", is_boss_room=True)
        if any(cand_br.intersects(r, padding=1) for r in normal_rooms):
            return MapRoom(cand.room_id, cand.x, cand.y, cand.w, cand.h, room_type="boss", is_boss_room=True)
        return cand_br

    def _carve_rooms(self, tiles: List[List[TileCell]], rooms: List[MapRoom]) -> None:
        for r in rooms:
            for ry in range(r.y, r.y + r.h):
                for rx in range(r.x, r.x + r.w):
                    c = tiles[ry][rx]
                    c.tile_type, c.walkable, c.blocks_vision = TileType.FLOOR, True, False

    def _carve_boss_room_interior(self, tiles: List[List[TileCell]], boss_room: MapRoom) -> None:
        for ry in range(boss_room.y + 1, boss_room.y + boss_room.h - 1):
            for rx in range(boss_room.x + 1, boss_room.x + boss_room.w - 1):
                c = tiles[ry][rx]
                c.tile_type, c.walkable, c.blocks_vision = TileType.FLOOR, True, False

    def _carve_corridor_path(
        self, tiles: List[List[TileCell]], start: Tuple[int, int], end: Tuple[int, int], forbidden_room: Optional[MapRoom] = None
    ) -> None:
        frontier = [(0.0, start)]
        came_from = {start: None}
        cost_so_far = {start: 0.0}
        reached = False

        while frontier:
            _, current = heapq.heappop(frontier)
            if current == end:
                reached = True
                break
            cx, cy = current
            for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                nx, ny = cx + dx, cy + dy
                if 1 <= nx < self.width - 1 and 1 <= ny < self.height - 1:
                    if forbidden_room and forbidden_room.contains(nx, ny):
                        continue
                    new_cost = cost_so_far[current] + (1.0 if tiles[ny][nx].walkable else 3.0)
                    if (nx, ny) not in cost_so_far or new_cost < cost_so_far[(nx, ny)]:
                        cost_so_far[(nx, ny)] = new_cost
                        h = abs(end[0] - nx) + abs(end[1] - ny)
                        heapq.heappush(frontier, (new_cost + h, (nx, ny)))
                        came_from[(nx, ny)] = current

        if reached:
            curr = end
            while curr is not None:
                tiles[curr[1]][curr[0]].tile_type, tiles[curr[1]][curr[0]].walkable, tiles[curr[1]][curr[0]].blocks_vision = TileType.FLOOR, True, False
                for dx, dy in [(1, 0), (0, 1)]:
                    wx, wy = curr[0] + dx, curr[1] + dy
                    if 1 <= wx < self.width - 1 and 1 <= wy < self.height - 1:
                        if not (forbidden_room and forbidden_room.contains(wx, wy)):
                            tiles[wy][wx].tile_type, tiles[wy][wx].walkable, tiles[wy][wx].blocks_vision = TileType.FLOOR, True, False
                curr = came_from[curr]

    def _connect_normal_rooms(
        self, tiles: List[List[TileCell]], normal_rooms: List[MapRoom], boss_room: MapRoom
    ) -> None:
        chain = [normal_rooms[0]]
        unvisited = set(normal_rooms[1:])
        while unvisited:
            nxt = min(unvisited, key=lambda r: math.hypot(r.center[0] - chain[-1].center[0], r.center[1] - chain[-1].center[1]))
            chain.append(nxt)
            unvisited.remove(nxt)

        for i in range(len(chain) - 1):
            self._carve_corridor_path(tiles, chain[i].center, chain[i + 1].center, forbidden_room=boss_room)
        if len(chain) >= 4:
            self._carve_corridor_path(tiles, chain[0].center, chain[len(chain) // 2].center, forbidden_room=boss_room)

    def _connect_and_gate_boss_room(
        self, tiles: List[List[TileCell]], normal_rooms: List[MapRoom], boss_room: MapRoom
    ) -> Tuple[int, int]:
        entry_room = min(normal_rooms, key=lambda r: math.hypot(r.center[0] - boss_room.center[0], r.center[1] - boss_room.center[1]))
        ecx, ecy, bcx, bcy = entry_room.center[0], entry_room.center[1], boss_room.center[0], boss_room.center[1]

        if abs(ecx - bcx) >= abs(ecy - bcy):
            gx, gy = (boss_room.x if ecx < bcx else boss_room.x + boss_room.w - 1), bcy
            approach = (max(1, gx - 1) if ecx < bcx else min(self.width - 2, gx + 1), gy)
        else:
            gx, gy = bcx, (boss_room.y if ecy < bcy else boss_room.y + boss_room.h - 1)
            approach = (gx, max(1, gy - 1) if ecy < bcy else min(self.height - 2, gy + 1))

        self._carve_corridor_path(tiles, (ecx, ecy), approach, forbidden_room=boss_room)

        # Seal all perimeter cells of the Boss Arena as solid walls
        for x in range(boss_room.x, boss_room.x + boss_room.w):
            tiles[boss_room.y][x].tile_type = tiles[boss_room.y + boss_room.h - 1][x].tile_type = TileType.WALL
            tiles[boss_room.y][x].walkable = tiles[boss_room.y + boss_room.h - 1][x].walkable = False
            tiles[boss_room.y][x].blocks_vision = tiles[boss_room.y + boss_room.h - 1][x].blocks_vision = True
        for y in range(boss_room.y, boss_room.y + boss_room.h):
            tiles[y][boss_room.x].tile_type = tiles[y][boss_room.x + boss_room.w - 1].tile_type = TileType.WALL
            tiles[y][boss_room.x].walkable = tiles[y][boss_room.x + boss_room.w - 1].walkable = False
            tiles[y][boss_room.x].blocks_vision = tiles[y][boss_room.x + boss_room.w - 1].blocks_vision = True

        tiles[approach[1]][approach[0]].tile_type, tiles[approach[1]][approach[0]].walkable = TileType.FLOOR, True
        tiles[approach[1]][approach[0]].blocks_vision = False
        tiles[gy][gx].tile_type, tiles[gy][gx].walkable, tiles[gy][gx].blocks_vision = TileType.BOSS_GATE, True, True
        return (gx, gy)

    def _embellish_boss_arena(self, tiles: List[List[TileCell]], boss_room: MapRoom) -> None:
        bcx, bcy = boss_room.center
        for ry in range(boss_room.y + 1, boss_room.y + boss_room.h - 1):
            for rx in range(boss_room.x + 1, boss_room.x + boss_room.w - 1):
                if rx == bcx and ry == bcy:
                    tiles[ry][rx].tile_type, tiles[ry][rx].walkable = TileType.BOSS_ALTAR, True
                elif max(abs(rx - bcx), abs(ry - bcy)) <= 1:
                    tiles[ry][rx].tile_type, tiles[ry][rx].walkable = TileType.RUNIC_FLOOR, True

    def _carve_dead_ends(
        self, tiles: List[List[TileCell]], rooms: List[MapRoom], rng: random.Random, boss_room: MapRoom
    ) -> List[Tuple[int, int]]:
        dead_ends: List[Tuple[int, int]] = []
        for r in rooms[:3]:
            ex, ey = r.center
            dx, dy = rng.choice([(1, 0), (-1, 0), (0, 1), (0, -1)])
            for _ in range(rng.randint(4, 7)):
                nx, ny = ex + dx, ey + dy
                if 2 <= nx < self.width - 2 and 2 <= ny < self.height - 2 and not boss_room.contains(nx, ny):
                    tiles[ny][nx].tile_type, tiles[ny][nx].walkable, tiles[ny][nx].blocks_vision = TileType.FLOOR, True, False
                    ex, ey = nx, ny
            dead_ends.append((ex, ey))
        return dead_ends

    def _inject_anti_bot_obstacles(
        self,
        tiles: List[List[TileCell]],
        rooms: List[MapRoom],
        boss_room: MapRoom,
        boss_gate_coord: Optional[Tuple[int, int]],
        rng: random.Random,
        biome: MapBiomeDefinition,
    ) -> Dict[Tuple[int, int], ObstacleInstance]:
        obstacles: Dict[Tuple[int, int], ObstacleInstance] = {}
        room_centers = {r.center for r in rooms}
        obs_id_counter = 0

        def add_obs(obs_t: ObstacleType, ox: int, oy: int, hp: int = 150, blk_v: bool = False) -> None:
            nonlocal obs_id_counter
            obs_id_counter += 1
            obstacles[(ox, oy)] = ObstacleInstance(
                f"obs_{obs_t.name.lower()}_{obs_id_counter}", ox, oy, obs_t, hp, hp, False, True, blk_v
            )

        walkables = [
            (x, y)
            for y in range(2, self.height - 2)
            for x in range(2, self.width - 2)
            if tiles[y][x].walkable and (x, y) not in room_centers and not boss_room.contains(x, y)
        ]
        rng.shuffle(walkables)

        barricade_placed = False
        for x, y in walkables:
            is_choke = (not tiles[y][x - 1].walkable and not tiles[y][x + 1].walkable) or (
                not tiles[y - 1][x].walkable and not tiles[y + 1][x].walkable
            )
            if is_choke or not barricade_placed:
                add_obs(ObstacleType.BARRICADE_WOOD_BONE, x, y, hp=80, blk_v=True)
                barricade_placed = True
                break

        limit = min(20, int(len(walkables) * biome.obstacle_density))
        for x, y in walkables[1:limit]:
            if (x, y) in obstacles:
                continue
            obs_type = rng.choice(biome.obstacle_palette)
            if obs_type == ObstacleType.MUD_QUAGMIRE:
                tiles[y][x].tile_type, tiles[y][x].movement_cost = TileType.MUD_POOL, 2.0
            elif obs_type == ObstacleType.CONCEALED_SPIKES:
                tiles[y][x].tile_type, tiles[y][x].hazard_damage = TileType.SPIKE_TRAP, 25
            else:
                add_obs(obs_type, x, y, hp=150)

        if boss_gate_coord:
            add_obs(ObstacleType.BOSS_SEAL_BARRIER, boss_gate_coord[0], boss_gate_coord[1], hp=200, blk_v=True)

        bcx, bcy = boss_room.center
        for dx, dy in [(-2, -2), (2, -2), (-2, 2), (2, 2)]:
            bx, by = bcx + dx, bcy + dy
            if boss_room.contains(bx, by) and (bx, by) not in obstacles:
                add_obs(ObstacleType.BONE_BRAZIER, bx, by, hp=120)

        return obstacles

    def _carve_line(
        self, tiles: List[List[TileCell]], x1: int, y1: int, x2: int, y2: int, width: int = 1
    ) -> None:
        half_w = width // 2
        dx, dy = abs(x2 - x1), abs(y2 - y1)
        sx, sy = (1 if x1 < x2 else -1), (1 if y1 < y2 else -1)
        err, curr_x, curr_y = dx - dy, x1, y1
        while True:
            for dy_off in range(-half_w, half_w + 1):
                for dx_off in range(-half_w, half_w + 1):
                    nx, ny = curr_x + dx_off, curr_y + dy_off
                    if 0 <= nx < self.width and 0 <= ny < self.height:
                        tiles[ny][nx].tile_type, tiles[ny][nx].walkable, tiles[ny][nx].blocks_vision = TileType.FLOOR, True, False
            if curr_x == x2 and curr_y == y2:
                break
            e2 = 2 * err
            if e2 > -dy:
                err -= dy
                curr_x += sx
            if e2 < dx:
                err += dx
                curr_y += sy

    def damage_obstacle(self, map_data: MapGridData, x: int, y: int, damage: int) -> Tuple[bool, int]:
        obs = map_data.obstacles.get((x, y))
        if not obs or obs.is_destroyed:
            return (False, 0)
        obs.hp = max(0, obs.hp - damage)
        if obs.hp == 0:
            obs.is_destroyed, obs.blocks_movement, obs.blocks_vision = True, False, False
            if (x, y) == map_data.boss_gate:
                map_data.boss_gate_breached = True
            else:
                map_data.tiles[y][x].tile_type = TileType.FLOOR
            map_data.tiles[y][x].walkable, map_data.tiles[y][x].blocks_vision = True, False
            return (True, 0)
        return (False, obs.hp)

    def breach_boss_gate(self, map_data: MapGridData) -> bool:
        if not map_data.boss_gate:
            return False
        gx, gy = map_data.boss_gate
        obs = map_data.obstacles.get((gx, gy))
        if obs:
            obs.is_destroyed, obs.blocks_movement, obs.blocks_vision = True, False, False
        map_data.tiles[gy][gx].walkable, map_data.tiles[gy][gx].blocks_vision = True, False
        map_data.boss_gate_breached = True
        return True

    _active_zone_maps: Dict[str, MapGridData] = {}

    @classmethod
    def set_zone_map(cls, zone_id: str, map_data: MapGridData) -> None:
        """Sets or registers the active map grid for a zone (used in runtime & test fixtures)."""
        cls._active_zone_maps[zone_id] = map_data

    @classmethod
    def clear_zone_maps(cls) -> None:
        """Clears cached map grids."""
        cls._active_zone_maps.clear()

    @classmethod
    def get_tile_type(cls, zone_id: str, tx: int, ty: int, seed: int = 0) -> TileType:
        """
        Returns the TileType at tile coordinates (tx, ty) for a given zone.
        Queries active cached map or generates procedural map on demand.
        Out-of-bounds coordinates return TileType.WALL.
        """
        if zone_id in cls._active_zone_maps:
            grid = cls._active_zone_maps[zone_id]
            if not grid.is_in_bounds(tx, ty):
                return TileType.WALL
            return grid.tiles[ty][tx].tile_type
        try:
            grid = cls.generate_wilderness_map(zone_id, seed=seed)
            cls._active_zone_maps[zone_id] = grid
            if not grid.is_in_bounds(tx, ty):
                return TileType.WALL
            return grid.tiles[ty][tx].tile_type
        except Exception:
            return TileType.FLOOR

    @classmethod
    def generate_wilderness_map(
        cls, zone_id: str, seed: int, biome_id: Optional[str] = None
    ) -> MapGridData:
        """Facade method delegating to WildernessMapGenerator for open-world zones."""
        from server.world.wilderness_map_generator import WildernessMapGenerator
        generator = WildernessMapGenerator.for_zone(zone_id, seed=seed, biome_id=biome_id)
        return generator.generate(seed=seed, zone_id=zone_id)


def generate_wilderness_map(
    zone_id: str, seed: int, biome_id: Optional[str] = None
) -> MapGridData:
    """Facade for generating open-world wilderness procedural map grids."""
    return ProceduralMapEngine.generate_wilderness_map(zone_id, seed, biome_id)


def verify_path_connectivity(
    map_data: MapGridData, start: Tuple[int, int], target: Tuple[int, int], allow_sealed_boss_gate: bool = True
) -> Tuple[bool, List[Tuple[int, int]]]:
    if not map_data.is_in_bounds(start[0], start[1]) or not map_data.is_in_bounds(target[0], target[1]):
        return (False, [])

    frontier = [(0.0, start)]
    came_from = {start: None}
    cost_so_far = {start: 0.0}

    while frontier:
        _, current = heapq.heappop(frontier)
        if current == target:
            path, curr = [], target
            while curr is not None:
                path.append(curr)
                curr = came_from[curr]
            path.reverse()
            return (True, path)

        cx, cy = current
        for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
            nx, ny = cx + dx, cy + dy
            if not map_data.is_in_bounds(nx, ny) or map_data.tiles[ny][nx].tile_type == TileType.WALL:
                continue

            obs = map_data.obstacles.get((nx, ny))
            if obs and not obs.is_destroyed and obs.blocks_movement:
                if obs.obstacle_type == ObstacleType.BOSS_SEAL_BARRIER and not allow_sealed_boss_gate:
                    continue
                if obs.obstacle_type in (
                    ObstacleType.BARRICADE_WOOD_BONE,
                    ObstacleType.BOSS_SEAL_BARRIER,
                    ObstacleType.DEBRIS_COLLAPSE,
                    ObstacleType.BONE_CLUSTER,
                ):
                    step_cost = 4.0
                else:
                    continue
            else:
                step_cost = map_data.tiles[ny][nx].movement_cost

            new_cost = cost_so_far[current] + step_cost
            if (nx, ny) not in cost_so_far or new_cost < cost_so_far[(nx, ny)]:
                cost_so_far[(nx, ny)] = new_cost
                heapq.heappush(frontier, (new_cost + math.hypot(target[0] - nx, target[1] - ny), (nx, ny)))
                came_from[(nx, ny)] = current

    return (False, [])


def calculate_path_sinuosity(path: List[Tuple[int, int]], start: Tuple[int, int], target: Tuple[int, int]) -> float:
    euclidean = math.hypot(target[0] - start[0], target[1] - start[1])
    return round(len(path) / euclidean, 2) if euclidean > 0 else 1.0
