# SPDX-FileCopyrightText: 2025 XLeitstelle Planen und Bauen <xleitstelle@gv.hamburg.de>
# SPDX-FileContributor: Anton Jacobsson <anton.jacobsson@init.de>
# SPDX-FileContributor: Tobias Kraft <tobias.kraft@gv.hamburg.de>
#
# SPDX-License-Identifier: EUPL-1.2

import json
import logging

from packaging.requirements import Requirement
from packaging.version import Version
from qgis.core import (
    QgsNetworkAccessManager,
)
from qgis.PyQt.QtCore import QObject, QTimer, QUrl, pyqtSignal
from qgis.PyQt.QtNetwork import QNetworkReply, QNetworkRequest

from xmas_plugin.settings_manager import load_setting
from xmas_plugin.util.db import get_db_schema
from xmas_plugin.util.metadata import DEPENDENCIES, PLUGIN_DIR_NAME, PLUGIN_NAME

logger = logging.getLogger(PLUGIN_DIR_NAME)


class HealthChecker(QObject):
    statusChanged = pyqtSignal(bool)
    error = pyqtSignal(str)

    def __init__(self, parent=None, user_agent=PLUGIN_NAME):
        super().__init__(parent)
        self.nam = QgsNetworkAccessManager(self)
        self.user_agent = user_agent
        self._reply = None
        self._timeout = QTimer(self)
        self._timeout.setSingleShot(True)
        self._timeout.timeout.connect(self._on_timeout)
        self._version_checked_for: str | None = None
        self.response: dict | None = None

    def check(self, url: str, timeout_ms: int = 15000):
        if self._reply is not None:
            self._reply.abort()
            self._cleanup()

        req = QNetworkRequest(QUrl(url))
        req.setRawHeader(b"User-Agent", self.user_agent.encode("utf-8"))
        req.setAttribute(
            QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.AlwaysNetwork
        )
        req.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        req.setRawHeader(b"Cache-Control", b"no-cache")
        req.setRawHeader(b"Pragma", b"no-cache")

        # Explicitly GET
        self._reply = self.nam.get(req)
        self._reply.finished.connect(self._on_finished)
        self._reply.errorOccurred.connect(self._on_error)
        self._timeout.start(timeout_ms)

    def _parse_json_response(self) -> dict:
        if self._reply is None:
            raise RuntimeError("no reply")

        data = bytes(self._reply.readAll())
        if not data:
            raise ValueError("empty response")

        resp = json.loads(data.decode("utf-8"))
        if not isinstance(resp, dict):
            raise TypeError(f"unexpected response type: {type(resp)!r}")
        return resp

    def _check_webapp_version(self, resp: dict) -> None:
        version = resp.get("version")
        if not version:
            raise KeyError(f"version key not found in response: {resp}")

        app_version = Version(version)
        dep = DEPENDENCIES["optional"][0]
        if not dep.lower().startswith("xmas"):
            raise RuntimeError(
                f"unexpected dependency for webapp version check: {dep!r}"
            )

        app_req = Requirement(dep).specifier
        if not app_req.contains(app_version):
            raise RuntimeError(
                f"Webapp-Version '{app_version}' entspricht nicht der erwarteten Version '{app_req}'"
            )

    def _check_db_schema(self, resp: dict) -> None:
        webapp_schema = resp.get("db_schema")
        if not webapp_schema:
            raise RuntimeError(f"db_schema missing in health_check response: {resp}")

        plugin_schema = get_db_schema(load_setting("db_connection", "")) or "public"
        if not plugin_schema:
            raise RuntimeError("plugin db_schema not configured")

        if webapp_schema != plugin_schema:
            raise RuntimeError(
                f"Datenbankschema stimmt nicht überein (Plugin={plugin_schema}, Webapp={webapp_schema}). "
                "Bitte Schema in QGIS-Verbindung bzw. Webapp-Konfiguration angleichen."
            )

    def stop(self):
        if self._reply is not None:
            self._reply.abort()
        self._cleanup()

    def _on_finished(self):
        if self._reply is None:
            logger.debug("[Health] finished after cleanup; ignoring")
            return

        code = self._reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        err = self._reply.error()
        ok_http = (err == QNetworkReply.NoError) and (
            code in (200, 204, 301, 302, 307, 308)
        )

        # Only log when something's wrong
        if not ok_http:
            logger.warning("[Health] finished bad: code=%s err=%s", code, int(err))
            self.statusChanged.emit(False)
            self._cleanup()
            return

        # parse + checks
        try:
            resp = self._parse_json_response()
            self.response = resp

            # schema should be checked every time (fast + important)
            # self._check_db_schema(resp)

            # version check once per URL
            url = self._reply.url().toString()
            if self._version_checked_for != url:
                self._check_webapp_version(resp)
                self._version_checked_for = url

        except Exception as e:
            logger.exception("[Health] validation failed: %s", e)
            self.statusChanged.emit(False)
            self.error.emit(str(e))

            self._cleanup()
            return

        self.statusChanged.emit(True)
        self._cleanup()

    def _on_error(self, _code):
        if (
            self._reply is not None
            and self._reply.error() != QNetworkReply.OperationCanceledError
        ):
            self.statusChanged.emit(False)
            self.error.emit(self._reply.errorString())
        self._version_checked_for = None
        self._cleanup()

    def _on_timeout(self):
        if self._reply is not None:
            self._reply.abort()
        self.statusChanged.emit(False)
        self.error.emit("Health check timed out")
        self._cleanup()

    def _cleanup(self):
        self._timeout.stop()
        if self._reply is not None:
            self._reply.deleteLater()
            self._reply = None
