import asyncio
import copy
import logging
from typing import Callable, cast, TYPE_CHECKING

from pywa.types.base_update import BaseUpdate, BaseUserUpdate
from . import errors, handlers
from . import utils
from .handlers import (
    MessageHandler,
    MessageStatusHandler,
    CallbackButtonHandler,
    CallbackSelectionHandler,
    FlowCompletionHandler,
    RawUpdateHandler,
    Handler,
    TemplateStatusUpdateHandler,
    ChatOpenedHandler,
    EncryptedFlowRequestType,
)
from .types import (
    Message,
)
from .types import (
    TemplateStatusUpdate,
    MessageStatus,
    CallbackButton,
    CallbackSelection,
    ChatOpened,
    FlowCompletion,
    ContinueHandling,
    StopHandling,
)

_logger = logging.getLogger(__name__)


if TYPE_CHECKING:
    from pywa_async import WhatsApp


class Server:
    async def webhook_challenge_handler(
        self: "WhatsApp", vt: str, ch: str
    ) -> tuple[str, int]:
        """
        Handle the verification challenge from the webhook manually.

        - Use this function only if you are using a custom server (e.g. Django etc.).

        Args:
            vt: The verify token param (utils.HUB_VT).
            ch: The challenge param (utils.HUB_CH).

        Returns:
            A tuple containing the challenge and the status code.
        """
        return super().webhook_challenge_handler(vt=vt, ch=ch)

    async def webhook_update_handler(
        self: "WhatsApp", update: bytes, hmac_header: str = None
    ) -> tuple[str, int]:
        """
        Handle the incoming update from the webhook manually.

        - Use this function only if you are using a custom server (e.g. Django etc.).

        Args:
            update: The incoming raw update from the webhook (bytes)
            hmac_header: The ``X-Hub-Signature-256`` header (to validate the signature, use ``utils.HUB_SIG`` for the key).

        Returns:
            A tuple containing the response and the status code.
        """
        res, status, update_dict, update_hash = self._check_and_prepare_update(
            update=update, hmac_header=hmac_header
        )
        if res:
            return res, status
        await self._call_handlers(update_dict)
        return self._after_handling_update(update_hash)

    def _register_routes(self: "WhatsApp") -> None:
        match self._server_type:
            case utils.ServerType.FLASK:
                import flask

                if not utils.is_installed("asgiref"):  # flask[async]
                    raise ValueError(
                        "Flask with ASGI is required to handle incoming updates asynchronously. Please install "
                        """the `asgiref` package (`pip install "flask[async]"` / `pip install "asgiref"`)"""
                    )

                _logger.info("Using Flask with ASGI")

                @self._server.route(self._webhook_endpoint, methods=["GET"])
                @utils.rename_func(f"('{self._webhook_endpoint}')")
                async def pywa_challenge() -> flask.Response:
                    """Automatically generated by pywa to handle the verification challenge."""
                    ch, code = await self.webhook_challenge_handler(
                        vt=flask.request.args.get(utils.HUB_VT),
                        ch=flask.request.args.get(utils.HUB_CH),
                    )
                    return flask.Response(
                        response=ch,
                        status=code,
                        content_type="text/plain",
                        headers={
                            "X-Content-Type-Options": "nosniff",
                        },
                    )

                @self._server.route(self._webhook_endpoint, methods=["POST"])
                @utils.rename_func(f"('{self._webhook_endpoint}')")
                async def pywa_webhook() -> flask.Response:
                    """Automatically generated by pywa to handle incoming updates."""
                    res, status = await self.webhook_update_handler(
                        update=flask.request.data,
                        hmac_header=flask.request.headers.get(utils.HUB_SIG),
                    )
                    return flask.Response(
                        response=res,
                        status=status,
                        content_type="text/plain",
                        headers={
                            "X-Content-Type-Options": "nosniff",
                        },
                    )

            case utils.ServerType.FASTAPI:
                _logger.info("Using FastAPI")
                import fastapi

                @self._server.get(self._webhook_endpoint)
                @utils.rename_func(f"('{self._webhook_endpoint}')")
                async def pywa_challenge(
                    vt: str = fastapi.Query(alias=utils.HUB_VT, example="xyzxyz"),
                    ch: str = fastapi.Query(alias=utils.HUB_CH, example="1858252904"),
                ) -> fastapi.Response:
                    """Automatically generated by pywa to handle the verification challenge."""
                    content, status_code = await self.webhook_challenge_handler(
                        vt=vt,
                        ch=ch,
                    )
                    return fastapi.Response(
                        content=content,
                        status_code=status_code,
                        media_type="text/plain",
                        headers={
                            "X-Content-Type-Options": "nosniff",
                        },
                    )

                @self._server.post(self._webhook_endpoint)
                @utils.rename_func(f"('{self._webhook_endpoint}')")
                async def pywa_webhook(
                    req: fastapi.Request,
                    hmac_header: str = fastapi.Header(
                        alias=utils.HUB_SIG, example="sha256=..."
                    ),
                ) -> fastapi.Response:
                    """Automatically generated by pywa to handle incoming updates."""
                    content, status_code = await self.webhook_update_handler(
                        update=await req.body(),
                        hmac_header=hmac_header,
                    )
                    return fastapi.Response(
                        content=content,
                        status_code=status_code,
                        media_type="text/plain",
                        headers={
                            "X-Content-Type-Options": "nosniff",
                        },
                    )
            case None:
                _logger.info("Using a custom server")

            case _:
                raise ValueError(
                    f"The `server` must be one of {utils.ServerType.protocols_names()} or None for a custom server"
                )

    def _register_flow_callback_wrapper(
        self: "WhatsApp",
        callback_wrapper: handlers.FlowRequestCallbackWrapper,
    ) -> handlers.FlowRequestCallbackWrapper:
        """Register the flow callback wrapper to the server."""
        match self._server_type:
            case utils.ServerType.FLASK:
                if not utils.is_installed("asgiref"):  # flask[async]
                    raise ValueError(
                        "Flask with ASGI is required to handle incoming updates asynchronously. Please install "
                        """the `asgiref` package (`pip install "flask[async]"` / `pip install "asgiref"`)"""
                    )

                import flask

                @self._server.route(callback_wrapper._endpoint, methods=["POST"])
                @utils.rename_func(f"('{callback_wrapper._endpoint}')")
                async def pywa_flow() -> tuple[str, int]:
                    """Automatically generated by pywa to handle incoming flow requests."""
                    return await callback_wrapper.handle_async(flask.request.json)

            case utils.ServerType.FASTAPI:
                import fastapi

                @self._server.post(callback_wrapper._endpoint)
                @utils.rename_func(f"('{callback_wrapper._endpoint}')")
                async def pywa_flow(
                    flow_req: EncryptedFlowRequestType,
                ) -> fastapi.Response:
                    """Automatically generated by pywa to handle incoming flow requests."""
                    response, status_code = await callback_wrapper.handle_async(
                        flow_req
                    )
                    return fastapi.Response(
                        content=response,
                        status_code=status_code,
                    )

        return callback_wrapper

    async def _call_handlers(self: "WhatsApp", update: dict) -> None:
        """Call the handlers for the given update."""
        try:
            try:
                handler_type = self._get_handler_type(update)
            except (KeyError, ValueError, TypeError, IndexError):
                (_logger.error if self._validate_updates else _logger.debug)(
                    "Webhook ('%s') received unexpected update%s: %s",
                    self._webhook_endpoint,
                    " (Enable `validate_updates` to ignore updates with invalid data)"
                    if not self._validate_updates
                    else "",
                    update,
                )
                handler_type = None

            if handler_type is None:
                return
            try:
                constructed_update: BaseUpdate = self._handlers_to_updates[
                    handler_type
                ].from_update(client=self, update=update)
                if await self._process_listener(constructed_update):
                    return
                await self._invoke_callbacks(handler_type, constructed_update)
            except Exception:
                _logger.exception("Failed to construct update: %s", update)
        finally:
            # Always call raw update handler last
            await self._call_raw_update_handler(update)

    async def _call_raw_update_handler(self: "WhatsApp", update: dict) -> None:
        """Invoke the raw update handler."""
        await self._invoke_callbacks(RawUpdateHandler, update)

    async def _invoke_callbacks(
        self: "WhatsApp", handler_type: type[Handler], update: BaseUpdate | dict
    ) -> None:
        """Process and call registered handlers for the update."""
        for handler in self._handlers[handler_type]:
            try:
                handled = await handler.ahandle(self, update)
            except StopHandling:
                break
            except ContinueHandling:
                continue
            except Exception:
                handled = True
                _logger.exception(
                    "An error occurred while '%s' was handling an update",
                    handler._callback.__name__,
                )
            if handled and not self._continue_handling:
                break

    async def _process_listener(self: "WhatsApp", update: BaseUpdate) -> bool:
        """Process and answer a listener if present."""
        listener = self._listeners.get(update.listener_identifier)
        if not listener:
            return False

        try:
            if await listener.apply_filters(self, update):
                listener.set_result(update)
                return not self._continue_handling
            elif await listener.apply_cancelers(self, update):
                listener.cancel(update)
                return not self._continue_handling
            else:
                return False  # if no filters or cancelers matched, continue handling
        except ContinueHandling:
            return False
        except StopHandling:
            return True
        except Exception as e:
            listener.set_exception(e)

        return not self._continue_handling

    def _register_callback_url(
        self: "WhatsApp",
        callback_url: str,
        callback_url_scope: utils.CallbackURLScope,
        app_id: int,
        app_secret: str,
        verify_token: str,
        fields: tuple[str, ...] | None,
    ) -> None:
        """
        This is a non-blocking function that registers the callback URL.
        It must be called after the server is running so that the challenge can be verified.
        """
        loop = asyncio.new_event_loop()
        api = copy.copy(self.api)
        api._session = self._httpx_client(  # TODO: copy the session properly
            timeout=api._session.timeout,
            base_url=api._session.base_url,
            headers=api._session.headers,
        )

        try:
            match callback_url_scope:
                case utils.CallbackURLScope.APP:
                    app_access_token = loop.run_until_complete(
                        api.get_app_access_token(app_id=app_id, app_secret=app_secret)
                    )
                    res = loop.run_until_complete(
                        api.set_app_callback_url(
                            app_id=app_id,
                            app_access_token=app_access_token["access_token"],
                            callback_url=callback_url,
                            verify_token=verify_token,
                            fields=fields,
                        )
                    )
                case utils.CallbackURLScope.WABA:
                    res = loop.run_until_complete(
                        api.set_waba_alternate_callback_url(
                            waba_id=self.business_account_id,
                            callback_url=callback_url,
                            verify_token=verify_token,
                        )
                    )
                case utils.CallbackURLScope.PHONE:
                    res = loop.run_until_complete(
                        api.set_phone_alternate_callback_url(
                            callback_url=callback_url,
                            verify_token=verify_token,
                            phone_id=self.phone_id,
                        )
                    )
                case _:
                    raise ValueError("Invalid callback URL scope")

            if not res["success"]:
                raise RuntimeError("Failed to register callback URL.")
            _logger.info("Callback URL '%s' registered successfully", callback_url)
        except errors.WhatsAppError as e:
            raise RuntimeError(
                f"Failed to register callback URL '{callback_url}'. if you are using a slow/custom server, you can "
                "increase the delay using the `webhook_challenge_delay` parameter when initializing the WhatsApp client."
            ) from e
