import asyncio
from lchttp.json import json_dumps
import logging
from signal import SIGINT, SIGTERM
from typing import Any

from nio import AsyncClient
from nio.events.room_events import RoomMessageText
from nio.exceptions import LocalProtocolError
from nio.responses import JoinError, RoomSendError
from nio.rooms import MatrixRoom

from markdown import markdown

from . import utils

logger = logging.getLogger(__name__)


class ClarionServer:
    """
    Clarion ASGI Server.
    """

    def __init__(
        self,
        server: str,
        user: str,
        password: str,
        application: str,
    ):
        self.server = server
        self.user = user
        self.password = password
        self.application = utils.get_application(application)

        self.client = AsyncClient(self.server, self.user)
        self.client.add_event_callback(self.message_callback, RoomMessageText)

        self.app_scope = {
            "type": "concord",
            "asgi": {
                "version": "3.0",
                "spec_version": "1.0",
            },
        }
        self.queue: asyncio.Queue | None = None
        self.event: asyncio.Event | None = None

    def run(self) -> None:
        """
        Start the synchronous ASGI server.
        """
        logger.info("Starting Clarion ASGI server")
        asyncio.run(self.main())
        logger.info("Clarion ASGI server shutdown complete")

    async def login(self) -> None:
        """
        Login or re-login to the Clarion homeserver.
        """
        await self.client.login(self.password)

    async def main(self, event: asyncio.Event | None = None) -> None:
        """
        Start the asynchronous ASGI server.
        """
        self.queue = asyncio.Queue()
        self.event = event or asyncio.Event()

        if event is None:
            for sig in (SIGINT, SIGTERM):
                asyncio.get_running_loop().add_signal_handler(sig, utils.terminate, self.event, sig)

        await self.login()
        asyncio.create_task(
            self.application(self.app_scope, receive=self.queue.get, send=self.app_send),  # type: ignore[operator]
        )
        asyncio.create_task(
            self.client.sync_forever(timeout=36_000),
        )
        await self.queue.put({
            "type": "clarion.connect",
        })

        logger.info("Clarion ASGI Server startup complete")

        if event is None:
            await self.serve()

    async def serve(self) -> None:
        """
        Server requests forever.
        """
        if self.event is None or self.queue is None:
            return
        await self.event.wait()
        await self.queue.put({
            "type": "clarion.disconnect",
        })
        logger.info("Stopping Clarion ASGI Server")
        await self.client.close()

    async def app_send(self, message: dict) -> None:
        """
        ASGI application `send` method: Dispatch incoming Channel messages.
        """
        logger.debug(f"app_send {message=}")
        await self.clarion_room_send(message["room"], message["body"])

    async def message_callback(self, room: MatrixRoom, event: RoomMessageText) -> None:
        """
        Handle an incoming message from matrix-nio client: put it in the Queue.
        """
        logger.debug("message_callback {room=} {event=}")
        if not self.queue:
            return
        await self.queue.put({
            "type": "clarion.receive",
            "room": room.display_name,
            "user": room.user_name(event.sender),
            "body": event.body,
        })

    async def join_room(self, room_id: str) -> bool:
        """
        Use matrix-nio client to join a room.
        """
        logger.debug(f"Join room {room_id=}")

        for _ in range(10):
            try:
                resp = await self.client.join(room_id)
                if not isinstance(resp, JoinError):
                    return True
                if resp.status_code == "M_UNKNOWN_TOKEN":
                    logger.warning("Reconnecting")
                    await self.login()
                elif resp.status_code in ["M_FORBIDDEN", "M_CONSENT_NOT_GIVEN"]:
                    logger.error("room access is forbidden")
                    return False
                elif resp.status_code == "M_UNKNOWN":
                    logger.error(f"join error: {resp.transport_response.status}")
                    return False
            except LocalProtocolError as e:
                logger.error(f"Send error: {e}")
            logger.warning("Trying again")
        logger.error("Clarion Homeserver not responding")
        return False

    async def send_room_message(self, room_id: str, content: dict[Any, Any]) -> bool:
        """
        Use matrix-nio client to send a Clarion message to a room.
        """
        logger.debug(f"Sending room message in {room_id=}: {content=}")

        for _ in range(10):
            try:
                resp = await self.client.room_send(
                    room_id=room_id,
                    message_type="m.room.message",
                    content=content,
                )
                if not isinstance(resp, RoomSendError):
                    return True
                if resp.status_code == "M_UNKNOWN_TOKEN":
                    logger.warning("Unknown token - reconnecting to Clarion Homeserver")
                    await self.login()
                else:
                    logger.error(f"room send error {resp=}")
                    return False
            except LocalProtocolError as e:
                logger.error(f"Send error: {e}")
            logger.warning("Trying again")
        logger.error("Clarion Homeserver not responding")
        return False

    async def clarion_room_send(self, room: str, message: str) -> None:
        """
        Handle an incoming `clarion.send` message from Channel,
        and forward it to Clarion.
        """
        content = {
            "msgtype": "m.text",
            "body": message,
            "format": "org.matrix.custom.html",
            "formatted_body": markdown(message, extensions=["extra"]),
        }
        await self.send_room_message(room, content)

    async def clarion_record_send(self, room: str, message: str) -> None:
        """
        Handle an incoming `clarion.record` message from Channel,
        and forward it to Clarion.

        The record is dumped to JSON before posting to the room.
        """
        if not isinstance(message, str):
            message = json_dumps(message, default=str, rtype=str)

        content = {
            "msgtype": "m.text",
            "body": message,
        }
        await self.send_room_message(room, content)
