"""Matrix room administration helpers."""

from __future__ import annotations

import json
from collections.abc import Iterable, MutableMapping
from enum import StrEnum
from typing import TYPE_CHECKING, Any

import nio

from mindroom.logging_config import get_logger
from mindroom.thread_tags import THREAD_TAGS_EVENT_TYPE

if TYPE_CHECKING:
    from mindroom.config.matrix import RoomDirectoryVisibility, RoomJoinRule

logger = get_logger(__name__)

_POWER_LEVELS_EVENT_TYPE = "m.room.power_levels"
_ROOM_ENCRYPTION_EVENT_TYPE = "m.room.encryption"
_ROOM_ENCRYPTION_CONTENT = {"algorithm": "m.megolm.v1.aes-sha2"}
_ROOM_ADMIN_POWER_LEVEL = 100
_DEFAULT_STATE_EVENT_POWER_LEVEL = 50
_DEFAULT_USER_POWER_LEVEL = 0
_POWER_USER_POWER_LEVEL = 50

# Element Call membership state event (deployed MSC3401 flavor). Regular room
# members must be able to publish it to join a call, so it is pinned to PL0 —
# the same convention Element uses for call-capable rooms. Mirrors
# ``mindroom.matrix_rtc.events.CALL_MEMBER_EVENT_TYPE`` (kept as a literal here
# to avoid a core -> matrix_rtc dependency).
_CALL_MEMBER_EVENT_TYPE = "org.matrix.msc3401.call.member"
_MANAGED_ROOM_EVENT_POWER_LEVELS = {
    THREAD_TAGS_EVENT_TYPE: 0,
    _CALL_MEMBER_EVENT_TYPE: 0,
}
_TERMINAL_ROOM_JOIN_ERROR_CODES = frozenset(
    {
        "M_BAD_STATE",
        "M_GUEST_ACCESS_FORBIDDEN",
        "M_UNRECOGNIZED",
    },
)


class RoomJoinOutcome(StrEnum):
    """Typed outcome for one Matrix room join attempt."""

    JOINED = "joined"
    RETRYABLE_FAILURE = "retryable_failure"
    TERMINAL_FAILURE = "terminal_failure"


async def invite_to_room(
    client: nio.AsyncClient,
    room_id: str,
    user_id: str,
) -> bool:
    """Invite a user to a room."""
    response = await client.room_invite(room_id, user_id)
    if isinstance(response, nio.RoomInviteResponse):
        logger.info("matrix_room_invited", room_id=room_id, user_id=user_id)
        return True
    logger.error("matrix_room_invite_failed", room_id=room_id, user_id=user_id, error=str(response))
    return False


def _create_room_initial_state(
    client: nio.AsyncClient,
    power_users: list[str] | None,
    admin_users: list[str] | None,
    *,
    encrypted: bool,
) -> list[dict[str, Any]]:
    """Build the initial state events for one managed room creation."""
    power_level_content: dict[str, Any] = {
        "users_default": _DEFAULT_USER_POWER_LEVEL,
        "state_default": _DEFAULT_STATE_EVENT_POWER_LEVEL,
        "events": dict(_MANAGED_ROOM_EVENT_POWER_LEVELS),
    }
    users: dict[str, int] = {}
    if power_users:
        users.update(dict.fromkeys(power_users, _POWER_USER_POWER_LEVEL))
    if admin_users:
        users.update(dict.fromkeys(admin_users, _ROOM_ADMIN_POWER_LEVEL))
    if client.user_id:
        users[client.user_id] = _ROOM_ADMIN_POWER_LEVEL
    if users:
        power_level_content["users"] = users
    initial_state: list[dict[str, Any]] = [{"type": _POWER_LEVELS_EVENT_TYPE, "content": power_level_content}]
    if encrypted:
        initial_state.append(
            {"type": _ROOM_ENCRYPTION_EVENT_TYPE, "state_key": "", "content": dict(_ROOM_ENCRYPTION_CONTENT)},
        )
    return initial_state


async def create_room(
    client: nio.AsyncClient,
    name: str,
    alias: str | None = None,
    topic: str | None = None,
    power_users: list[str] | None = None,
    admin_users: list[str] | None = None,
    *,
    encrypted: bool = False,
) -> str | None:
    """Create a new Matrix room."""
    room_config: dict[str, Any] = {"name": name}
    if alias:
        room_config["alias"] = alias
    if topic:
        room_config["topic"] = topic
    room_config["initial_state"] = _create_room_initial_state(client, power_users, admin_users, encrypted=encrypted)

    response = await client.room_create(**room_config)
    if isinstance(response, nio.RoomCreateResponse):
        logger.info("matrix_room_created", room_id=str(response.room_id), name=name)
        room_id = str(response.room_id)
        if power_users:
            for user_id in power_users:
                if user_id != client.user_id:
                    await invite_to_room(client, room_id, user_id)
        return room_id
    logger.error("matrix_room_creation_failed", name=name, error=str(response))
    return None


async def room_encryption_enabled(client: nio.AsyncClient, room_id: str) -> bool | None:
    """Return whether a room has encryption enabled, or None when the state is unreadable."""
    response = await client.room_get_state_event(room_id, _ROOM_ENCRYPTION_EVENT_TYPE)
    if isinstance(response, nio.RoomGetStateEventResponse):
        return True
    if isinstance(response, nio.RoomGetStateEventError) and response.status_code == "M_NOT_FOUND":
        return False
    logger.warning("matrix_room_encryption_state_unreadable", room_id=room_id, error=str(response))
    return None


async def ensure_room_encryption_enabled(client: nio.AsyncClient, room_id: str) -> bool:
    """Enable Matrix encryption on a room if it is not already enabled.

    Enabling encryption is irreversible; this helper never disables it.
    Returns whether the room is encrypted after the call.
    """
    enabled = await room_encryption_enabled(client, room_id)
    if enabled:
        return True
    if enabled is None:
        return False
    response = await client.room_put_state(room_id, _ROOM_ENCRYPTION_EVENT_TYPE, dict(_ROOM_ENCRYPTION_CONTENT))
    if isinstance(response, nio.RoomPutStateResponse):
        cached_room = client.rooms.get(room_id)
        if cached_room is not None:
            # nio only encrypts sends once the cached room flips encrypted, which
            # normally happens on the next sync; flip it now so replies sent inside
            # the sync window (e.g. the `!encrypt` confirmation) are not plaintext.
            cached_room.encrypted = True
        logger.info("matrix_room_encryption_enabled", room_id=room_id)
        return True
    logger.error("matrix_room_encryption_enable_failed", room_id=room_id, error=str(response))
    return False


def _with_event_power_level(
    power_levels_content: dict[str, Any],
    event_type: str,
    power_level: int,
) -> dict[str, Any]:
    """Return power-level content with one event-type override applied."""
    next_content = dict(power_levels_content)
    existing_events = power_levels_content.get("events")
    next_events = dict(existing_events) if isinstance(existing_events, dict) else {}
    next_events[event_type] = power_level
    next_content["events"] = next_events
    return next_content


async def ensure_managed_room_power_levels(
    client: nio.AsyncClient,
    room_id: str,
    admin_user_ids: Iterable[str] = (),
) -> bool:
    """Reconcile managed-room power levels with one read-modify-write.

    Applies the PL0 state events used by MindRoom clients and grants configured
    room admins power level 100 in one conditional PUT.
    """
    current_response = await client.room_get_state_event(room_id, _POWER_LEVELS_EVENT_TYPE)
    if not isinstance(current_response, nio.RoomGetStateEventResponse):
        logger.error(
            "Failed to read room power levels for managed room reconciliation",
            room_id=room_id,
            error=_describe_matrix_response_error(current_response),
        )
        return False
    if not isinstance(current_response.content, dict):
        logger.error(
            "Room power levels state has unexpected content shape",
            room_id=room_id,
            content=current_response.content,
        )
        return False
    current_content = current_response.content

    desired_content = current_content
    for event_type, power_level in _MANAGED_ROOM_EVENT_POWER_LEVELS.items():
        desired_content = _with_event_power_level(desired_content, event_type, power_level)
    concrete_admin_ids = {user_id for user_id in admin_user_ids if user_id}
    if concrete_admin_ids:
        desired_content = _with_room_admin_power_levels(desired_content, concrete_admin_ids)
    if desired_content == current_content:
        logger.debug(
            "Managed room power levels already configured",
            room_id=room_id,
            admin_user_ids=sorted(concrete_admin_ids),
        )
        return True

    response = await client.room_put_state(
        room_id=room_id,
        event_type=_POWER_LEVELS_EVENT_TYPE,
        content=desired_content,
    )
    if isinstance(response, nio.RoomPutStateResponse):
        logger.info(
            "Updated managed room power levels",
            room_id=room_id,
            admin_user_ids=sorted(concrete_admin_ids),
        )
        return True

    logger.error(
        "Failed to update managed room power levels",
        room_id=room_id,
        admin_user_ids=sorted(concrete_admin_ids),
        error=_describe_matrix_response_error(response),
        hint="Ensure the service account is joined and can update m.room.power_levels.",
    )
    return False


def _with_room_admin_power_levels(
    power_levels_content: dict[str, Any],
    user_ids: Iterable[str],
) -> dict[str, Any]:
    """Return power-level content with users promoted while preserving existing admins."""
    next_content = dict(power_levels_content)
    existing_users = power_levels_content.get("users")
    next_users = dict(existing_users) if isinstance(existing_users, dict) else {}
    for user_id in sorted(set(user_ids)):
        current_level = next_users.get(user_id)
        if not isinstance(current_level, int) or current_level < _ROOM_ADMIN_POWER_LEVEL:
            next_users[user_id] = _ROOM_ADMIN_POWER_LEVEL
    next_content["users"] = next_users
    return next_content


def _room_power_level_for_user(power_levels_content: dict[str, Any], user_id: str) -> int:
    """Return one user's current room power level from power-level state content."""
    users = power_levels_content.get("users")
    if isinstance(users, dict):
        user_level = users.get(user_id)
        if isinstance(user_level, int):
            return user_level
    users_default = power_levels_content.get("users_default")
    return users_default if isinstance(users_default, int) else _DEFAULT_USER_POWER_LEVEL


async def room_admin_power_user(
    client: nio.AsyncClient,
    room_id: str,
    user_ids: Iterable[str],
) -> str | None:
    """Return the first supplied user ID with Matrix room admin power."""
    concrete_user_ids = list(dict.fromkeys(user_id for user_id in user_ids if user_id))
    if not concrete_user_ids:
        return None

    try:
        current_response = await client.room_get_state_event(room_id, _POWER_LEVELS_EVENT_TYPE)
    except Exception as exc:  # fail closed for chat-admin checks
        logger.warning("Failed to read room power levels for admin check", room_id=room_id, error=str(exc))
        return None

    if not isinstance(current_response, nio.RoomGetStateEventResponse):
        logger.warning(
            "Room power levels unavailable for admin check",
            room_id=room_id,
            user_ids=sorted(concrete_user_ids),
            error=_describe_matrix_response_error(current_response),
        )
        return None
    if not isinstance(current_response.content, dict):
        logger.warning(
            "Room power levels state has unexpected content shape for admin check",
            room_id=room_id,
            user_ids=sorted(concrete_user_ids),
            content=current_response.content,
        )
        return None

    for user_id in concrete_user_ids:
        if _room_power_level_for_user(current_response.content, user_id) >= _ROOM_ADMIN_POWER_LEVEL:
            return user_id
    return None


async def ensure_room_admin_power_levels(
    client: nio.AsyncClient,
    room_id: str,
    user_ids: Iterable[str],
) -> bool:
    """Grant Matrix room admin power to users without revoking existing admins."""
    concrete_user_ids = {user_id for user_id in user_ids if user_id}
    if not concrete_user_ids:
        return True

    current_response = await client.room_get_state_event(room_id, _POWER_LEVELS_EVENT_TYPE)
    if not isinstance(current_response, nio.RoomGetStateEventResponse):
        logger.error(
            "Failed to read room power levels for admin reconciliation",
            room_id=room_id,
            user_ids=sorted(concrete_user_ids),
            error=_describe_matrix_response_error(current_response),
        )
        return False
    if not isinstance(current_response.content, dict):
        logger.error(
            "Room power levels state has unexpected content shape",
            room_id=room_id,
            user_ids=sorted(concrete_user_ids),
            content=current_response.content,
        )
        return False

    current_content = current_response.content
    desired_content = _with_room_admin_power_levels(current_content, concrete_user_ids)
    if desired_content == current_content:
        logger.debug(
            "Room admins already have sufficient power",
            room_id=room_id,
            user_ids=sorted(concrete_user_ids),
            power_level=_ROOM_ADMIN_POWER_LEVEL,
        )
        return True

    response = await client.room_put_state(
        room_id=room_id,
        event_type=_POWER_LEVELS_EVENT_TYPE,
        content=desired_content,
    )
    if isinstance(response, nio.RoomPutStateResponse):
        logger.info(
            "Updated room power levels for admins",
            room_id=room_id,
            user_ids=sorted(concrete_user_ids),
            power_level=_ROOM_ADMIN_POWER_LEVEL,
        )
        return True

    logger.error(
        "Failed to update room power levels for admins",
        room_id=room_id,
        user_ids=sorted(concrete_user_ids),
        error=_describe_matrix_response_error(response),
        hint="Ensure the service account is joined and can update m.room.power_levels.",
    )
    return False


async def create_space(
    client: nio.AsyncClient,
    name: str,
    alias: str | None = None,
    topic: str | None = None,
) -> str | None:
    """Create a private Matrix Space."""
    room_config: dict[str, Any] = {
        "name": name,
        "space": True,
        "preset": nio.RoomPreset.private_chat,
    }
    if alias:
        room_config["alias"] = alias
    if topic:
        room_config["topic"] = topic

    response = await client.room_create(**room_config)
    if isinstance(response, nio.RoomCreateResponse):
        logger.info("matrix_space_created", room_id=str(response.room_id), name=name)
        return str(response.room_id)

    logger.error("matrix_space_creation_failed", name=name, error=str(response))
    return None


def _describe_matrix_response_error(response: object) -> str:
    """Convert a Matrix response object into a concise error string."""
    if isinstance(response, nio.ErrorResponse):
        if response.status_code and response.message:
            return f"{response.status_code}: {response.message}"
        if response.status_code:
            return str(response.status_code)
        if response.message:
            return str(response.message)
    return str(response)


async def _get_room_join_rule(client: nio.AsyncClient, room_id: str) -> str | None:
    """Read the current join rule from room state."""
    response = await client.room_get_state_event(room_id, "m.room.join_rules")
    if isinstance(response, nio.RoomGetStateEventResponse):
        join_rule = response.content.get("join_rule")
        if isinstance(join_rule, str):
            return join_rule
        logger.warning(
            "Room join rule state missing expected 'join_rule' field",
            room_id=room_id,
            content=response.content,
        )
        return None

    logger.warning(
        "Failed to read room join rule",
        room_id=room_id,
        error=_describe_matrix_response_error(response),
    )
    return None


async def _set_room_join_rule(
    client: nio.AsyncClient,
    room_id: str,
    join_rule: RoomJoinRule,
) -> bool:
    """Write the room join rule state event."""
    response = await client.room_put_state(
        room_id=room_id,
        event_type="m.room.join_rules",
        content={"join_rule": join_rule},
    )
    if isinstance(response, nio.RoomPutStateResponse):
        logger.info("Updated room join rule", room_id=room_id, join_rule=join_rule)
        return True

    logger.warning(
        "Failed to set room join rule",
        room_id=room_id,
        join_rule=join_rule,
        error=_describe_matrix_response_error(response),
        hint=(
            "Ensure the service account is joined to the room and has enough power "
            "to send m.room.join_rules state events."
        ),
    )
    return False


async def ensure_room_join_rule(
    client: nio.AsyncClient,
    room_id: str,
    target_join_rule: RoomJoinRule,
) -> bool:
    """Ensure a room has the desired join rule."""
    current_join_rule = await _get_room_join_rule(client, room_id)
    if current_join_rule == target_join_rule:
        logger.debug("Room join rule already configured", room_id=room_id, join_rule=target_join_rule)
        return True
    return await _set_room_join_rule(client, room_id, target_join_rule)


async def _get_room_directory_visibility(client: nio.AsyncClient, room_id: str) -> str | None:
    """Read the current room directory visibility."""
    response = await client.room_get_visibility(room_id)
    if isinstance(response, nio.RoomGetVisibilityResponse):
        return str(response.visibility)

    logger.warning(
        "Failed to read room directory visibility",
        room_id=room_id,
        error=_describe_matrix_response_error(response),
    )
    return None


async def _set_room_directory_visibility(
    client: nio.AsyncClient,
    room_id: str,
    visibility: RoomDirectoryVisibility,
) -> bool:
    """Set room visibility in the server room directory."""
    if not client.access_token:
        logger.warning(
            "Cannot set room directory visibility without access token",
            room_id=room_id,
            visibility=visibility,
        )
        return False

    _method, path = nio.Api.room_get_visibility(room_id)
    payload = json.dumps({"visibility": visibility})
    response = await client.send(
        "PUT",
        path,
        data=payload,
        headers={
            "Authorization": f"Bearer {client.access_token}",
            "Content-Type": "application/json",
        },
    )
    if 200 <= response.status < 300:
        response.release()
        logger.info("Updated room directory visibility", room_id=room_id, visibility=visibility)
        return True

    error_text = await response.text()
    response.release()
    hint = (
        "Ensure the service account is a room moderator/admin; Synapse requires sufficient "
        "power in the room to edit directory entries."
        if response.status == 403
        else "Check homeserver logs and Matrix API response for details."
    )
    logger.warning(
        "Failed to set room directory visibility",
        room_id=room_id,
        visibility=visibility,
        http_status=response.status,
        error=error_text,
        hint=hint,
    )
    return False


async def ensure_room_directory_visibility(
    client: nio.AsyncClient,
    room_id: str,
    target_visibility: RoomDirectoryVisibility,
) -> bool:
    """Ensure a room has the desired directory visibility."""
    current_visibility = await _get_room_directory_visibility(client, room_id)
    if current_visibility == target_visibility:
        logger.debug("Room directory visibility already configured", room_id=room_id, visibility=target_visibility)
        return True
    return await _set_room_directory_visibility(client, room_id, target_visibility)


async def ensure_room_name(
    client: nio.AsyncClient,
    room_id: str,
    name: str,
) -> bool:
    """Ensure a room or Space has the desired display name."""
    current_response = await client.room_get_state_event(room_id, "m.room.name")
    if isinstance(current_response, nio.RoomGetStateEventResponse) and current_response.content.get("name") == name:
        logger.debug("Room name already configured", room_id=room_id, name=name)
        return True

    response = await client.room_put_state(
        room_id=room_id,
        event_type="m.room.name",
        content={"name": name},
    )
    if isinstance(response, nio.RoomPutStateResponse):
        logger.info("Updated room name", room_id=room_id, name=name)
        return True

    logger.error(
        "Failed to update room name",
        room_id=room_id,
        name=name,
        error=_describe_matrix_response_error(response),
    )
    return False


async def add_room_to_space(
    client: nio.AsyncClient,
    space_id: str,
    room_id: str,
    via_server_name: str,
    *,
    suggested: bool = True,
) -> bool:
    """Ensure a room is linked as a child of a root Space."""
    desired_content = {
        "via": [via_server_name],
        "suggested": suggested,
    }

    current_response = await client.room_get_state_event(space_id, "m.space.child", room_id)
    if isinstance(current_response, nio.RoomGetStateEventResponse) and current_response.content == desired_content:
        logger.debug("Room already linked under root space", space_id=space_id, room_id=room_id)
        return True

    response = await client.room_put_state(
        room_id=space_id,
        event_type="m.space.child",
        content=desired_content,
        state_key=room_id,
    )
    if isinstance(response, nio.RoomPutStateResponse):
        logger.info("Linked room under root space", space_id=space_id, room_id=room_id)
        return True

    logger.error(
        "Failed to link room under root space",
        space_id=space_id,
        room_id=room_id,
        error=_describe_matrix_response_error(response),
    )
    return False


async def join_room(client: nio.AsyncClient, room_id: str) -> RoomJoinOutcome:
    """Join a Matrix room and classify explicit terminal server rejections."""
    response = await client.join(room_id)
    if isinstance(response, nio.JoinResponse):
        rooms = client.rooms
        if isinstance(rooms, MutableMapping) and response.room_id not in rooms:
            rooms[response.room_id] = nio.MatrixRoom(
                room_id=response.room_id,
                own_user_id=client.user_id or "",
            )
        logger.info("matrix_room_joined", room_id=room_id)
        return RoomJoinOutcome.JOINED
    outcome = (
        RoomJoinOutcome.TERMINAL_FAILURE
        if isinstance(response, nio.JoinError) and response.status_code in _TERMINAL_ROOM_JOIN_ERROR_CODES
        else RoomJoinOutcome.RETRYABLE_FAILURE
    )
    logger.warning(
        "matrix_room_join_failed",
        room_id=room_id,
        error=str(response),
        outcome=outcome.value,
    )
    return outcome


async def get_room_members(client: nio.AsyncClient, room_id: str) -> set[str] | None:
    """Get the current members of a room, or ``None`` when the fetch fails."""
    response = await client.joined_members(room_id)
    if isinstance(response, nio.JoinedMembersResponse):
        return {member.user_id for member in response.members}
    logger.warning("matrix_room_members_fetch_failed", room_id=room_id, error=str(response))
    return None


async def get_joined_rooms(client: nio.AsyncClient) -> list[str] | None:
    """Get all rooms the client has joined."""
    response = await client.joined_rooms()
    if isinstance(response, nio.JoinedRoomsResponse):
        return list(response.rooms)
    logger.error("matrix_joined_rooms_fetch_failed", error=str(response))
    return None


async def get_room_name(client: nio.AsyncClient, room_id: str) -> str:
    """Get the display name of a Matrix room."""
    response = await client.room_get_state_event(room_id, "m.room.name")
    if isinstance(response, nio.RoomGetStateEventResponse) and response.content.get("name"):
        return str(response.content["name"])

    response = await client.room_get_state(room_id)
    if not isinstance(response, nio.RoomGetStateResponse):
        return "Unnamed Room"

    for event in response.events:
        if event.get("type") == "m.room.name" and event.get("content", {}).get("name"):
            return str(event["content"]["name"])

    members = [
        event.get("content", {}).get("displayname", event.get("state_key", ""))
        for event in response.events
        if event.get("type") == "m.room.member"
        and event.get("content", {}).get("membership") == "join"
        and event.get("state_key") != client.user_id
    ]

    if len(members) == 1:
        return f"DM with {members[0]}"
    if members:
        return f"Room with {', '.join(members[:3])}" + (" and others" if len(members) > 3 else "")
    return "Unnamed Room"


async def leave_room(client: nio.AsyncClient, room_id: str) -> bool:
    """Leave a Matrix room."""
    response = await client.room_leave(room_id)
    if isinstance(response, nio.RoomLeaveResponse):
        logger.info("matrix_room_left", room_id=room_id)
        return True
    logger.error("matrix_room_leave_failed", room_id=room_id, error=str(response))
    return False


__all__ = [
    "RoomJoinOutcome",
    "add_room_to_space",
    "create_room",
    "create_space",
    "ensure_managed_room_power_levels",
    "ensure_room_admin_power_levels",
    "ensure_room_directory_visibility",
    "ensure_room_encryption_enabled",
    "ensure_room_join_rule",
    "ensure_room_name",
    "get_joined_rooms",
    "get_room_members",
    "get_room_name",
    "invite_to_room",
    "join_room",
    "leave_room",
    "room_admin_power_user",
    "room_encryption_enabled",
]
