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

import json
import logging
import uuid
from pathlib import Path
from typing import Optional

from qgis.core import (
    Qgis,
    QgsDataSourceUri,
    QgsEditError,
    QgsFeature,
    QgsFeatureRequest,
    QgsGeometry,
    QgsLayerTreeGroup,
    QgsMapLayer,
    QgsMessageLog,
    QgsProject,
    QgsRasterLayer,
    QgsRuleBasedRenderer,
    QgsVectorLayer,
    QgsVectorLayerUtils,
    edit,
)
from qgis.gui import (
    QgsAttributeDialog,
    QgsAttributeEditorContext,
    QgsMapToolDigitizeFeature,
)
from qgis.PyQt.QtCore import QObject, Qt, QTimer, QVariant, pyqtSignal, pyqtSlot
from qgis.utils import iface

from xmas_plugin import settings_manager
from xmas_plugin.util.db import get_db_schema, get_db_uri
from xmas_plugin.util.form_config import configure_layer_form
from xmas_plugin.util.geom import transform_to_crs
from xmas_plugin.util.helpers import get_plugin_root
from xmas_plugin.util.metadata import PLUGIN_DIR_NAME, PLUGIN_NAME, PLUGIN_VERSION
from xmas_plugin.util.webengine import _is_valid_qt

logger = logging.getLogger(PLUGIN_DIR_NAME)


class PlanManager(QObject):
    """Class to manage plan layers in QGIS.

    Loads plan layers based on configs specified in the layers.toml file. Each layer
    may define:
     - A geometry type (e.g., "MultiSurface", "MultiCurve", "MultiPoint")
     - An optional custom SQL filter
     - A QML style file path
    We then add these layers to QGIS with setParam('type', <geometrytype>) and setDataSource(...) filters.
    """

    planCreated = pyqtSignal(QVariant)

    def __init__(
        self,
        parent: QObject | None = None,
    ) -> None:
        """Initializes the PlanManager."""
        super().__init__(parent)

        self.connection_type = settings_manager.get_connection_type()
        self.connection_name = settings_manager.load_db_connection_name()
        self.config = settings_manager.load_layer_config()
        self.renderer_cache: dict[str, QgsRuleBasedRenderer] = {}
        self._preload_renderer_cache()

        # placeholders for create_plan method
        self.plan_tree = None
        self.digit_tool = None

    @staticmethod
    def on_plans_deleted(plans_data: list[dict]):
        """Handle deletion of a plan from the DB.

        Remove respective plan groups/layers from the layer tree.
        """
        for plan_data in plans_data:
            try:
                plan_name = plan_data["name"]
                plan_id = plan_data["id"]
            except KeyError:
                QgsMessageLog().logMessage(
                    "Ein gelöschter Plan konnte nicht automatisch aus dem Layerbaum entfernt werden. Bitte manuell den Layerbaum aktualisieren bzw. lade das Projekt neu.",
                    PLUGIN_NAME,
                    Qgis.Warning,
                )
                logger.warning(
                    "deleted_plans received invalid entry (missing id/name): %r",
                    plan_data,
                )
                continue
            project = QgsProject.instance()
            root = project.layerTreeRoot()
            for group in root.findGroups(recursive=True):
                if (
                    group.name() == plan_name
                    and group.customProperty(f"{PLUGIN_DIR_NAME}/plan_id") == plan_id
                ):
                    group.parent().removeChildNode(group)
                    iface.messageBar().pushInfo(
                        PLUGIN_NAME,
                        f"Plan '{plan_name}' wurde aus dem Layerbaum entfernt",
                    )
                    break

    def load_plan(
        self,
        plan_name: str,
        plan_id: str,
        plan_type: str,
        appschema: str,
        version: str,
        bereiche: Optional[list[dict]] = None,
        images: Optional[list[str]] = None,
    ) -> None:
        """Loads a plan from DB and adds it to the QGIS project.

        The layer tree is build from the layers configuration TOML with a QgsTask
        to prevent blocking the GUI. A layer group named after the plan is at the top level;
        if it already exists, the first section of the plan's UUID is appended to it's name.

        Effects:
            - Retrieves and sets the connection type from settings.
            - Builds the layer tree for the specified plan.
            - Shows a success message in QGIS’s message bar.

        Args:
            plan_name: The display name of the plan.
            plan_id: Unique identifier for the plan, used to filter features.
            plan_type: The plan's featuretype.
            appschema: The plan's application schema, e.g. 'xplan'.
            version: The version of the application schema.
            bereiche: Information on plan sections to build groups from, if existing.
            images: Optional list of raster image document names associated with the plan.
        """
        project = QgsProject.instance()
        root = project.layerTreeRoot()
        if plan_group := root.findGroup(plan_name):
            if plan_group.customProperty(f"{PLUGIN_DIR_NAME}/plan_id", plan_id):
                iface.messageBar().pushWarning(
                    PLUGIN_NAME,
                    f"{plan_type} '{plan_name}' ist bereits im Layerbaum vorhanden.",
                )
                return
            else:
                plan_name = f"{plan_name} {plan_id[:8]}"
        self.connection_type = settings_manager.get_connection_type()
        plan_group = QgsLayerTreeGroup("placeholder")
        plan_layers = []
        try:
            self.build_plan_group(
                group=plan_group,
                layers=plan_layers,
                plan_name=plan_name,
                plan_id=plan_id,
                appschema=appschema,
                version=version,
                bereiche=bereiche,
            )
            self._add_raster_layers(
                plan_id=plan_id,
                plan_name=plan_name,
                plan_group=plan_group,
                plan_layers=plan_layers,
                images=images,
            )
            project.layerStore().addMapLayers(plan_layers)
            root.insertChildNode(0, plan_group.findGroup(plan_name).clone())
        except Exception as e:
            logger.exception("Plan %s could not be loaded: %s", plan_name, str(e))
            QgsMessageLog().logMessage(
                f"Fehler beim Laden von Plan '{plan_name}': {str(e)}",
                PLUGIN_NAME,
                Qgis.Critical,
            )
            iface.messageBar().pushCritical(
                PLUGIN_NAME, f"Fehler beim Laden von Plan '{plan_name}'"
            )

    def create_plan(self, featuretype: str) -> None:
        """Create a new plan.

        Create a new plan layer of the given type, register it with the project,
        and either use pre-selected geometries or launch the digitizing tool so the user can draw the plan feature.

        Args:
            featuretype: The featuretype selected for the new plan.
        """

        def handle_cancellation(
            plan_tree: QgsLayerTreeGroup, plan_layer: QgsVectorLayer
        ) -> None:
            """Cleanup on cancellation.

            Roll back any uncommitted edits on the plan layer and remove
            the plan's layer-tree node from the current QGIS project.
            """
            if _is_valid_qt(plan_layer) and plan_layer.isEditable():
                plan_layer.rollBack()

            if _is_valid_qt(plan_tree):
                QgsProject.instance().layerTreeRoot().removeChildNode(plan_tree)
            iface.actionPan().trigger()
            if self.digit_tool:
                self.digit_tool.deleteLater()
                self.digit_tool = None

        @pyqtSlot(QgsLayerTreeGroup, QgsVectorLayer, QgsFeature, str, str)
        def handle_digitizing(
            plan_tree: QgsLayerTreeGroup,
            plan_layer: QgsVectorLayer,
            digit_feature: QgsFeature,
            plan_type: str,
            plan_id: str,
        ) -> None:
            """Handle completion of feature digitization on the plan layer.

            This function is called when the user finishes digitizing a new
            polygon on the plan layer. It assigns the generated plan ID and
            type to the feature, opens the feature form for attribute input,
            and if the form is accepted, adds the feature to the layer’s
            edit buffer. If the form is canceled, any partial edits are
            rolled back via handle_cancelation().
            """
            if plan_layer.isEditable():
                plan_layer.rollBack()
            feature = QgsVectorLayerUtils.createFeature(
                plan_layer,
                digit_feature.geometry(),
                {
                    plan_layer.fields().indexFromName("featuretype"): plan_type,
                    plan_layer.fields().indexFromName("id"): plan_id,
                },
            )
            dlg: QgsAttributeDialog = iface.getFeatureForm(plan_layer, feature)
            dlg.setMode(QgsAttributeEditorContext.Mode.AddFeatureMode)
            try:
                accepted = False
                with edit(plan_layer):
                    accepted = dlg.exec()
            except QgsEditError as e:
                iface.messageBar().pushCritical("Neuer Plan", repr(e))
            else:
                if not accepted:
                    handle_cancellation(plan_tree, plan_layer)
                else:
                    try:
                        plan_feature = dlg.attributeForm().currentFormFeature()
                        plan_name = plan_feature["properties"]["name"]
                        plan_tree.setName(plan_name)
                        iface.messageBar().pushSuccess(
                            "Neuer Plan", f"Plan {repr(plan_name)} wurde angelegt"
                        )
                        self.planCreated.emit(plan_feature["id"])
                    except Exception:
                        pass
                    try:
                        dlg.deleteLater()
                        dlg = None
                    except (NameError, AttributeError):
                        pass
                    iface.actionPan().trigger()
                    if self.digit_tool:
                        self.digit_tool.deleteLater()
                        self.digit_tool = None

        def digitize_geometry(
            plan_tree: QgsLayerTreeGroup,
            plan_layer: QgsVectorLayer,
            plan_type: str,
            plan_id: str,
        ):
            """Digitize a new geometry to create the plan object from."""
            canvas = iface.mapCanvas()
            self.digit_tool = QgsMapToolDigitizeFeature(canvas, iface.cadDockWidget())
            digit_tool = self.digit_tool
            digit_tool.setLayer(plan_layer)
            try:
                digit_tool.digitizingCompleted.connect(
                    lambda digit_feature: (
                        digit_tool.digitizingFinished.disconnect(),
                        handle_digitizing(
                            plan_tree, plan_layer, digit_feature, plan_type, plan_id
                        ),
                    ),
                    type=Qt.UniqueConnection,
                )
            except TypeError:
                pass
            try:
                digit_tool.digitizingCanceled.connect(
                    lambda: handle_cancellation(plan_tree, plan_layer),
                    type=Qt.UniqueConnection,
                )
            except TypeError:
                pass
            try:
                digit_tool.digitizingFinished.connect(
                    lambda: (
                        digit_tool.digitizingFinished.disconnect(),
                        handle_cancellation(plan_tree, plan_layer),
                    ),
                    type=Qt.UniqueConnection,
                )
            except TypeError:
                pass
            plan_layer.startEditing()
            canvas.setMapTool(digit_tool)
            iface.messageBar().pushInfo(
                "Neuer Plan", "jetzt räumlichen Geltungsbereich erfassen"
            )

        appschema, version = settings_manager.get_appschema()
        root = QgsProject.instance().layerTreeRoot()
        plan_id = str(uuid.uuid4())
        plan_name = "Neuer Plan"
        plan_group = QgsLayerTreeGroup("placeholder")
        plan_layers = []
        self.build_plan_group(
            group=plan_group,
            layers=plan_layers,
            plan_name=plan_name,
            plan_id=plan_id,
            appschema=appschema,
            version=version,
            bereiche=[],
        )
        plan_tree = plan_group.findGroup(plan_name).clone()
        root.insertChildNode(0, plan_tree)
        plan_layer = None
        for map_layer in plan_layers:
            QgsProject.instance().addMapLayer(map_layer, False)
            if map_layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") == "plan":
                plan_layer = map_layer
        existing_feature = None
        # collect geometries from previously selected features (e.g. parcels) as plan geometry
        select_layer = iface.activeLayer()
        if (
            isinstance(select_layer, QgsVectorLayer)
            and select_layer.geometryType() == Qgis.GeometryType.Polygon
        ):
            selected_ids = select_layer.selectedFeatureIds()
            request = QgsFeatureRequest(selected_ids).setNoAttributes()
            geometries = [f.geometry() for f in select_layer.getFeatures(request)]
            if geometries:
                union_geom = QgsGeometry.unaryUnion(geometries).coerceToType(
                    plan_layer.wkbType()
                )
                if len(union_geom) == 1:
                    geom = transform_to_crs(
                        union_geom[0], select_layer.crs(), plan_layer.crs()
                    )
                    existing_feature = QgsFeature()
                    existing_feature.setGeometry(geom)
        if existing_feature:
            QTimer.singleShot(
                0,
                lambda: handle_digitizing(
                    plan_tree, plan_layer, existing_feature, featuretype, plan_id
                ),
            )
        else:
            digitize_geometry(plan_tree, plan_layer, featuretype, plan_id)

    def build_plan_group(
        self,
        group: QgsLayerTreeGroup,
        layers: list[QgsVectorLayer],
        plan_name: str,
        plan_id: str,
        appschema: str,
        version: str,
        bereiche: Optional[list[dict]],
    ) -> None:
        """Create and configure a plan group under the given layer tree group.

        Args:
            group: Parent `QgsLayerTreeGroup` to attach the plan group..
            layers: The layer list to append the plan layer to.
            plan_name: Display name for the new plan group.
            plan_id: Unique identifier of the plan.
            appschema: Schema name (e.g. "xplan") to determine content structure.
            version: Schema version to load appropriate layers/styles.
            bereiche: List of plan sections (only used when appschema == "xplan").
        """
        plan_group = group.addGroup(plan_name)
        plan_group.setExpanded(True)
        plan_group.setCustomProperty(f"{PLUGIN_DIR_NAME}/plan_id", plan_id)
        logger.info(f"Created new plan group '{plan_name}' with UUID {plan_id}.")
        layers_config = self.config.get(appschema).get("layers", [])
        for layer_def in layers_config:
            if layer_def["type"] not in ["plan", "text"]:
                continue
            geometry_type_key = layer_def.get("geometry", "nogeom")
            style_file = layer_def.get("style", "")
            featuretype_regex = layer_def.get("featuretype_regex", "")
            property_filter = layer_def.get("properties", {})
            layer_name = layer_def.get("name", "Unnamed Layer")
            self._create_and_add_layer(
                layer_list=layers,
                group_node=plan_group,
                layer_name=layer_name,
                layer_type=layer_def["type"],
                style_filename=style_file,
                geometry_type=geometry_type_key,
                plan_id=plan_id,
                parent_id=plan_id,
                featuretype_regex=featuretype_regex,
                property_filter=property_filter,
                appschema=appschema,
                version=version,
            )
        if appschema in ["xplan", "xwp"]:
            for bereich in bereiche:
                self.build_section_group(
                    bereich=bereich,
                    group=plan_group,
                    layers=layers,
                    plan_id=plan_id,
                    appschema=appschema,
                    version=version,
                )
        else:
            self.build_content_group(
                group=plan_group,
                layers=layers,
                types=["subject", "presentation"],
                plan_id=plan_id,
                parent_id=plan_id,
                appschema=appschema,
                version=version,
            )

    def build_section_group(
        self,
        bereich: dict,
        group: QgsLayerTreeGroup,
        layers: list[QgsVectorLayer],
        plan_id: str,
        appschema: str,
        version: str,
    ) -> None:
        """Build and return a layer tree group for a specific section (Bereich).

        Creates a QgsLayerTreeGroup named "Bereich {nummer}" under the given
        parent group, applies custom properties, adds all configured section
        layers, and optionally populates it with content subgroups.

        Args:
            bereich: dict with keys 'nummer', 'id', and optional 'geometry'.
            group: Parent `QgsLayerTreeGroup` to attach the section group.
            layers: The layer list to append content layers to.
            plan_id: Identifier of the current plan.
            appschema: Application schema name for data sources.
            version: Schema version string.
        """
        section_group = group.addGroup(f"Bereich {bereich['nummer']}")
        section_group.setExpanded(False)
        section_group.setCustomProperty(f"{PLUGIN_DIR_NAME}/plan_id", plan_id)
        section_group.setCustomProperty(f"{PLUGIN_DIR_NAME}/bereich_id", bereich["id"])
        layers_config = self.config.get(appschema).get("layers", [])
        for layer_def in layers_config:
            if not layer_def["type"] == "section":
                continue
            geometry_type_key = (
                layer_def.get("geometry", "nogeom") if bereich["geometry"] else "nogeom"
            )
            style_file = layer_def.get("style", "")
            featuretype_regex = layer_def.get("featuretype_regex", "")
            property_filter = layer_def.get("properties", {})
            layer_name = layer_def.get("name", "Unnamed Layer")
            self._create_and_add_layer(
                layer_list=layers,
                group_node=section_group,
                layer_name=layer_name,
                layer_type="section",
                style_filename=style_file,
                geometry_type=geometry_type_key,
                plan_id=plan_id,
                parent_id=plan_id,
                featuretype_regex=featuretype_regex,
                property_filter=property_filter,
                appschema=appschema,
                version=version,
                section=bereich,
            )

        self.build_content_group(
            group=section_group,
            layers=layers,
            types=["subject", "presentation"],
            plan_id=plan_id,
            parent_id=bereich["id"],
            appschema=appschema,
            version=version,
        )

    def build_content_group(
        self,
        group: QgsLayerTreeGroup,
        layers: list[QgsVectorLayer],
        types: list,
        plan_id: str,
        parent_id: str,
        appschema: str,
        version: str,
    ) -> None:
        """Build and add configured content layers to the given layer tree group.

        Iterates through the configured layers in self.config[<appschema>]["layers"]. For each layer
        whose type is in the provided `types` list, it creates (and optionally groups)
        the layer, applies styles, geometry settings, and property filters, then adds
        it to the specified QGIS group.

        Args:
            group: Parent QGIS layer tree group to populate.
            layers: The layer list to append content layers to.
            types: Layer type identifiers to include, e.g. 'presentation' or 'subject'.
            plan_id: Identifier for the current plan context.
            parent_id: Identifier of the parent element within the plan.
            appschema: Application schema name for building service URLs.
            version: Service version string for constructing URLs.
        """
        layers_config = self.config.get(appschema).get("layers", [])
        for layer_def in layers_config:
            layer_type = layer_def["type"]
            if layer_type not in types:
                continue
            geometry_type_key = layer_def.get("geometry", "nogeom")
            style_file = layer_def.get("style", "")
            featuretype_regex = layer_def.get("featuretype_regex", "")
            property_filter = layer_def.get("properties", {})
            layer_name = layer_def.get("name", "Unnamed Layer")
            group_name = layer_def.get("group", None)
            if group_name:
                if existing := group.findGroup(group_name):
                    group_node = existing
                else:
                    group_node = group.addGroup(group_name)
                    group_node.setExpanded(False)
            else:
                group_node = group
            self._create_and_add_layer(
                layer_list=layers,
                group_node=group_node,
                layer_name=layer_name,
                layer_type=layer_type,
                style_filename=style_file,
                geometry_type=geometry_type_key,
                plan_id=plan_id,
                parent_id=parent_id,
                featuretype_regex=featuretype_regex,
                property_filter=property_filter,
                appschema=appschema,
                version=version,
            )

    def _build_sql_filter(
        self,
        featuretype_regex: str,
        property_filter: dict,
        parent_id: str,
        layer_type: str,
        section: dict | None = None,
    ) -> str:
        """Build an SQL filter expression based on plan or section ID."""
        if layer_type == "plan":
            plan_filter = f"(id = '{parent_id}')"
        elif layer_type == "section":
            section_id = section["id"]
            plan_filter = f"(id = '{section_id}')"
        else:
            refs_table = (
                f"{schema}.refs"
                if (schema := get_db_schema(self.connection_name))
                else "refs"
            )
            plan_filter = f"(id in (select b.related_id from {refs_table} b where b.base_id = '{parent_id}'))"

        filters = []

        if featuretype_regex.strip():
            filters.append(f"(featuretype ~ '{featuretype_regex}')")

        if property_filter:
            for k, v in property_filter.items():
                filters.append(
                    f"(properties['{k}'] = '{str(v).lower() if isinstance(v, bool) else v}')"
                )

        filters.append(plan_filter)

        combined_filter = " AND ".join(filters)
        logger.debug(f"[PlanManager] Combined filter: {combined_filter}")
        return combined_filter

    def _create_and_add_layer(
        self,
        layer_list: list[QgsVectorLayer],
        group_node: QgsLayerTreeGroup,
        layer_name: str,
        layer_type: settings_manager.LAYER_TYPES,
        plan_id: str,
        parent_id: str,
        appschema: str,
        version: str,
        section: dict | None = None,
        style_filename: str | None = None,
        geometry_type: settings_manager.GEOMETRY_TYPES = "nogeom",
        featuretype_regex: str = "",
        property_filter: dict = {},
    ) -> None:
        """Add a layer to the tree.

        Creates a QgsVectorLayer from the given filter + geometry_type,
        adds it to the project group, loads style, etc.

        Args:
            layer_list: A list of plan layers to add the layer to.
            group_node: The QGIS group node to which the layer belongs.
            layer_name: Display name of the layer in QGIS.
            layer_type: The type of the layer, e.g. plan.
            plan_id: The UUID of the plan the layer belongs to.
            parent_id: The UUID of the parent feature of features in the layer, e.g. a section.
            appschema: The shortname of the application schema.
            version: The version of the application schema.
            section: For plan layer, this contains a dictionary with section data to build sub-groups from.
            style_filename: Optional QML style path.
            geometry_type: The basic geometry type of the layer to select the corresponding db view.
            featuretype_regex: A regex string to limit featuretypes in a layer.
            property_filter: A dictionary with properties as keys and allowed values as values to use in a sql filter.
        """
        uri_string = get_db_uri(self.connection_name, False)

        source_filter = self._build_sql_filter(
            featuretype_regex, property_filter, parent_id, layer_type, section
        )

        data_source = QgsDataSourceUri(uri_string)

        data_source.setDataSource(
            get_db_schema(self.connection_name),
            f"coretable_{geometry_type}s",
            "geometry",
            source_filter,
            "pk",
        )

        if geometry_type == "nogeom":
            data_source.setSrid(None)
            data_source.setGeometryColumn(None)

        data_source.setParam("checkPrimaryKeyUnicity", "0")
        data_source.setParam(
            "plan_id", plan_id
        )  # TODO: generally use params instead of custom properties?
        data_source.setParam("appschema", appschema)
        data_source.setParam("version", version)

        final_uri = data_source.uri(expandAuthConfig=False)
        logger.debug(f"[PlanManager] Final URI for '{layer_name}': {final_uri}")

        layer = QgsVectorLayer(final_uri, layer_name, PLUGIN_DIR_NAME)

        if not layer.isValid():
            logger.error(f"[PlanManager] Layer '{layer_name}' is invalid!")
            return iface.messageBar().pushMessage(
                "Layer invalide",
                f"Layer '{layer_name}' konnte nicht hinzugefügt werden",
                Qgis.Critical,
            )

        # Set geometry options
        geom_options = layer.geometryOptions()
        checks = ["QgsIsValidCheck", "QgsGeometryMissingVertexCheck"]
        if property_filter.get("flaechenschluss"):
            checks.extend(["QgsGeometryOverlapCheck", "QgsGeometryGapCheck"])
        geom_options.setGeometryChecks(checks)
        geom_options.setRemoveDuplicateNodes(True)

        layer.setCustomProperty(f"{PLUGIN_DIR_NAME}/plugin_version", PLUGIN_VERSION)
        layer.setCustomProperty(f"{PLUGIN_DIR_NAME}/plan_id", plan_id)
        if plan_id != parent_id:
            layer.setCustomProperty(f"{PLUGIN_DIR_NAME}/bereich_id", parent_id)
        layer.setCustomProperty(f"{PLUGIN_DIR_NAME}/appschema", appschema)
        layer.setCustomProperty(f"{PLUGIN_DIR_NAME}/appschema_version", version)
        if featuretype_regex:
            layer.setCustomProperty(
                f"{PLUGIN_DIR_NAME}/featuretype_regex", featuretype_regex
            )
        if property_filter:
            layer.setCustomProperty(
                f"{PLUGIN_DIR_NAME}/property_filter", json.dumps(property_filter)
            )
        layer.setCustomProperty(f"{PLUGIN_DIR_NAME}/layer_type", layer_type)

        #  refresh layer on DB NOTIFY events
        layer.setRefreshOnNotifyEnabled(True)
        layer.setRefreshOnNofifyMessage(
            f"{PLUGIN_DIR_NAME}/refresh_{layer.geometryType().name.lower()}"
        )

        if geometry_type != "nogeom":
            style_manager = layer.styleManager()

            style_manager.addStyleFromLayer("default")

            for file, name in [
                (style_filename, "Standard"),
                (f"styles/{geometry_type}_edit.qml", "Editieren"),
            ]:
                if not file:
                    continue
                style_path = self._resolve_style_path(file)
                if style_path:
                    try:
                        layer.setRenderer(self.renderer_cache[style_path].clone())
                        configure_layer_form(layer)
                        style_manager.addStyleFromLayer(name)
                        logger.debug(
                            "[PlanManager] added style %r to layer %s",
                            name,
                            layer.id(),
                        )
                    except Exception as e:
                        logger.exception(
                            "[PlanManager] failed to apply style %r to layer %s: %s",
                            style_path,
                            layer,
                            e,
                        )

            if "Standard" in style_manager.styles():
                style_manager.setCurrentStyle("Standard")
                style_manager.removeStyle("default")
            else:
                style_manager.setCurrentStyle("default")
                configure_layer_form(layer)

        tree_layer = group_node.addLayer(layer)
        tree_layer.setCustomProperty("showFeatureCount", True)
        tree_layer.setExpanded(False)
        layer_list.append(layer)
        logger.debug(
            f"[PlanManager] Successfully added layer '{layer_name}' for plan with id '{plan_id}' to group '{group_node.name()}'."
        )

    def _preload_renderer_cache(self) -> None:
        """Pre-populate renderer_cache by loading each unique QML once.

        Creates a temporary in-memory scratch layer of the matching WKB type,
        calls loadNamedStyle once per unique style path, and stores the resulting
        renderer clone in self.renderer_cache so that _create_and_add_layer can
        set renderers without re-parsing QML files.
        """
        _wkb_type_map = {
            "point": "Point",
            "line": "LineString",
            "polygon": "Polygon",
        }

        # Collect unique (style_path, geometry_type) pairs across all schemas
        to_load: list[tuple[str, str]] = []
        seen_paths: set[str] = set()
        for schema_config in self.config.values():
            for layer_def in schema_config.get("layers", []):
                geometry_type = layer_def.get("geometry", "nogeom")
                if geometry_type == "nogeom":
                    continue
                wkb_type = _wkb_type_map.get(geometry_type)
                if not wkb_type:
                    continue
                for style_file in [
                    layer_def.get("style", ""),
                    f"styles/{geometry_type}_edit.qml",
                ]:
                    if not style_file:
                        continue
                    style_path = self._resolve_style_path(style_file)
                    if style_path and style_path not in seen_paths:
                        seen_paths.add(style_path)
                        to_load.append((style_path, wkb_type))

        for style_path, wkb_type in to_load:
            scratch = QgsVectorLayer(wkb_type, "scratch", "memory")
            scratch.loadNamedStyle(style_path, QgsMapLayer.StyleCategory.Symbology)
            self.renderer_cache[style_path] = scratch.renderer().clone()
            logger.debug("[PlanManager] preloaded renderer cache for %s", style_path)

    @staticmethod
    def _resolve_style_path(style_filename: str) -> str | None:
        """Resolve the style path provided in a layer config.

        Checks for existence of the target QML file. Relative paths are resolved
        relative <plugin root>/resources.
        """
        style_path = Path(style_filename)

        if not style_path.is_absolute():
            style_path = Path(get_plugin_root()) / "resources" / style_path

        if not style_path.exists():
            QgsMessageLog().logMessage(
                f"Kein Layerstyle unter '{style_path}' gefunden",
                PLUGIN_NAME,
                Qgis.Warning,
            )
            iface.openMessageLog(tabName=PLUGIN_NAME)
            logger.warning(f"[PlanManager] Style file not found: {style_path}")
            return None

        return str(style_path)

    def _add_raster_layers(
        self,
        plan_id: str,
        plan_name: str,
        plan_group: QgsLayerTreeGroup,
        plan_layers: list[QgsVectorLayer],
        images: list[str],
    ) -> None:
        logger.debug("images related to plan: %s", images)
        if not images:
            return
        base_url = settings_manager.get_normalized_url()
        raster_layers = []
        for image in images:
            layer = QgsRasterLayer(
                f"/vsicurl/{base_url}/plan/{plan_id}/documents/{image}", image
            )
            if layer.isValid():
                raster_layers.append(layer)
            else:
                logger.warning("Image %r is not a valid raster image source", image)
        logger.debug("raster layers created: %s", raster_layers)
        if not raster_layers:
            return
        raster_group = plan_group.findGroup(plan_name).addGroup("Rasterdaten")
        for layer in raster_layers:
            raster_group.addLayer(layer)
            plan_layers.append(layer)
        raster_group.setExpanded(False)
