"""
Room-to-Agent Mapping — Phase M2.0 (N rooms, 1 agent per room)

Parses BRIDGE_ROOM_MAP env var and provides:
  - room_id → agent_id lookup
  - agent_id allowlist validation (from BRIDGE_ALLOWED_AGENTS)
  - summary for /bridge/mappings endpoint

Format of BRIDGE_ROOM_MAP:
  "agent_id:!room_id:server,agent2:!room2:server"
  e.g. "sofiia:!QwHczWXgefDHBEVkTH:daarion.space"

Multiple mappings separated by comma.
"""

import logging
import re
from dataclasses import dataclass, field
from typing import Dict, FrozenSet, List, Optional

logger = logging.getLogger(__name__)

# Room ID format: !<localpart>:<server>
_ROOM_ID_RE = re.compile(r"^![A-Za-z0-9\-_.]+:[A-Za-z0-9\-_.]+$")


@dataclass(frozen=True)
class RoomMapping:
    """Single room → agent binding."""
    room_id: str          # e.g. "!abc:daarion.space"
    agent_id: str         # e.g. "sofiia"


@dataclass
class RoomMappingConfig:
    """
    Parsed mapping configuration.

    Attributes:
        mappings:        List of RoomMapping (room_id → agent_id)
        allowed_agents:  Frozenset of allowlisted agent ids
    """
    mappings: List[RoomMapping] = field(default_factory=list)
    allowed_agents: FrozenSet[str] = field(default_factory=frozenset)

    # Internal index for O(1) lookup
    _room_to_agent: Dict[str, str] = field(default_factory=dict, repr=False, compare=False)
    _agent_to_rooms: Dict[str, List[str]] = field(default_factory=dict, repr=False, compare=False)

    def __post_init__(self) -> None:
        self._rebuild_index()

    def _rebuild_index(self) -> None:
        self._room_to_agent = {m.room_id: m.agent_id for m in self.mappings}
        self._agent_to_rooms = {}
        for m in self.mappings:
            self._agent_to_rooms.setdefault(m.agent_id, []).append(m.room_id)

    def agent_for_room(self, room_id: str) -> Optional[str]:
        """
        Returns agent_id for a room_id, or None if room is not mapped.
        Also returns None if agent is not in allowed_agents.
        """
        agent = self._room_to_agent.get(room_id)
        if agent is None:
            return None
        if agent not in self.allowed_agents:
            logger.warning("Room %s mapped to agent %s which is NOT in allowed_agents", room_id, agent)
            return None
        return agent

    def rooms_for_agent(self, agent_id: str) -> List[str]:
        """Returns list of room_ids mapped to an agent_id."""
        return list(self._agent_to_rooms.get(agent_id, []))

    def as_summary(self) -> List[Dict]:
        """
        Returns a safe summary list for the /bridge/mappings endpoint.
        Room IDs are NOT secrets (they identify chat rooms), but tokens are never included.
        """
        return [
            {
                "room_id": m.room_id,
                "agent_id": m.agent_id,
                "allowed": m.agent_id in self.allowed_agents,
            }
            for m in self.mappings
        ]

    @property
    def total_mappings(self) -> int:
        return len(self.mappings)


def parse_room_map(raw: str, allowed_agents: FrozenSet[str]) -> RoomMappingConfig:
    """
    Parse BRIDGE_ROOM_MAP string into RoomMappingConfig.

    Format: "agent_id:!room_id:server[,agent2:!room2:server2,...]"

    Raises ValueError on malformed entries (but skips warn-only issues).
    """
    mappings: List[RoomMapping] = []
    errors: List[str] = []

    if not raw or not raw.strip():
        return RoomMappingConfig(mappings=[], allowed_agents=allowed_agents)

    for idx, entry in enumerate(raw.split(",")):
        entry = entry.strip()
        if not entry:
            continue

        # Find the colon that separates agent_id from room_id
        # Room IDs look like !localpart:server — the separator colon is after agent_id
        # Format: "sofiia:!QwHczWXgefDHBEVkTH:daarion.space"
        #         ^agent^:^------room_id---------^
        colon_idx = entry.find(":")
        if colon_idx < 1:
            errors.append(f"Entry[{idx}] missing agent:room separator: {entry!r}")
            continue

        agent_id = entry[:colon_idx].strip()
        room_id = entry[colon_idx + 1:].strip()

        if not agent_id:
            errors.append(f"Entry[{idx}] empty agent_id in: {entry!r}")
            continue

        if not room_id:
            errors.append(f"Entry[{idx}] empty room_id in: {entry!r}")
            continue

        if not _ROOM_ID_RE.match(room_id):
            errors.append(
                f"Entry[{idx}] invalid room_id format (expected !localpart:server): {room_id!r}"
            )
            continue

        if agent_id not in allowed_agents:
            logger.warning(
                "Entry[%d] agent %r not in allowed_agents %s — mapping accepted but will be rejected at runtime",
                idx, agent_id, set(allowed_agents),
            )

        mappings.append(RoomMapping(room_id=room_id, agent_id=agent_id))

    if errors:
        raise ValueError(f"BRIDGE_ROOM_MAP parse errors: {'; '.join(errors)}")

    # M2.0: fail fast on duplicate room_id (1 room must map to exactly 1 agent)
    seen_rooms: Dict[str, str] = {}
    for m in mappings:
        if m.room_id in seen_rooms:
            raise ValueError(
                f"Duplicate room_id {m.room_id!r}: already bound to agent "
                f"{seen_rooms[m.room_id]!r}, cannot rebind to {m.agent_id!r}"
            )
        seen_rooms[m.room_id] = m.agent_id

    config = RoomMappingConfig(mappings=mappings, allowed_agents=allowed_agents)
    logger.info(
        "Room mapping loaded: %d entries, allowed_agents=%s",
        len(mappings), set(allowed_agents),
    )
    return config
