"""Async webhook HTTP server for pararam.io bot events."""

from __future__ import annotations

import hmac
import logging
from collections.abc import Awaitable, Callable
from typing import Any, Self

from aiohttp import web
from pydantic import ValidationError

from pararamio_bot.models import WebhookAction, WebhookMessage
from pararamio_bot.signature import extract_api_key, extract_url_key, verify_signature
from pararamio_bot.types import ActionCallback, MessageCallback

__all__ = ('WebhookServer',)

logger = logging.getLogger(__name__)

SenderValidator = Callable[[WebhookMessage | WebhookAction], Awaitable[str | None]]
"""Async callable that validates a sender.

Returns None if allowed, or a string with rejection reason (sent as bot reply).
"""


async def _default_validate_sender(_message: WebhookMessage | WebhookAction) -> str | None:
    """Default sender validator — allows all senders."""
    return None


class WebhookServer:
    """Async HTTP server that receives pararam.io bot webhook events.

    Args:
        bot_secret: Full bot secret key.
        host: Bind address.
        port: Listen port.
        validate_sender: Async callable that checks if the sender is allowed.
            Receives WebhookMessage or WebhookAction, returns None to allow
            or a string rejection reason (sent back as bot reply).
            Default: allow all senders.

    Usage::

        async with WebhookServer(bot_secret='...') as server:
            server.on_message(my_message_handler)
            server.on_action(my_action_handler)
            # server is listening, await something else or sleep

    With sender validation::

        async def validate(msg):
            if msg.user_unique_name not in allowed_users:
                return f'@{msg.user_unique_name} not authorized'
            return None


        server = WebhookServer(bot_secret='...', validate_sender=validate)
    """

    def __init__(
        self,
        bot_secret: str,
        *,
        host: str = '0.0.0.0',
        port: int = 8080,
        validate_sender: SenderValidator = _default_validate_sender,
    ) -> None:
        self._bot_secret = bot_secret
        self._url_key = extract_url_key(bot_secret)
        self._api_key = extract_api_key(bot_secret)
        self._host = host
        self._port = port
        self._validate_sender = validate_sender
        self._message_callbacks: list[MessageCallback] = []
        self._action_callbacks: list[ActionCallback] = []
        self._app = web.Application()
        self._app.router.add_post('/{url_key}', self._handle_message)
        self._app.router.add_post('/{url_key}/act', self._handle_action)
        self._runner: web.AppRunner | None = None

    def on_message(self, callback: MessageCallback) -> None:
        """Register a handler for incoming message webhooks."""
        self._message_callbacks.append(callback)

    def on_action(self, callback: ActionCallback) -> None:
        """Register a handler for bot action webhooks."""
        self._action_callbacks.append(callback)

    async def start(self) -> None:
        """Start the HTTP server (non-blocking)."""
        self._runner = web.AppRunner(self._app)
        await self._runner.setup()
        site = web.TCPSite(self._runner, self._host, self._port)
        await site.start()
        logger.info('Webhook server listening on %s:%d', self._host, self._port)

    async def stop(self) -> None:
        """Stop the HTTP server gracefully."""
        if self._runner is not None:
            await self._runner.cleanup()
            self._runner = None
            logger.info('Webhook server stopped')

    async def __aenter__(self) -> Self:
        await self.start()
        return self

    async def __aexit__(self, *_args: Any) -> None:
        await self.stop()

    # -- request handlers --

    def _verify_url_key(self, request: web.Request) -> web.Response | None:
        """Verify URL key matches the bot secret. Returns error response or None if OK."""
        url_key = request.match_info.get('url_key', '')
        if not hmac.compare_digest(url_key, self._url_key):
            return web.Response(status=404)
        return None

    def _verify_signature(self, body: bytes, signature: str | None) -> web.Response | None:
        """Verify X-Signature header. Returns error response or None if OK."""
        if not signature:
            return web.Response(status=401, text='Missing X-Signature header')
        if not verify_signature(body, signature, self._bot_secret):
            return web.Response(status=401, text='Invalid signature')
        return None

    def _verify_request(self, request: web.Request, body: bytes) -> web.Response | None:
        """Verify URL key and signature. Returns error response or None if OK."""
        error = self._verify_url_key(request)
        if error is not None:
            return error
        signature = request.headers.get('X-Signature')
        return self._verify_signature(body, signature)

    async def _handle_message(self, request: web.Request) -> web.Response:
        body = await request.read()

        error = self._verify_request(request, body)
        if error is not None:
            return error

        try:
            message = WebhookMessage.model_validate_json(body)
        except ValidationError as exc:
            logger.warning('Invalid webhook message payload: %s', exc)
            return web.Response(status=400, text='Invalid payload')

        # Validate sender (None=allowed, ''=silent reject, 'text'=reject with reply)
        rejection = await self._validate_sender(message)
        if rejection is not None:
            if rejection:
                logger.info('Sender rejected: %s — %s', message.user_unique_name, rejection)
                return web.json_response({'text': rejection})
            return web.json_response({})

        reply_text: str | None = None
        for callback in self._message_callbacks:
            result = await callback(message)
            if result is not None and reply_text is None:
                reply_text = result

        if reply_text:
            return web.json_response({'text': reply_text})
        return web.json_response({})

    async def _handle_action(self, request: web.Request) -> web.Response:
        body = await request.read()

        error = self._verify_request(request, body)
        if error is not None:
            return error

        try:
            action = WebhookAction.model_validate_json(body)
        except ValidationError as exc:
            logger.warning('Invalid webhook action payload: %s', exc)
            return web.Response(status=400, text='Invalid payload')

        # Validate sender (None=allowed, ''=silent reject, 'text'=reject with reply)
        rejection = await self._validate_sender(action)
        if rejection is not None:
            if rejection:
                logger.info('Sender rejected: user_id=%d — %s', action.user_id, rejection)
                return web.json_response({'text': rejection})
            return web.json_response({})

        reply_text: str | None = None
        for callback in self._action_callbacks:
            result = await callback(action)
            if result is not None and reply_text is None:
                reply_text = result

        if reply_text:
            return web.json_response({'text': reply_text})
        return web.json_response({})
