from __future__ import annotations

import logging
import queue

from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse

from qtoggleserver import persist
from qtoggleserver.conf import settings
from qtoggleserver.core import responses as core_responses
from qtoggleserver.core.api import auth as core_api_auth
from qtoggleserver.core.device import attrs as core_device_attrs
from qtoggleserver.core.typing import GenericJSONDict
from qtoggleserver.utils import json as json_utils


logger = logging.getLogger(__name__)

_webhooks: Webhooks | None = None


# TODO: add password support


class WebhooksError(Exception):
    pass


class InvalidParamError(WebhooksError):
    def __init__(self, param: str) -> None:
        self.param: str = param

        super().__init__(f"Invalid field: {param}")


class WebhooksRequest:
    def __init__(self, body: GenericJSONDict) -> None:
        self.body: GenericJSONDict = body
        self.retries: int = 0


class Webhooks:
    def __init__(
        self,
        scheme: str | None = None,
        host: str | None = None,
        port: int | None = None,
        path: str | None = None,
        timeout: int | None = None,
        retries: int | None = None,
        **kwargs,
    ) -> None:
        # The enabled value comes with kwargs but is ignored; webhooks will be explicitly enabled afterwards

        self._enabled: bool = False
        self._scheme: str | None = scheme
        self._host: str | None = host
        self._port: int | None = port
        self._path: str | None = path
        self._timeout: int | None = timeout
        self._retries: int | None = retries
        self._url: str | None = None

        self._queue: queue.Queue = queue.Queue(settings.core.event_queue_size)

    def __str__(self) -> str:
        s = "webhooks"

        if not self._enabled:
            s += " [disabled]"

        if self._scheme:
            s += " " + self.get_url()

        return s

    def is_enabled(self) -> bool:
        return self._enabled

    def enable(self) -> None:
        if self._enabled:
            return

        logger.debug("enabling %s", self)

        self._enabled = True

    def disable(self) -> None:
        if not self._enabled:
            return

        logger.debug("disabling %s", self)

        self._enabled = False

        # Drop all queued requests
        while not self._queue.empty():
            self._queue.get()

    def get_url(self) -> str:
        if not self._url:
            if self._scheme == "http" and self._port == 80 or self._scheme == "https" and self._port == 443:
                self._url = f"{self._scheme}://{self._host}{self._path}"
            else:
                self._url = f"{self._scheme}://{self._host}:{self._port}{self._path}"

        return self._url

    def call(self, body: GenericJSONDict) -> None:
        if not self._enabled:
            return None

        request = WebhooksRequest(body)

        if self._queue.empty():
            return self._request(request)

        try:
            self._queue.put(request)
        except queue.Full:
            logger.error("%s: queue is full", self)

        return None

    def _request(self, request: WebhooksRequest) -> None:
        if not self._enabled:
            return

        url = self.get_url()
        body = json_utils.dumps(request.body)

        def on_response(response: HTTPResponse) -> None:
            try:
                core_responses.parse(response)
            except core_responses.Error as e:
                logger.error("%s: call failed: %s", self, e)

                # Retry mechanism
                if not self._retries:
                    return self._check_pending()

                if request.retries <= self._retries:
                    request.retries += 1
                    logger.debug("%s: resending request (retry %s/%s)", self, request.retries, self._retries)

                    self._request(request)
                else:
                    self._check_pending()
            else:
                logger.debug("%s: call succeeded", self)
                self._check_pending()

            return None

        http_client = AsyncHTTPClient()
        # TODO use webhooks password
        headers = {
            "Content-Type": json_utils.JSON_CONTENT_TYPE,
            "Authorization": core_api_auth.make_auth_header(
                core_api_auth.ORIGIN_DEVICE, username=None, password_hash=core_device_attrs.normal_password_hash
            ),
        }
        request = HTTPRequest(
            url,
            "POST",
            headers=headers,
            body=body,
            connect_timeout=self._timeout,
            request_timeout=self._timeout,
            follow_redirects=True,
        )

        logger.debug("%s: calling", self)

        http_client.fetch(request, callback=on_response)  # TODO implement me using await

    def _check_pending(self) -> None:
        try:
            request = self._queue.get(block=False)
        except queue.Empty:
            return

        self._request(request)

    def to_json(self) -> GenericJSONDict:
        d = {
            "enabled": self._enabled,
            "scheme": self._scheme,
            "host": self._host,
            "port": self._port,
            "path": self._path,
            "timeout": self._timeout,
            "retries": self._retries,
        }

        return d


def get() -> Webhooks | None:
    return _webhooks


def setup(
    enabled: bool,
    scheme: str | None = None,
    host: str | None = None,
    port: int | None = None,
    path: str | None = None,
    timeout: int | None = None,
    retries: int | None = None,
    **kwargs,
) -> None:
    global _webhooks

    if _webhooks and _webhooks.is_enabled():
        _webhooks.disable()

    _webhooks = Webhooks(scheme, host, port, path, timeout, retries)
    if enabled:
        _webhooks.enable()


async def load() -> None:
    data = await persist.get_value("webhooks")
    if data is None:
        setup(enabled=False)
        logger.debug("loaded %s", _webhooks)

        return

    setup(**data)
    logger.debug("loaded %s", _webhooks)


async def save() -> None:
    if _webhooks is None:
        return

    logger.debug("saving persisted data")
    await persist.set_value("webhooks", _webhooks.to_json())


async def reset() -> None:
    logger.debug("clearing persisted data")
    await persist.remove("webhooks")


async def init() -> None:
    logger.debug("loading persisted data")
    await load()


async def cleanup() -> None:
    pass
