"""Matrix channel implementation using the Client-Server API.

Connects to a Matrix homeserver, listens for messages via long-poll sync,
and sends responses back to rooms. Uses httpx (already a dependency) — no
extra Matrix SDK needed.

Configuration (in config.toml or env):
    matrix_homeserver = "https://matrix.org"
    matrix_user_id = "@bot:matrix.org"
    matrix_access_token = "syt_..."
"""

from __future__ import annotations

import asyncio
import logging
from collections.abc import Callable
from typing import Any

import httpx

from agntspace.channels.base import Channel, ChannelMessage

log = logging.getLogger(__name__)

# Matrix Client-Server API paths
_SYNC = "/_matrix/client/v3/sync"
_SEND = "/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txn_id}"
_JOINED = "/_matrix/client/v3/joined_rooms"


class MatrixChannel(Channel):
    """Matrix messaging channel via Client-Server API."""

    name = "matrix"

    def __init__(self, homeserver: str, user_id: str, access_token: str) -> None:
        self._homeserver = homeserver.rstrip("/")
        self._user_id = user_id
        self._token = access_token
        self._connected = False
        self._message_callback: Callable | None = None
        self._chat_id: str | None = None  # last active room_id
        self._sync_token: str | None = None
        self._sync_task: asyncio.Task | None = None
        self._txn_counter = 0
        self._client: httpx.AsyncClient | None = None

    def _headers(self) -> dict[str, str]:
        return {"Authorization": f"Bearer {self._token}"}

    def _url(self, path: str) -> str:
        return f"{self._homeserver}{path}"

    # ── lifecycle ────────────────────────────────────────────────────

    async def connect(self) -> None:
        """Verify credentials and start the sync loop."""
        self._client = httpx.AsyncClient(timeout=60.0)

        # Verify token by checking joined rooms
        resp = await self._client.get(self._url(_JOINED), headers=self._headers())
        if resp.status_code != 200:
            raise ConnectionError(f"Matrix auth failed ({resp.status_code}): {resp.text[:200]}")

        rooms = resp.json().get("joined_rooms", [])
        log.info("Matrix connected: %s (%d rooms)", self._user_id, len(rooms))
        self._connected = True

        # Start background sync loop
        self._sync_task = asyncio.create_task(self._sync_loop())

    async def disconnect(self) -> None:
        """Stop sync loop and close client."""
        self._connected = False
        if self._sync_task and not self._sync_task.done():
            self._sync_task.cancel()
            try:
                await self._sync_task
            except asyncio.CancelledError:
                pass
        if self._client:
            await self._client.aclose()
            self._client = None
        log.info("Matrix disconnected")

    @property
    def is_connected(self) -> bool:
        return self._connected

    # ── sending ──────────────────────────────────────────────────────

    async def send_message(self, chat_id: str, text: str) -> None:
        """Send a text message to a Matrix room."""
        if not self._client:
            return
        self._txn_counter += 1
        path = _SEND.format(room_id=chat_id, txn_id=f"agnt{self._txn_counter}")
        body = {
            "msgtype": "m.text",
            "body": text,
        }
        resp = await self._client.put(self._url(path), headers=self._headers(), json=body)
        if resp.status_code not in (200, 202):
            log.warning("Matrix send failed (%d): %s", resp.status_code, resp.text[:200])

    async def send_notification(self, chat_id: str, text: str) -> None:
        await self.send_message(chat_id, f"🔔 {text}")

    # ── sync loop (long-poll for incoming messages) ──────────────────

    async def _sync_loop(self) -> None:
        """Long-poll the /sync endpoint for new messages."""
        # Initial sync to get the since token (don't process old messages)
        try:
            params: dict[str, Any] = {"timeout": "0", "filter": '{"room":{"timeline":{"limit":0}}}'}
            resp = await self._client.get(self._url(_SYNC), headers=self._headers(), params=params)  # type: ignore[union-attr]
            if resp.status_code == 200:
                self._sync_token = resp.json().get("next_batch")
        except Exception as e:
            log.warning("Matrix initial sync failed: %s", e)

        while self._connected:
            try:
                params = {"timeout": "30000"}
                if self._sync_token:
                    params["since"] = self._sync_token

                resp = await self._client.get(  # type: ignore[union-attr]
                    self._url(_SYNC),
                    headers=self._headers(),
                    params=params,
                    timeout=45.0,
                )
                if resp.status_code != 200:
                    log.warning("Matrix sync error (%d), retrying...", resp.status_code)
                    await asyncio.sleep(5)
                    continue

                data = resp.json()
                self._sync_token = data.get("next_batch", self._sync_token)

                # Process room events
                rooms = data.get("rooms", {}).get("join", {})
                for room_id, room_data in rooms.items():
                    events = room_data.get("timeline", {}).get("events", [])
                    for event in events:
                        await self._handle_event(room_id, event)

            except asyncio.CancelledError:
                break
            except httpx.ReadTimeout:
                continue  # normal for long-poll
            except Exception as e:
                log.warning("Matrix sync error: %s", e)
                await asyncio.sleep(5)

    async def _handle_event(self, room_id: str, event: dict) -> None:
        """Process a single Matrix room event."""
        if event.get("type") != "m.room.message":
            return

        sender = event.get("sender", "")
        # Skip our own messages
        if sender == self._user_id:
            return

        content = event.get("content", {})
        msgtype = content.get("msgtype", "")
        body = content.get("body", "")

        if msgtype != "m.text" or not body:
            return

        self._chat_id = room_id

        # Extract display name from sender (@user:server → user)
        display_name = sender.split(":")[0].lstrip("@") if ":" in sender else sender

        channel_msg = ChannelMessage(
            channel="matrix",
            sender=display_name,
            text=body,
            metadata={
                "chat_id": room_id,
                "sender_id": sender,
                "event_id": event.get("event_id", ""),
            },
        )

        if self._message_callback:
            try:
                response = await self._message_callback(channel_msg)
                if response:
                    await self.send_message(room_id, response)
            except Exception as e:
                log.error("Matrix message handler error: %s", e)

    # ── webhook (for external push, optional) ────────────────────────

    async def handle_webhook(self, payload: dict[str, Any]) -> str | None:
        """Process an incoming webhook event (e.g., from an appservice).

        For most setups, the sync loop handles messages directly.
        This is an alternative for Matrix Application Service bridges.
        """
        events = payload.get("events", [payload]) if "events" in payload else [payload]
        last_response = None

        for event in events:
            room_id = event.get("room_id", "")
            if room_id:
                await self._handle_event(room_id, event)

        return last_response
