# 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
from functools import partial
from typing import TYPE_CHECKING

from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineView
from qgis.core import (
    QgsMapLayer,
    QgsProject,
    QgsVectorLayer,
)
from qgis.PyQt import sip
from qgis.PyQt.QtCore import QObject, Qt, QUrl
from qgis.PyQt.QtWidgets import QComboBox

from xmas_plugin.settings_manager import get_normalized_url
from xmas_plugin.util.metadata import PLUGIN_DIR_NAME
from xmas_plugin.util.webengine import detach_channel, wire_page_lifecycle
from xmas_plugin.widgets.handler import PlanToolsHandler
from xmas_plugin.widgets.page import XMASPluginPage

if TYPE_CHECKING:
    from xmas_plugin.dock import XMASPluginDockWidget  # Only for IDE/type checker

logger = logging.getLogger(PLUGIN_DIR_NAME)


class PlanTools(QObject):
    """Encapsulates the PlanTools page and wires up signals for QGIS interaction."""

    def __init__(self, parent: "XMASPluginDockWidget") -> None:
        super().__init__(parent)
        self.parent = parent
        self.iface = parent.iface
        self._commit_callbacks: dict[str, object] = {}

        self.comboBox_plans: QComboBox = self.parent.findChild(
            QComboBox, "planToolsComboBox"
        )
        self.web_view: QWebEngineView = self.parent.findChild(
            QWebEngineView, "planToolsView"
        )
        self._base_url = get_normalized_url()
        self._setup_plan_tools()
        # use generic QgsProject signals to track addition/removal of plugin layers
        QgsProject.instance().layersWillBeRemoved.connect(self.on_layers_removed)
        QgsProject.instance().layersAdded.connect(self.on_layers_added)
        self.comboBox_plans.currentIndexChanged.connect(self.on_current_plan_changed)
        # load/clear web_view depending on tab visibility
        self.parent.tabWidget.currentChanged.connect(self.on_tab_open)
        self.parent.tabWidget.tabCloseRequested.connect(self.on_tab_close)
        self._clear_plan_tools()

    def on_tab_open(self, index: int) -> None:
        """Load the web view when planTools tab is opened."""
        if (
            index == self.parent.tabWidget.indexOf(self.parent.planTools)
            and self.web_view.url().toString() == "about:blank"
        ):
            self.on_current_plan_changed(self.comboBox_plans.currentIndex())

    def on_tab_close(self, index: int) -> None:
        """Reset the web view when the planTools tab is closed."""
        if self.parent.tabWidget.indexOf(self.parent.planTools) == index:
            self._clear_plan_tools()

    def on_current_plan_changed(self, index: int) -> None:
        """Load the plan tree when selection has changed an tab is visible."""
        plan_data = self.comboBox_plans.itemData(index)
        # only load view if it's visibible, otherwise defer to on_tab_open
        if plan_data and self.web_view.isVisible():
            self._load_plan_tools(plan_data["plan_id"])

    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:
            layer = QgsProject.instance().mapLayer(layer_id)
            if (
                not layer
                or layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") != "plan"
            ):
                continue
            plan_data = {"plan_id": layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id")}
            data_index = self.comboBox_plans.findData(plan_data)
            if data_index == -1:
                continue

            # disconnect the afterCommitChanges callback before removal
            callback = self._commit_callbacks.pop(layer_id, None)
            if callback:
                try:
                    layer.afterCommitChanges.disconnect(callback)
                except (TypeError, RuntimeError):
                    pass
            if self.comboBox_plans.currentIndex() == data_index:
                self._clear_plan_tools()
            self.comboBox_plans.removeItem(data_index)

    def on_layers_added(self, layers: list[QgsMapLayer]) -> None:
        """Update combobox items when plan layers are added."""
        for layer in layers:
            if layer.customProperty(f"{PLUGIN_DIR_NAME}/layer_type") != "plan":
                continue
            plan_data = {"plan_id": layer.customProperty(f"{PLUGIN_DIR_NAME}/plan_id")}
            try:
                plan_name = next(layer.getFeatures())["properties"]["name"]
                self.comboBox_plans.addItem(plan_name, plan_data)
            except StopIteration:
                self.comboBox_plans.addItem("Neuer Plan", plan_data)
            # connect signal to track and handle plan name changes
            callback = partial(self.on_plan_plan_layer_commit, layer)
            self._commit_callbacks[layer.id()] = callback
            layer.afterCommitChanges.connect(callback)

    def on_plan_plan_layer_commit(self, layer: QgsVectorLayer):
        """Update plan name in combobox on change."""
        try:
            feature = next(layer.getFeatures())
        except StopIteration:
            return
        attribute_map = feature.attributeMap()
        if not isinstance(
            attribute_map.get("properties"), dict
        ) or not attribute_map.get("id"):
            return
        plan_data = {"plan_id": feature["id"]}
        try:
            data_index = self.comboBox_plans.findData(plan_data)
            if data_index >= 0:
                self.comboBox_plans.setItemText(
                    data_index, feature["properties"].get("name", "Neuer Plan")
                )
        except RuntimeError:
            pass

    def _setup_plan_tools(self) -> None:
        """Create/prepare the plan tools QWebEngineView + WebChannel (idempotent)."""
        detach_channel(self.web_view)  # unplug stale channel if any

        # Settings
        self.web_view.settings().setAttribute(
            QWebEngineSettings.ErrorPageEnabled, False
        )

        # Pause health polling during heavier loads (connect once)
        try:
            self.web_view.loadStarted.connect(
                lambda: self.parent._pause_polls(True),
                type=Qt.UniqueConnection,
            )
        except TypeError:
            pass
        try:
            self.web_view.loadFinished.connect(
                lambda _: self.parent._pause_polls(False),
                type=Qt.UniqueConnection,
            )
        except TypeError:
            pass

        # Cleanly unplug channel if the view is destroyed
        try:
            self.web_view.destroyed.connect(
                lambda: detach_channel(self.web_view), type=Qt.UniqueConnection
            )
        except TypeError:
            pass

        # Page
        self.web_view.setPage(XMASPluginPage(self))
        # wire webchannel and handler
        wire_page_lifecycle(self.web_view, self._make_assoc_handler)
        # initial population of combobox
        self.on_layers_added(list(QgsProject.instance().mapLayers().values()))

    def _clear_plan_tools(self):
        """Reset web view to blank url."""
        self.web_view.load(QUrl("about:blank"))

    def _load_plan_tools(self, plan_id: str | None) -> None:
        """Guarded navigation into the plan tools view."""
        from xmas_plugin.dock import ServerState

        if not plan_id:
            return self._clear_plan_tools()
        if self.parent.server_state != ServerState.RUNNING:
            logger.info("PlanTools: skip load (server not RUNNING)")
            return self._clear_plan_tools()

        if not self.web_view or sip.isdeleted(self.web_view):
            logger.warning("PlanTools: view missing/deleted; not loading")
            return

        if not self._base_url:
            self._base_url = get_normalized_url()

        url = QUrl(f"{self._base_url}/plan-tools/{plan_id}")

        logger.info("PlanTools: loading %s", url.toString())
        self.web_view.load(url)

    def _make_assoc_handler(self, parent_channel):
        """Create JS<->Python call handler for the plan tools view."""
        handler = PlanToolsHandler(parent_channel)
        return handler
