"""Matrix client-server API client for sending messages."""

from __future__ import annotations

import asyncio
import logging
import re
import uuid
from typing import Any, Final
from urllib.parse import quote

import aiohttp

from ..http_base import _MAX_RETRY_AFTER_S, safe_headers
from ..redact import redact, redact_exc

_LOGGER = logging.getLogger(__name__)

# Matrix room IDs are ``!opaque:server.name`` per the spec. We also allow
# the ``#alias:server`` form because some callers may pass aliases. The
# pattern's purpose is to reject obvious path-injection (``/``, ``..``,
# control chars, query/fragment chars) before the value reaches a URL.
_ROOM_ID_RE: Final = re.compile(r"^[!#][^\x00-\x1f\s/?#]{1,255}:[A-Za-z0-9.\-:]{1,255}$")

_DEFAULT_TIMEOUT: Final = aiohttp.ClientTimeout(total=30, connect=10)
_MAX_RETRIES: Final = 3


def _validate_room_id(room_id: str) -> str:
    if not room_id:
        raise ValueError("room_id is empty")
    if not _ROOM_ID_RE.match(room_id):
        raise ValueError("room_id format is invalid")
    return room_id


class MatrixClient:
    """Sends messages to Matrix rooms via the client-server API."""

    def __init__(
        self,
        session: aiohttp.ClientSession,
        homeserver_url: str,
        access_token: str,
    ) -> None:
        self._session = session
        self._homeserver = homeserver_url.rstrip("/")
        self._token = access_token

    @staticmethod
    def _txn_id() -> str:
        # uuid4 hex is collision-resistant across processes/restarts;
        # eliminates the previous module-level counter race.
        return uuid.uuid4().hex

    async def send_message(
        self,
        room_id: str,
        message: str,
        html_message: str | None = None,
    ) -> dict[str, Any]:
        """Send a text message to a Matrix room."""
        try:
            room_id = _validate_room_id(room_id)
        except ValueError as exc:
            return {"success": False, "error": f"Invalid room_id: {exc}"}

        encoded_room = quote(room_id, safe="")
        url = (
            f"{self._homeserver}/_matrix/client/v3/rooms/{encoded_room}"
            f"/send/m.room.message/{self._txn_id()}"
        )

        body: dict[str, Any] = {"msgtype": "m.text", "body": message}
        if html_message:
            body["format"] = "org.matrix.custom.html"
            body["formatted_body"] = html_message

        headers = safe_headers({
            "Authorization": f"Bearer {self._token}",
            "Content-Type": "application/json",
        })

        for attempt in range(1, _MAX_RETRIES + 1):
            try:
                async with self._session.put(
                    url, json=body, headers=headers,
                    timeout=_DEFAULT_TIMEOUT, allow_redirects=False,
                ) as resp:
                    if 200 <= resp.status < 300:
                        return {"success": True}
                    resp_body = await resp.text()
                    if resp.status == 429 and attempt < _MAX_RETRIES:
                        try:
                            wait_s = float(resp.headers.get("Retry-After", "2"))
                        except (TypeError, ValueError):
                            wait_s = 2.0
                        wait_s = max(0.0, min(wait_s, _MAX_RETRY_AFTER_S))
                        _LOGGER.warning(
                            "Matrix rate limited, retrying after %.2fs (attempt %d/%d)",
                            wait_s, attempt, _MAX_RETRIES,
                        )
                        await asyncio.sleep(wait_s)
                        continue
                    return {
                        "success": False,
                        "error": f"HTTP {resp.status}: {redact(resp_body)[:200]}",
                        "status_code": resp.status,
                    }
            except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e:
                return {"success": False, "error": redact_exc(e)}

        return {"success": False, "error": "Rate limited (retries exhausted)"}
