# SPDX-FileCopyrightText: 2025 XLeitstelle Planen und Bauen <xleitstelle@gv.hamburg.de>
# SPDX-FileContributor: Anton Jacobsson <anton.jacobsson@init.de>
#
# SPDX-License-Identifier: EUPL-1.2
import logging
from functools import partial
from typing import Callable

from PyQt5.QtCore import QObject, QTimer
from PyQt5.QtWidgets import QComboBox
from qgis.core import QgsProject, QgsVectorLayer

from xmas_plugin.processing.alg_split_plan import SPLIT_ROLE_KEY
from xmas_plugin.util.metadata import PLUGIN_DIR_NAME

logger = logging.getLogger(PLUGIN_DIR_NAME)


class PlanComboboxManager(QObject):
    def __init__(
        self,
        plan_combobox: QComboBox,
        on_selection_cleared: Callable[[], None],
        parent: QObject | None = None,
    ) -> None:
        super().__init__(parent)
        self.plan_combobox = plan_combobox
        self._on_selection_cleared = on_selection_cleared
        self._commit_callbacks: dict[str, object] = {}
        self._plan_refresh_pending = False
        QgsProject.instance().layersAdded.connect(self.on_layers_added)
        QgsProject.instance().layersRemoved.connect(self.on_layers_removed)
        self.schedule_plan_combobox_refresh()

    def on_layers_added(self, layers: list[QgsVectorLayer]) -> None:
        """Update combobox items when plan layers are added."""
        self.schedule_plan_combobox_refresh()

        for layer in layers:
            if layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") != "plan":
                continue

            if layer.customProperty(SPLIT_ROLE_KEY) in ("inner", "outer"):
                continue

            callback = partial(self.on_plan_plan_layer_commit, layer)
            self._commit_callbacks[layer.id()] = callback
            layer.afterCommitChanges.connect(callback)

    def on_layers_removed(self, layer_ids: list[str]) -> None:
        """Update combobox items when plan layers are removed and clear web view if currently selected plan is among them."""

        for layer_id in layer_ids:
            callback = self._commit_callbacks.pop(layer_id, None)
            layer = QgsProject.instance().mapLayer(layer_id)
            if callback and layer:
                try:
                    layer.afterCommitChanges.disconnect(callback)
                except (TypeError, RuntimeError):
                    pass

        current_data = self.plan_combobox.currentData()
        if (
            isinstance(current_data, dict)
            and current_data.get("qgis_layerid") in layer_ids
        ):
            self._on_selection_cleared()

        self.schedule_plan_combobox_refresh()

    def refresh_plan_combobox(self) -> None:
        """Rebuild the plan combobox from current project layers in alphabetical order."""
        current_data = self.plan_combobox.currentData()
        current_layer_id = None
        if isinstance(current_data, dict):
            current_layer_id = current_data.get("qgis_layerid")

        self.plan_combobox.blockSignals(True)
        try:
            self.plan_combobox.clear()

            items: list[tuple[str, str, dict]] = []

            project = QgsProject.instance()
            for layer in project.mapLayers().values():
                if not isinstance(layer, QgsVectorLayer):
                    continue
                if layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") != "plan":
                    continue

                split_role = layer.customProperty(SPLIT_ROLE_KEY)

                try:
                    base_name = next(layer.getFeatures())["properties"]["name"]
                except StopIteration:
                    logger.error("[Plan-ComboBox] Couldn't catch base_name")
                    base_name = "Neuer Plan"
                except Exception:
                    logger.exception(
                        "[Plan-ComboBox] Failed to read plan name from layer %s",
                        layer.id(),
                    )
                    base_name = "Neuer Plan"

                if split_role == "inner":
                    plan_name = f"{base_name} – Innen"
                elif split_role == "outer":
                    plan_name = f"{base_name} – Außen"
                else:
                    plan_name = base_name

                plan_data = {
                    "plan_id": layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id"),
                    "qgis_layerid": layer.id(),
                    "plan_name": plan_name,
                }

                items.append((plan_name.casefold(), plan_name, plan_data))

            items.sort(key=lambda x: x[0])

            restore_index = -1
            for idx, (_, plan_name, plan_data) in enumerate(items):
                self.plan_combobox.addItem(plan_name, plan_data)
                if plan_data.get("qgis_layerid") == current_layer_id:
                    restore_index = idx

            if restore_index >= 0:
                self.plan_combobox.setCurrentIndex(restore_index)
            elif self.plan_combobox.count() == 0:
                self._on_selection_cleared()

        finally:
            self.plan_combobox.blockSignals(False)

    def schedule_plan_combobox_refresh(self) -> None:
        if self._plan_refresh_pending:
            return
        self._plan_refresh_pending = True
        QTimer.singleShot(0, self._run_scheduled_plan_combobox_refresh)

    def _run_scheduled_plan_combobox_refresh(self) -> None:
        self._plan_refresh_pending = False
        self.refresh_plan_combobox()

    def _find_plan_combobox_index_by_layer_id(self, layer_id: str) -> int:
        for i in range(self.plan_combobox.count()):
            data = self.plan_combobox.itemData(i)
            if isinstance(data, dict) and data.get("qgis_layerid") == layer_id:
                return i
        return -1

    def on_plan_plan_layer_commit(self, layer: QgsVectorLayer):
        try:
            feature = next(layer.getFeatures())
        except StopIteration:
            return

        props = feature["properties"]
        if not isinstance(props, dict):
            return

        base_name = props.get("name", "Neuer Plan")
        split_role = layer.customProperty(SPLIT_ROLE_KEY)

        if split_role == "inner":
            plan_name = f"{base_name} – Innen"
        elif split_role == "outer":
            plan_name = f"{base_name} – Außen"
        else:
            plan_name = base_name

        idx = self._find_plan_combobox_index_by_layer_id(layer.id())
        if idx >= 0:
            self.plan_combobox.setItemText(idx, plan_name)
            data = self.plan_combobox.itemData(idx)
            if isinstance(data, dict):
                data["plan_name"] = plan_name
                self.plan_combobox.setItemData(idx, data)

    def cleanup(self) -> None:
        # Disconnect project signals
        try:
            QgsProject.instance().layersAdded.disconnect(self.on_layers_added)
        except (TypeError, RuntimeError):
            pass

        try:
            QgsProject.instance().layersRemoved.disconnect(self.on_layers_removed)
        except (TypeError, RuntimeError):
            pass

        # Disconnect afterCommitChanges for all tracked layers
        for layer_id, callback in list(self._commit_callbacks.items()):
            layer = QgsProject.instance().mapLayer(layer_id)
            if not layer:
                continue
            try:
                layer.afterCommitChanges.disconnect(callback)
            except (TypeError, RuntimeError):
                pass

        self._commit_callbacks.clear()
