# SPDX-FileCopyrightText: 2025 XLeitstelle Planen und Bauen <xleitstelle@gv.hamburg.de>
# SPDX-FileContributor: Tobias Kraft <tobias.kraft@gv.hamburg.de>
#
# SPDX-License-Identifier: EUPL-1.2
import logging
import re

from qgis.core import (
    Qgis,
    QgsDefaultValue,
    QgsEditError,
    QgsFeature,
    QgsMessageLog,
    QgsProject,
    QgsVectorLayer,
    QgsVectorLayerUtils,
    edit,
)
from qgis.gui import (
    QgsAttributeEditorContext,
    QgsMapToolDigitizeFeature,
)
from qgis.PyQt.QtCore import QObject, QTimer, QVariant, pyqtSignal, pyqtSlot
from qgis.utils import iface

from xmas_plugin.util.geom import transform_to_project_crs
from xmas_plugin.util.layer import get_layer_and_feature
from xmas_plugin.util.metadata import PLUGIN_DIR_NAME, PLUGIN_NAME

logger = logging.getLogger(PLUGIN_DIR_NAME)


class FeatureInteractionHandler(QObject):
    def __init__(self, parent=None):
        super().__init__(parent)

    def _normalize_feature_payload(self, data) -> dict:
        if isinstance(data, dict):
            return data
        if isinstance(data, str):
            return {"target": data.strip()}
        raise TypeError(f"Unexpected payload type: {type(data)}: {data!r}")

    @pyqtSlot("QVariant")
    def highlight_feature(self, data):
        """Highlights a feature (by UUID or FID) and zooms in on it."""
        logger.info("highlight_feature data type=%s value=%r", type(data), data)

        payload = self._normalize_feature_payload(data)
        target = payload.get("target")

        if not target:
            raise ValueError(f"No target provided in payload: {data!r}")

        target_layer, target_feature = get_layer_and_feature(target)

        if target_layer is None or target_feature is None:
            iface.messageBar().pushCritical(
                "Zum Feature springen", "Feature nicht gefunden"
            )
            logger.debug("No layer/feature found for feature_key=%s", target)
            return

        geom = transform_to_project_crs(target_feature.geometry(), target_layer.crs())

        iface.mapCanvas().setExtent(geom.boundingBox())
        iface.mapCanvas().flashGeometries([geom])
        iface.mapCanvas().refresh()

        logger.debug("Zoom and highlight successful")

    @pyqtSlot("QVariant")
    def show_attribute_form(self, data):
        """Open the QGIS feature form for a specific feature ID."""
        logger.debug(
            "show_attribute_form data is of type: %s, value: %r", type(data), data
        )

        payload = self._normalize_feature_payload(data)
        target = payload.get("target")
        source = payload.get("source")

        if not target:
            raise ValueError(f"No target provided in payload: {data!r}")

        target_layer, target_feature = get_layer_and_feature(target)
        if target_layer is None or target_feature is None:
            iface.messageBar().pushCritical(
                "Attributformular öffnen", "Layer/Feature nicht gefunden"
            )
            return

        if source:
            logger.debug("Retrieving source layer for feature ID %r", source)
            source_layer, _ = get_layer_and_feature(source)

            if not source_layer:
                logger.warning("No source layer for feature ID %r found", source)
            elif source_layer.isEditable() and not target_layer.isEditable():
                success = target_layer.startEditing()
                if not success:
                    logger.warning(
                        "Failed to activate editing for target layer %r",
                        target_layer.id(),
                    )

        QTimer.singleShot(
            0, lambda: iface.openFeatureForm(target_layer, target_feature)
        )
        logger.debug(
            f"attribute form opened for layer={target_layer.id()}, FID={target_feature.id()}, feature_key= {target}"
        )

    @pyqtSlot("QVariant")
    def select_feature(self, data: dict):
        """Open the QGIS feature form for a specific feature ID."""
        target = data["target"]

        target_layer, target_feature = get_layer_and_feature(target)

        if target_layer is None or target_feature is None:
            iface.messageBar().pushCritical(
                "Feature selektieren", "Layer/Feature nicht gefunden"
            )
            return
        logger.debug(
            f"Found: layer={target_layer.id()}, FID={target_feature.id()} for feature_key= {target}"
        )

        # Perform selection
        iface.setActiveLayer(target_layer)
        target_layer.selectByIds([target_feature.id()])
        logger.debug(f"Feature selected by FID: {target_feature.id()}")

        # Jump to feature BBOX if it has a geometry
        if target_feature.hasGeometry():
            geom = transform_to_project_crs(
                target_feature.geometry(), target_layer.crs()
            )
            iface.mapCanvas().setExtent(geom.boundingBox())
            iface.mapCanvas().refresh()


class PlanToolsHandler(FeatureInteractionHandler):
    featureAdded = pyqtSignal()  # signal for emitEvent handler in the webapp

    def __init__(self, parent=None):
        super().__init__(parent)

    @pyqtSlot("QVariant")
    def migrated_plan(self, plan_data: dict):
        """Handle version migration of a plan.

        Update the appschema version layer property of all plan layers.
        """
        project = QgsProject.instance()
        if not project:
            return
        try:
            plan_version = plan_data["version"]
            plan_id = plan_data["id"]
        except KeyError:
            QgsMessageLog().logMessage(
                "Anwendungsschema-Version von migriertem Plan konnte nicht aktualisiert werden. Bitte Plan neu laden.",
                PLUGIN_NAME,
                Qgis.Warning,
            )
            logger.warning(
                "migrated_plan received invalid entry (missing id/version): %r",
                plan_data,
            )
            return
        updated = False
        for layer in project.mapLayers().values():
            if layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id") == plan_id:
                layer.setCustomProperty(
                    f"{PLUGIN_DIR_NAME}/appschema_version", plan_version
                )
                uri = layer.dataProvider().uri()
                uri.removeParam("version")
                uri.setParam("version", plan_version)
                layer.setDataSource(uri.uri(expandAuthConfig=False))
                layer.setDefaultValueDefinition(
                    layer.fields().indexFromName("version"),
                    QgsDefaultValue(repr(plan_version)),
                )
                updated = True
        if updated:
            msg = f"Anwendungsschema-Version für Layer zu Plan-ID {plan_id!r} aktualisiert"
            iface.messageBar().pushInfo(
                PLUGIN_NAME,
                msg,
            )

    @pyqtSlot("QVariant")
    def add_feature(self, data: dict):
        """Adds a new feature related to an existing feature."""

        def _get_addfeature_layer(
            featuretype: str,
            geometrytype: str,
            parent: str,
            parent_id: str,
            plan_id: str,
        ) -> QgsVectorLayer | None:
            """Find a matching layer to add a new feature base on featuretype regex and geometrytype.

            If no exact match (shared parent) can be determined, a matching layer regarding featuretype and geometrytype
            is returned and the user would have to choose a parent in the attribute form, if applicable.
            """
            candidates = []
            for lyr in QgsProject.instance().mapLayers().values():
                if not lyr.customProperty(f"{PLUGIN_DIR_NAME}/plan_id") == plan_id:
                    continue
                if (
                    re.match(
                        lyr.customProperty(f"{PLUGIN_DIR_NAME}/featuretype_regex"),
                        featuretype,
                    )
                    and lyr.geometryType().name == geometrytype
                ):
                    if (
                        lyr.customProperty(
                            f"{PLUGIN_DIR_NAME}/{'bereich_id' if parent.endswith('Bereich') else 'plan_id'}"
                        )
                        == parent_id
                    ):
                        return lyr
                    else:
                        candidates.append(lyr)
            return candidates[0] if candidates else None

        def _handle_digitizing(digit_feature: QgsFeature = QgsFeature()):
            target_layer.rollBack()
            iface.actionPan().trigger()
            feature = QgsVectorLayerUtils.createFeature(
                target_layer,
                digit_feature.geometry(),
                {
                    target_layer.fields().indexFromName("featuretype"): data[
                        "target_featuretype"
                    ],
                    target_layer.fields().indexFromName("properties"): {
                        rel_inv: [data["origin_id"]]
                        if data["rel_inv_list"]
                        else data["origin_id"],
                    }
                    if (rel_inv := data.get("rel_inv"))
                    else QVariant(),
                },
            )
            dlg = iface.getFeatureForm(target_layer, feature)
            dlg.setMode(QgsAttributeEditorContext.Mode.AddFeatureMode)
            try:
                with edit(target_layer):
                    accepted = dlg.exec()
                # only edit origin feature if relation is not bidirectional
                if (
                    accepted
                    and not rel_inv
                    and not origin_feature["featuretype"].endswith("Plan")
                    # for plan objects, unidirectional relations are handled in the webapp
                ):
                    # TODO make this a single transaction (implement buffered transaction groups)
                    with edit(origin_layer):
                        old_value = origin_layer.dataProvider().get_feature_properties(
                            origin_feature["id"]
                        )
                        new_value = old_value.copy()
                        new_value.setdefault(
                            data["rel"], [] if data["rel_list"] else feature["id"]
                        )
                        if data["rel_list"]:
                            new_value[data["rel"]].append(feature["id"])
                        origin_layer.changeAttributeValue(
                            origin_feature.id(),
                            feature.fieldNameIndex("properties"),
                            new_value,
                            old_value,
                        )
            except QgsEditError as e:
                iface.messageBar().pushCritical("Feature hinzufügen", repr(e))
            else:
                if accepted:
                    target_layer.reload()
                    origin_layer.reload()
                    iface.messageBar().pushSuccess(
                        "Feature hinzufügen", "Feature hinzugefügt"
                    )
                    self.featureAdded.emit()

        def _cleanup(*_):
            try:
                iface.mapCanvas().mapToolSet.disconnect(_cleanup)
                self.digit_tool.deleteLater()
                del self.digit_tool
            except Exception:
                logger.debug("_cleanup error", exc_info=True)

        logger.debug("add_feature triggered with data: %s", data)
        origin_layer, origin_feature = get_layer_and_feature(data["origin_id"])
        if not origin_feature:
            iface.messageBar().pushCritical(
                "Feature hinzufügen", "Ausgangsfeature nicht gefunden"
            )
            logger.error("No origin layer/feature found for data: %s", data)
            return
        target_layer = _get_addfeature_layer(
            data["target_featuretype"],
            data["target_geometrytype"],
            data["origin_featuretype"],
            data["origin_id"],
            origin_layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id"),
        )
        if not target_layer:
            iface.messageBar().pushCritical(
                "Feature hinzufügen",
                f"""kein passender Layer für Objektart '{data["target_featuretype"]}'
                mit Geomtrietyp '{data["target_geometrytype"]}' gefunden""",
            )
            logger.error("No target layer found for data: %s", data)
            return

        if target_layer.isSpatial():
            canvas = iface.mapCanvas()
            self.digit_tool = QgsMapToolDigitizeFeature(canvas, iface.cadDockWidget())
            self.digit_tool.setLayer(target_layer)
            self.digit_tool.digitizingCompleted.connect(_handle_digitizing)
            target_layer.startEditing()
            canvas.setMapTool(self.digit_tool)
            canvas.mapToolSet.connect(_cleanup)
            iface.messageBar().pushInfo("Feature hinzufügen", "Geometrie erfassen")
        else:
            QTimer.singleShot(10, _handle_digitizing)
