"""POI Radar Overlay & Shared Memory IPC Consumer for Poe2VisualTool v1.2.0.

Reads real-time POI coordinates (Bosses, Strongboxes, Shrines) from the C++
engine's shared memory segment (`Poe2VisualTool_Poi_v1`) with seqlock
synchronization and triggers proximity audio alarms.
"""

from __future__ import annotations

import ctypes
import logging
import mmap
import struct
from dataclasses import dataclass
from typing import Callable, List, Optional

logger = logging.getLogger("Poe2VisualTool.Radar")

POI_IPC_NAME = "Poe2VisualTool_Poi_v1"
POI_MAGIC = 0x504F4931  # 'POI1'
MAX_POI_ENTRIES = 64

# Binary layout matches protocol.hpp:
# Header: magic(4B), seqlock(4B), count(4B), reserved(4B), player_x(4B), player_y(4B), player_z(4B), camera_yaw(4B) = 32B
HEADER_FORMAT = "<IIII4f"
HEADER_SIZE = 32

# Entity: id(8B), type(4B), world_x,y,z(12B), screen_x,y(8B), on_screen(1B), pad(3B), dist(4B), hp(4B), name(32B) = 76B
ENTITY_FORMAT = "<QI5fB3sff32s"
ENTITY_SIZE = 76

TOTAL_STATE_SIZE = HEADER_SIZE + MAX_POI_ENTRIES * ENTITY_SIZE


@dataclass
class RadarEntity:
    id: int
    poi_type: int
    world_x: float
    world_y: float
    world_z: float
    screen_x: float
    screen_y: float
    on_screen: bool
    distance: float
    health_pct: float
    name: str

    @property
    def is_boss(self) -> bool:
        return self.poi_type == 1

    @property
    def is_rare(self) -> bool:
        return self.poi_type == 2

    @property
    def is_strongbox(self) -> bool:
        return self.poi_type == 3

    @property
    def is_shrine(self) -> bool:
        return self.poi_type == 4


@dataclass
class RadarSnapshot:
    count: int
    player_x: float
    player_y: float
    player_z: float
    camera_yaw: float
    entities: List[RadarEntity]


class PoiRadarReader:
    """Consumes Poe2VisualTool_Poi_v1 shared memory segment."""

    def __init__(self, ipc_name: str = POI_IPC_NAME) -> None:
        self.ipc_name = ipc_name
        self._mmap: Optional[mmap.mmap] = None
        self._header_struct = struct.Struct(HEADER_FORMAT)
        self._entity_struct = struct.Struct(ENTITY_FORMAT)
        self._seen_high_value_ids: set[int] = set()

    def open(self) -> bool:
        """Opens the named memory mapping."""
        if self._mmap is not None:
            return True
        try:
            self._mmap = mmap.mmap(
                -1,
                TOTAL_STATE_SIZE,
                tagname=self.ipc_name,
                access=mmap.ACCESS_READ,
            )
            return True
        except Exception as err:
            logger.debug("Failed to open POI shared memory (%s): %s", self.ipc_name, err)
            self._mmap = None
            return False

    def close(self) -> None:
        if self._mmap is not None:
            try:
                self._mmap.close()
            except Exception:
                pass
            self._mmap = None

    def read_snapshot(self, max_retries: int = 4) -> Optional[RadarSnapshot]:
        """Reads a consistent radar snapshot using seqlock synchronization."""
        if self._mmap is None:
            if not self.open():
                return None

        assert self._mmap is not None
        for _ in range(max_retries):
            try:
                self._mmap.seek(0)
                raw_header = self._mmap.read(HEADER_SIZE)
                if len(raw_header) < HEADER_SIZE:
                    continue

                magic, seq1, count, _, px, py, pz, yaw = self._header_struct.unpack(raw_header)
                if magic != POI_MAGIC:
                    return None

                if seq1 % 2 != 0:
                    # Write in progress, spin and retry
                    continue

                clamped_count = min(count, MAX_POI_ENTRIES)
                entities: List[RadarEntity] = []

                for _ in range(clamped_count):
                    raw_ent = self._mmap.read(ENTITY_SIZE)
                    if len(raw_ent) < ENTITY_SIZE:
                        break
                    (
                        eid,
                        etype,
                        wx,
                        wy,
                        wz,
                        sx,
                        sy,
                        on_scr,
                        _,
                        dist,
                        hp,
                        raw_name,
                    ) = self._entity_struct.unpack(raw_ent)
                    name_str = raw_name.split(b"\x00", 1)[0].decode("utf-8", errors="replace")
                    entities.append(
                        RadarEntity(
                            id=eid,
                            poi_type=etype,
                            world_x=wx,
                            world_y=wy,
                            world_z=wz,
                            screen_x=sx,
                            screen_y=sy,
                            on_screen=bool(on_scr),
                            distance=dist,
                            health_pct=hp,
                            name=name_str,
                        )
                    )

                # Check seqlock consistency at end
                self._mmap.seek(4)
                raw_seq2 = self._mmap.read(4)
                (seq2,) = struct.unpack("<I", raw_seq2)

                if seq1 == seq2:
                    return RadarSnapshot(
                        count=len(entities),
                        player_x=px,
                        player_y=py,
                        player_z=pz,
                        camera_yaw=yaw,
                        entities=entities,
                    )
            except Exception as err:
                logger.debug("Error reading POI snapshot: %s", err)

        return None

    def check_high_value_threats(
        self,
        snapshot: RadarSnapshot,
        distance_threshold: float = 180.0,
        on_new_target: Optional[Callable[[RadarEntity], None]] = None,
    ) -> List[RadarEntity]:
        """Identifies new Bosses or Strongboxes within proximity and triggers alert."""
        new_targets: List[RadarEntity] = []
        current_frame_ids = set()

        for ent in snapshot.entities:
            current_frame_ids.add(ent.id)
            if (ent.is_boss or ent.is_strongbox) and ent.distance <= distance_threshold:
                if ent.id not in self._seen_high_value_ids:
                    self._seen_high_value_ids.add(ent.id)
                    new_targets.append(ent)
                    if on_new_target:
                        on_new_target(ent)

        # Clean up stale IDs that left radar range
        self._seen_high_value_ids.intersection_update(current_frame_ids)
        return new_targets
