# SPDX-FileCopyrightText: 2025 XLeitstelle Planen und Bauen <xleitstelle@gv.hamburg.de>
# SPDX-FileContributor: Tobias Kraft <tobias.kraft@gv.hamburg.de>
# SPDX-FileContributor: Michael Holzapfel <michael.holzapfel@geocledian.com>
#
# SPDX-License-Identifier: EUPL-1.2

import json
import logging
from uuid import UUID, uuid4

from qgis.core import (
    QgsFeature,
    QgsFeatureRequest,
    QgsFieldConstraints,
    QgsProject,
    QgsVectorLayer,
)
from qgis.gui import (
    QgsEditorWidgetFactory,
    QgsEditorWidgetWrapper,
    QgsGui,
)
from qgis.PyQt.QtCore import (
    QObject,
    QSize,
    QUrl,
    QUrlQuery,
    QVariant,
    pyqtSignal,
    pyqtSlot,
)
from qgis.PyQt.QtWebEngineWidgets import QWebEngineView
from qgis.PyQt.QtWidgets import QWidget
from qgis.utils import iface

from xmas_plugin.settings_manager import get_normalized_url
from xmas_plugin.util.metadata import PLUGIN_DIR_NAME, PLUGIN_NAME
from xmas_plugin.util.webengine import (
    wire_page_lifecycle,
)
from xmas_plugin.widgets.handler import FeatureInteractionHandler
from xmas_plugin.widgets.page import XMASPluginPage

logger = logging.getLogger(PLUGIN_DIR_NAME)

WIDGET_SHORTNAME = PLUGIN_DIR_NAME
WIDGET_NAME = f"{PLUGIN_NAME} Editor"


class CallHandler(FeatureInteractionHandler):
    """Transfers the QGIS feature data to/from the webapp via QWebChannel."""

    dataReceived = pyqtSignal(QVariant)

    def __init__(self, parent: QObject, view: "XMASPluginEditorWidget"):
        super().__init__(parent)
        self.view = view

    @pyqtSlot(result=QVariant)
    def transfer_feature(self):
        """Transfer feature data via webchannel to the webapp."""
        data = {}
        if self.view._editable:
            attributes = self.view._feature.attributeMap()
            properties = attributes.get("properties", {})
            if not properties and (
                property_filter := self.view._layer.customProperty(
                    f"{PLUGIN_DIR_NAME}/property_filter"
                )
            ):
                try:
                    properties = json.loads(property_filter)
                except json.JSONDecodeError as e:
                    logger.error(f"Error decoding property filter: {e}")
            data["properties"] = properties
            if self.view._layer.isSpatial():
                geom = self.view._feature.geometry()
                if not geom.isEmpty():
                    data["geometry"] = {
                        "wkt": geom.asWkt().upper(),
                        "srid": self.view._layer.sourceCrs().postgisSrid(),
                    }
        return data

    @pyqtSlot(QVariant)
    def receive_feature(self, feature_data: dict):
        """Receive feature data via webchannel from the webapp."""
        self.dataReceived.emit(feature_data)


class XMASPluginEditorPage(XMASPluginPage):
    def acceptNavigationRequest(self, new_url, _type, isMainFrame):
        """Intercept navigation to a referenced feature in the webapp and open new attribute form instead."""
        try:
            _, feature_path, new_id = new_url.path().split("/")
            assert feature_path == "feature"
            UUID(new_id)
        except Exception:
            return super().acceptNavigationRequest(new_url, _type, isMainFrame)
        feature = getattr(self.view(), "_feature", None)
        if not feature:
            return False
        if feature["id"] == new_id:
            return True
        new_feature = None
        new_layer = None
        # Find referenced feature in map layers by id
        for map_layer in QgsProject.instance().mapLayers().values():
            if map_layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id"):
                request = (
                    QgsFeatureRequest()
                    .setFlags(QgsFeatureRequest.NoGeometry)
                    .setFilterExpression(f"id = '{new_id}'")
                )
                for req_feature in map_layer.getFeatures(request):
                    new_feature = req_feature
                    new_layer = map_layer
        iface.openFeatureForm(new_layer, new_feature, showModal=False)
        return False


class XMASPluginEditorWidget(QWebEngineView):
    """Widget to edit features.

    The form for attributes is provided by a webapp backend.
    Edits are reflected to QGIS via QWebchannel.

    Loading of the remote form is deferred until the widget becomes visible.
    A pending URL is kept if the widget is not visible at the time a feature
    is set, and the actual QWebEngineView load is performed in `showEvent`.
    """

    dataChanged = pyqtSignal()

    def __init__(self, layer: QgsVectorLayer, parent: QWidget | None = None):
        super().__init__(parent)
        self._feature: QgsFeature | None = None
        self._layer = layer
        self._data: dict = {}
        self._parent = parent
        self._editable = self._layer.isEditable()
        self.setMinimumSize(QSize(800, 600))

        # Pending URL to load when the widget becomes visible.
        self._pending_url: QUrl | None = None

        # set custom page that handles url changes/clicked links
        page = XMASPluginEditorPage(self)
        # attach page (parented) to view
        self.setPage(page)

        # Keep track whether lifecycle wiring was already done.
        self._lifecycle_wired = False

        self.loadFinished.connect(lambda _: self._emit_edit_mode())

    def _emit_edit_mode(self):
        self.page().runJavaScript(
            f"try {{emitEvent('editModeChanged', {self._editable});}} catch (e) {{}}"
        )

    def _update_data(self, feature_data: dict):
        changed = False
        if self._feature["properties"] != (
            properties := feature_data.get("properties")
        ):
            self._feature["properties"] = properties or QVariant()
            changed = True
        if self._feature["featuretype"] != (
            featuretype := feature_data.get("featuretype")
        ):
            self._feature["featuretype"] = featuretype or QVariant()
            changed = True
        if changed:
            self.dataChanged.emit()

    def _load_url(self):
        """Perform the actual load of the URL into the QWebEngineView."""
        # wire reload lifecycle only once, using factory
        if not getattr(self, "_lifecycle_wired", False):
            # Factory method to create and return the handler
            def make_handler(parent_channel):
                handler = CallHandler(parent_channel, self)
                handler.dataReceived.connect(self._update_data)
                return handler

            wire_page_lifecycle(self, make_handler)
            self._lifecycle_wired = True

        # finally load the URL
        if url := self._pending_url:
            self._pending_url = None
            self.load(url)

    def _prepare_url(self):
        """Prepare the URL and either load it or defer loading until visible."""
        feature = self._feature
        layer = self._layer

        try:
            id = feature["id"]
        except Exception:
            return

        url = QUrl(f"{get_normalized_url()}/feature/{id}")
        # set query params
        query = QUrlQuery()
        query.addQueryItem("planId", layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id"))
        if layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") in [
            "presentation",
            "subject",
        ] and (bereich_id := layer.customProperty(f"{PLUGIN_DIR_NAME}/bereich_id")):
            query.addQueryItem("parentId", bereich_id)
        elif layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") == "section":
            query.addQueryItem(
                "parentId", layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id")
            )
        query.addQueryItem("editable", str(self._editable).lower())
        if feature["featuretype"]:
            query.addQueryItem("featureType", feature["featuretype"])
        query.addQueryItem("wkbType", layer.wkbType().name)
        query.addQueryItem("appschema", feature["appschema"])
        query.addQueryItem("version", feature["version"])
        if featuretype_regex := layer.customProperty(
            f"{PLUGIN_DIR_NAME}/featuretype_regex", None
        ):
            query.addQueryItem("featuretypeRegex", featuretype_regex)
        url.setQuery(query)

        self._pending_url = url

        # if the widget is visible, load immediately; otherwise, defer
        if self.isVisible():
            self._load_url()

    def showEvent(self, event):
        """When the widget becomes visible, process any pending load."""
        super().showEvent(event)
        if self._pending_url:
            self._load_url()


class XMASPluginEditorWidgetWrapper(QgsEditorWidgetWrapper):
    """Wrapper for a XMASPluginEditor to handle communication with QGIS."""

    def __init__(
        self,
        layer: QgsVectorLayer,
        fieldIdx: int,
        editor: QWidget | None = None,
        parent: QWidget | None = None,
    ):
        super(XMASPluginEditorWidgetWrapper, self).__init__(
            layer, fieldIdx, editor, parent
        )
        self._layer = layer
        self._fieldIdx = fieldIdx
        self._widget = editor

    # Implementation of methods from QgsEditorWidgetWrapper

    def createWidget(self, parent: QWidget):
        return XMASPluginEditorWidget(self._layer, parent)

    def initWidget(self, editor: QWidget):
        self._widget = editor
        if self.valid():
            self._widget.dataChanged.connect(self._update_data)
            self._widget.dataChanged.connect(
                lambda: logger.debug(
                    "[editor] dataChanged: %s", self._widget._feature.attributes()
                )
            )

    def valid(self) -> bool:
        return isinstance(self._widget, XMASPluginEditorWidget)

    def additionalFields(self):
        return ["id", "featuretype"]

    def additionalFieldValues(self):
        if isinstance(self._widget._feature, QgsFeature):
            return [self._widget._feature["id"], self._widget._feature["featuretype"]]
        else:
            return [QVariant(), QVariant()]

    def setFeature(self, feature: QgsFeature):
        if not self.valid() or self._widget._feature == feature:
            return
        attributes = feature.attributeMap()
        # test for id field
        if id_value := attributes.get("id"):
            try:
                UUID(id_value)
            except Exception:
                feature["id"] = str(uuid4())
        else:
            return
        self._widget._feature = feature
        self._widget._prepare_url()

    def setEnabled(self, enabled: bool):
        if self.valid():
            self._widget._editable = enabled
            self._widget._emit_edit_mode()

    def value(self):
        if isinstance(self._widget._feature, QgsFeature):
            logger.debug(
                "[editor] wrapper.value: %s", self._widget._feature["properties"]
            )
            return self._widget._feature["properties"] or QVariant()
        else:
            return QVariant()

    def setValue(self, value):
        if (
            not self.valid()
            or not self._widget._feature
            or self._widget._feature["properties"] == value
        ):
            return
        self._widget._feature["properties"] = value
        self._update_data()
        self._widget._prepare_url()

    # Implementation of helper methods

    def _update_data(self):
        self.valuesChanged.emit(
            self._widget._feature["properties"],
            [self._widget._feature["id"], self._widget._feature["featuretype"]],
        )
        self.updateConstraint(
            self._widget._feature,
            QgsFieldConstraints.ConstraintOrigin.ConstraintOriginProvider,
        )


class XMASPluginEditorFactory(QgsEditorWidgetFactory):
    """Factory for a XMASPluginEditor."""

    @classmethod
    def register(cls):
        QgsGui.editorWidgetRegistry().registerWidget(WIDGET_SHORTNAME, cls())

    def __init__(self):
        QgsEditorWidgetFactory.__init__(self, WIDGET_NAME)

    def create(self, layer, fieldIdx, editor, parent):
        return XMASPluginEditorWidgetWrapper(layer, fieldIdx, editor, parent)

    def configWidget(self, *_):
        return None
