# 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

"""Main plugin controller — QGIS plugin lifecycle.

`XMASPlugin` is instantiated by `classFactory` and owns the complete plugin
lifecycle:

- `initGui` — creates the toolbar action, instantiates
  `XMASPluginDockWidget`, starts `ServerManager`, registers the
  processing provider (`XMASPluginProcessingProvider`),
  the custom vector data provider (`XMASPluginVectorDataProvider`), the
  editor widget factory, and all layer context-menu actions.
- `unload` — tears down every component registered in `initGui` in reverse
  order so that QGIS can reload or remove the plugin cleanly.

All QGIS signal/slot connections established here are disconnected in
`unload` to prevent memory leaks across reload cycles.

Also instantiates plugin wide logging, including faulthandler, under `<temp dir>/xmas_log`.
"""

import faulthandler
import logging
from pathlib import Path

import qgis.utils
from qgis import processing
from qgis.core import (
    Qgis,
    QgsApplication,
    QgsLayerTreeGroup,
    QgsLayerTreeLayer,
    QgsMapLayer,
    QgsMessageLog,
    QgsProcessingException,
    QgsProject,
    QgsSettings,
    QgsVectorLayer,
)
from qgis.gui import QgisInterface, QgsGui, QgsLayerTreeViewIndicator
from qgis.PyQt.QtCore import (
    Qt,
    QThreadPool,
    QtMsgType,
    QUrl,
    qInstallMessageHandler,
)
from qgis.PyQt.QtGui import QDesktopServices, QIcon
from qgis.PyQt.QtWidgets import QAction, QMessageBox
from qgis.utils import iface

from xmas_plugin.util.metadata import PLUGIN_DIR_NAME, PLUGIN_NAME

# wrap plugin module imports in try-except block to anticipate unsatisfied dependencies
import_error = None
try:
    from xmas_plugin.actions import (
        AddPO,
        AddSection,
        CopyGeometry,
        ForceCCW,
        ReverseLine,
        SnapGeometry,
    )
    from xmas_plugin.dock import XMASPluginDockWidget
    from xmas_plugin.plan_manager import PlanManager
    from xmas_plugin.processing.alg_split_plan import SplitPlanByBoundaryAlgorithm
    from xmas_plugin.processing.provider import XMASPluginProcessingProvider
    from xmas_plugin.provider.vector import XMASPluginVectorDataProvider
    from xmas_plugin.server_manager import ServerManager
    from xmas_plugin.settings_manager import (
        load_setting,
        save_setting,
        show_settings_dialog,
    )
    from xmas_plugin.util.helpers import (
        get_log_dir,
        get_plugin_root,
    )
    from xmas_plugin.util.layer_validator import (
        ValidationIssue,
        validate_custom_properties,
    )
    from xmas_plugin.util.webengine import (
        cleanup_all_webengine_views,
        destroy_all_webengine_views,
    )
    from xmas_plugin.widgets.editor import XMASPluginEditorFactory
except ImportError as e:
    import_error = str(e)


class XMASPlugin:
    """QGIS Plugin Implementation."""

    def __init__(self, iface: QgisInterface, deps_satisfied: bool) -> None:
        """Constructor.

        Args:
            iface: Reference to QgisInterface instance.
            deps_satisfied: Result of the dependency check in classFactory method. If False, the plugin won't work and is unloaded.
        """
        # Save references to params
        self.db_connection = None
        self.webapp_url = None
        self.dockwidget = None
        self.plan_manager = None
        self.server_manager = None
        self.iface = iface
        self.deps_satisfied = deps_satisfied
        # Cancel plugin initialization
        if not deps_satisfied or import_error:
            return
        self.plugin_dir = get_plugin_root()
        self.svg_path = str(Path(self.plugin_dir) / "resources/styles/svg")
        if self.svg_path not in (svg_paths := QgsApplication.svgPaths()):
            svg_paths.append(self.svg_path)
            QgsApplication.setSvgPaths(svg_paths)

        # Initialize logger
        self.logger = None
        try:
            self._init_logger()
        except Exception as e:
            raise RuntimeError("Failed to init logger:", e)

        # Initialize Qt resources from file resources.py
        try:
            import xmas_plugin.resources_rc  # noqa: F401
        except ModuleNotFoundError:
            self.logger.info("Couldn't import resources.")

        # Provider to register
        XMASPluginVectorDataProvider.register_provider()
        XMASPluginEditorFactory.register()
        self._provider = None

        p = QThreadPool.globalInstance()
        self.logger.info(
            "QThreadPool-snapshot  |  max=%s  active=%s",
            p.maxThreadCount(),
            p.activeThreadCount(),
        )

        # Indicators for invalid/preexisting xplan-layers
        self.tree_view = iface.layerTreeView()
        self.indicator_invalid_xplan_layer = QgsLayerTreeViewIndicator(self.tree_view)
        self.indicator_invalid_xplan_layer.setObjectName(
            f"{PLUGIN_DIR_NAME}/indicator/layer"
        )
        self.icon_xplan_layer = QIcon(":/plugins/xmas_plugin/resources/icons/x_red.ico")
        self.indicator_invalid_xplan_layer.setIcon(self.icon_xplan_layer)
        self.indicator_invalid_xplan_layer.setToolTip(f"Invalider {PLUGIN_NAME} Layer")

        self.indicator_invalid_xplan_group = QgsLayerTreeViewIndicator(self.tree_view)
        self.indicator_invalid_xplan_group.setObjectName(
            f"{PLUGIN_DIR_NAME}/indicator/group"
        )
        self.icon_xplan_group = QIcon(
            ":/plugins/xmas_plugin/resources/icons/x_orange.ico"
        )
        self.indicator_invalid_xplan_group.setIcon(self.icon_xplan_group)
        self.indicator_invalid_xplan_group.setToolTip(
            f"Gruppe enthält invalide {PLUGIN_NAME} Layer"
        )

    def initGui(self) -> None:
        """Create the menu entries and toolbar icons inside the QGIS GUI."""
        # Cancel plugin initialization
        if not self.deps_satisfied or import_error:
            iface.messageBar().pushCritical(
                PLUGIN_NAME,
                "Initialisierung auf Grund fehlender Abhängigkeiten abgebrochen.",
            )
            if import_error:
                QgsMessageLog.logMessage(
                    message=f"Import gescheitert: {import_error}",
                    tag=PLUGIN_NAME,
                    level=Qgis.MessageLevel.Critical,
                )
            return
        self.logger.info("Initialized GUI.")

        # set auto-open feature form on first start
        if load_setting("dock_visible") is None:
            QgsSettings().setValue("map/identifyAutoFeatureForm", True)

        self.dock_action = QAction(
            QIcon(":/plugins/xmas_plugin/resources/icons/xmas_plugin.svg"),
            f"{PLUGIN_NAME} öffnen",
            self.iface.mainWindow(),
        )
        self.iface.addToolBarIcon(self.dock_action)
        self.iface.addPluginToMenu(f"&{PLUGIN_NAME}", self.dock_action)
        self.dock_action.triggered.connect(self.run)

        # add help action
        self.help_action = QAction(
            QIcon(":images/themes/default/mActionHelpContents.svg"),
            PLUGIN_NAME,
            self.iface.mainWindow(),
        )
        self.iface.pluginHelpMenu().addAction(self.help_action)
        self.help_action.triggered.connect(self.show_manual)

        # add map actions
        self.map_actions = [
            AddSection(self.iface.mainWindow()),
            AddPO(self.iface.mainWindow()),
            ForceCCW(self.iface.mainWindow()),
            SnapGeometry(self.iface.mainWindow()),
            ReverseLine(self.iface.mainWindow()),
            CopyGeometry(self.iface.mainWindow()),
        ]
        for map_action in self.map_actions:
            QgsGui.mapLayerActionRegistry().addMapLayerAction(map_action)

        # has the dockwidget been visible the last time qgis was used and should be restored?
        if load_setting("dock_visible", "false") == "true":
            self.run()

        # Register the split plan processing provider
        self._provider = XMASPluginProcessingProvider()
        QgsApplication.instance().processingRegistry().addProvider(self._provider)

        project = QgsProject.instance()

        # Connect to project home path changes
        project.homePathChanged.connect(self.init_project)

        # check opened projects for existing xplan layers
        project.readProject.connect(self.validate_existing_layers)

        # remove all TOC Xplan indicators when project is about to be closed
        project.removeAll.connect(self.remove_indicators)
        project.cleared.connect(self.remove_indicators)

    def show_manual(self):
        """Open the online help."""
        QDesktopServices.openUrl(
            QUrl.fromLocalFile(
                str(Path(self.plugin_dir) / "resources/manual/index.html")
            )
        )

    def unload(self) -> None:
        """Removes the plugin menu item and icon from QGIS GUI."""
        # early return since plugin wasn't initialized if dependencies were not satisfied
        if not self.deps_satisfied:
            return

        self.logger.info("Unloading plugin dock.")

        # remove svg path
        if self.svg_path in (svg_paths := QgsApplication.svgPaths()):
            svg_paths.remove(self.svg_path)
            QgsApplication.setSvgPaths(svg_paths)

        for map_action in self.map_actions:
            QgsGui.mapLayerActionRegistry().removeMapLayerAction(map_action)

        if self.logger.handlers:
            for handler in self.logger.handlers:
                self.logger.removeHandler(handler)
                handler.close()

        if getattr(self, "dock_action", None):
            self.iface.removeToolBarIcon(self.dock_action)
            self.iface.removePluginMenu(f"&{PLUGIN_NAME}", self.dock_action)
            del self.dock_action

        if getattr(self, "help_action", None):
            self.iface.pluginHelpMenu().removeAction(self.help_action)
            del self.help_action

        # Disconnect signals
        try:
            QgsProject.instance().homePathChanged.disconnect(self.init_project)
            # clean up QWebEngineViews correctly
            cleanup_all_webengine_views(self.dockwidget)
        except Exception:
            pass
        # QgsProject.instance().readProject.disconnect(self.restoreDockIfNeeded)

        # Remove Layer/Group Indicators
        QgsProject.instance().readProject.disconnect(self.validate_existing_layers)
        QgsProject.instance().removeAll.disconnect(self.remove_indicators)
        QgsProject.instance().cleared.disconnect(self.remove_indicators)
        self.remove_indicators()

        # Remove the Toolbar
        if getattr(self, "toolbar", None):
            self.iface.mainWindow().removeToolBar(self.toolbar)
            self.toolbar.deleteLater()
            del self.toolbar

        # Remove ServerManager
        if getattr(self, "server_manager", None):
            self.server_manager.deleteLater()
            del self.server_manager

        # Persist final visibility state just before dock tear down
        if getattr(self, "dockwidget", None):
            vis = self.dockwidget.isVisible()
            save_setting("dock_visible", "true" if vis else "false")

            try:
                self.dockwidget.visibilityChanged.disconnect()
            except Exception:
                pass

        # Remove DockWidget
        if getattr(self, "dockwidget", None):
            destroy_all_webengine_views(self.dockwidget)
            self.iface.removeDockWidget(self.dockwidget)
            # self.dockwidget.close()
            self.dockwidget.deleteLater()
            del self.dockwidget

        if self._provider:
            QgsApplication.instance().processingRegistry().removeProvider(
                self._provider
            )
            self._provider = None

    # --------------------------------------------------------------------------

    def run_split_algorithm_dialog(self):
        # check if processing plugin is active
        if "processing" not in qgis.utils.active_plugins:
            # short message in messageBar
            iface.messageBar().pushMessage(
                title=PLUGIN_NAME,
                text="Processing-Plugin ist nicht aktiv!",
                level=Qgis.Critical,
            )
            # log message with guidance how to activate the proecssing-plugin
            QgsMessageLog.logMessage(
                message="Das Processing-Plugin ist nicht aktiv. "
                "Bitte aktivieren Sie es unter "
                '"Erweiterungen → Erweiterungen verwalten und installieren '
                '→ Installiert → Processing"',
                tag=PLUGIN_NAME,
                level=Qgis.MessageLevel.Critical,
            )
            # show MessageLog Widget
            iface.openMessageLog(tabName=PLUGIN_NAME)
            return

        provider_id = self._provider.id()  # e.g. "xplan"
        alg_id = f"{provider_id}:{SplitPlanByBoundaryAlgorithm().name()}"  # "xplan:split_plan_by_boundary"

        cut_layer = self.iface.activeLayer()
        init_params = {}

        # Pre-fill CUT_LAYER if a polygon layer is active
        if (
            isinstance(cut_layer, QgsVectorLayer)
            and cut_layer.geometryType() == Qgis.GeometryType.Polygon
        ):
            init_params[SplitPlanByBoundaryAlgorithm.CUT_LAYER] = cut_layer

        init_params[SplitPlanByBoundaryAlgorithm.USE_SELECTION] = True

        # Pre-fill PLAN_GROUP (best effort): choose the top-level group containing the active layer
        try:
            root = QgsProject.instance().layerTreeRoot()
            group_names = [
                g.name() for g in root.children() if g.nodeType() == g.NodeGroup
            ]
            plan_group_idx = None
            if cut_layer:
                node = root.findLayer(cut_layer.id())
                while node and node.parent() and node.parent() != root:
                    node = node.parent()
                if node and node.parent() == root and node.nodeType() == node.NodeGroup:
                    name = node.name()
                    if name in group_names:
                        plan_group_idx = group_names.index(name)
            if plan_group_idx is not None:
                init_params[SplitPlanByBoundaryAlgorithm.PLAN_GROUP] = plan_group_idx
        except Exception:
            pass

        try:
            processing.execAlgorithmDialog(alg_id, init_params)
            try:
                iface.messageBar().pushSuccess(
                    PLUGIN_NAME, "Plan erfolgreich aufgeteilt."
                )
            except Exception:
                pass
        except QgsProcessingException as e:
            QMessageBox.critical(self.iface.mainWindow(), "Plan aufteilen", f"{e}")

    def run(self) -> None:
        """Run method that loads and starts the plugin."""
        # Load settings on initialization
        if not getattr(self, "webapp_url", None) or not getattr(
            self, "db_connection", None
        ):
            webapp_url = load_setting("webapp_url", "")
            if not webapp_url:
                if not show_settings_dialog(None):
                    return self.iface.messageBar().pushMessage(
                        "Abbruch",
                        "Bitte erst Einstellungen vornehmen",
                        level=Qgis.MessageLevel.Critical,
                        duration=3,
                    )
                webapp_url = load_setting("webapp_url", "")
            self.webapp_url = webapp_url
            self.db_connection = load_setting("db_connection", "")

        # Instantiate server and plan manager; one instance per life cycle
        if not getattr(self, "server_manager", None):
            self.server_manager = ServerManager(self.iface)
        if not getattr(self, "plan_manager", None):
            self.plan_manager = PlanManager()
        if self.server_manager:
            self.logger.debug(f"ServerManager instantiated: {self.server_manager}")

        # Create the dockwidget and keep reference
        if not getattr(
            self,
            "dockwidget",
            None,
        ):
            self.dockwidget = XMASPluginDockWidget(
                iface=self.iface,
                server_manager=self.server_manager,
                plan_manager=self.plan_manager,
            )

            # Show the dockwidget
            # TODO: fix to allow choice of dock location
            self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockwidget)
            self.dockwidget.visibilityChanged.connect(
                lambda vis: save_setting("dock_visible", "true" if vis else "false")
            )

            # split tool (Queued connection not necessary since on same thread, but extra future safe)
            self.dockwidget.runSplitRequested.connect(
                self.run_split_algorithm_dialog, Qt.QueuedConnection
            )

        if not self.dockwidget.isVisible():
            self.dockwidget.show()
            self.dockwidget.raise_()

        if self.dockwidget.isVisible():
            save_setting("dock_visible", "true")

        return None

    def init_project(self) -> None:
        """Initialize project-specific settings."""

    def validate_existing_layers(self) -> None:
        root = QgsProject.instance().layerTreeRoot()
        stack: list[QgsLayerTreeGroup | QgsLayerTreeLayer] = [root]
        needs_attention: list[QgsMapLayer] = []

        while stack:
            node = stack.pop()
            for child in node.children():
                if isinstance(child, QgsLayerTreeGroup):
                    stack.append(child)
                    continue
                if not isinstance(child, QgsLayerTreeLayer):
                    continue

                layer = child.layer()
                try:
                    res = validate_custom_properties(layer)

                    if not res.is_plugin_layer:
                        continue

                    # Check for missing provider params on plugin layers
                    provider = layer.dataProvider()
                    if (
                        provider
                        and provider.name() == PLUGIN_DIR_NAME
                        and not provider._plan_id
                    ):
                        # Try to identify the plan from the layer tree
                        plan_name = layer.name()
                        parent = child.parent()
                        while parent and parent.parent() and parent.parent() != root:
                            parent = parent.parent()
                        if parent and parent != root:
                            plan_name = parent.name()

                        res.issues.append(
                            ValidationIssue(
                                "plan_id",
                                f"Fehlender Provider-Parameter im Plan '{plan_name}'. "
                                f"Plan bitte in einem neuen Projekt erneut laden.",
                                "error",
                            )
                        )

                    if res.ok and not res.issues:
                        continue

                    # everything else needs attention
                    needs_attention.append(layer)
                    self._add_group_chain_indicator(child)
                    self._add_layer_indicator(child, self.indicator_invalid_xplan_layer)
                    issue_text = "\n".join(f"{i.key}: {i.msg}" for i in res.issues)
                    QgsMessageLog.logMessage(
                        f"Layer '{layer.name()}' hat ungültige/fehlende Properties:\n{issue_text}",
                        PLUGIN_NAME,
                        Qgis.MessageLevel.Warning,
                    )
                    layer.setReadOnly()

                except Exception as e:
                    self.logger.exception(f"Layer-Validierung fehlgeschlagen: {e}")

        if needs_attention:
            self.iface.messageBar().pushMessage(
                PLUGIN_NAME,
                "Einige im Projekt bereits vorhandene Layer sind nicht kompatibel oder nicht vom Plugin verwaltet (siehe Log)",
                Qgis.MessageLevel.Warning,
            )

    def _add_layer_indicator(self, layer_node, indicator):
        current = self.iface.layerTreeView().indicators(layer_node)
        if not any(
            i.objectName() == f"{PLUGIN_DIR_NAME}/indicator/layer" for i in current
        ):
            self.iface.layerTreeView().addIndicator(layer_node, indicator)

    def _add_group_chain_indicator(self, layer_node):
        parent = layer_node.parent()
        seen = set()

        while parent is not None:
            if isinstance(parent, QgsLayerTreeGroup) and parent not in seen:
                seen.add(parent)
                current = self.iface.layerTreeView().indicators(parent)
                if not any(
                    i.objectName() == f"{PLUGIN_DIR_NAME}/indicator/group"
                    for i in current
                ):
                    self.iface.layerTreeView().addIndicator(
                        parent, self.indicator_invalid_xplan_group
                    )
            parent = parent.parent()

    def remove_indicators(self) -> None:
        """remove all custom xplan indicators from all layers and groups"""

        if not self.iface.layerTreeView():
            return

        # iterate layer tree
        stack: list[QgsLayerTreeGroup | QgsLayerTreeLayer] = [
            QgsProject.instance().layerTreeRoot()
        ]
        while stack:
            node = stack.pop()
            for child_node in node.children():
                if isinstance(child_node, QgsLayerTreeGroup):
                    stack.append(
                        child_node
                    )  # add group node back to stack for recursion
                child_node_indicators = self.iface.layerTreeView().indicators(
                    child_node
                )
                for _i in child_node_indicators:
                    if _i.objectName() == f"{PLUGIN_NAME}/indicator/group":
                        self.iface.layerTreeView().removeIndicator(
                            child_node, self.indicator_invalid_xplan_group
                        )
                    if _i.objectName() == f"{PLUGIN_NAME}/indicator/layer":
                        self.iface.layerTreeView().removeIndicator(
                            child_node, self.indicator_invalid_xplan_layer
                        )

    def _init_logger(self) -> None:
        """Set up a single logger for the entire plugin at DEBUG level, with tag support."""
        logger = logging.getLogger(PLUGIN_DIR_NAME)
        logger.setLevel(logging.DEBUG)
        logger.propagate = False  # prevent logs from also going to QGIS root logger
        logger.handlers = []  # Remove any old handlers to avoid duplicates

        # Quiet the urllib3/requests spam unless actually needed
        logging.getLogger("urllib3").setLevel(logging.WARNING)
        logging.getLogger("requests").setLevel(logging.WARNING)

        log_file = str(get_log_dir() / f"{PLUGIN_DIR_NAME}.log")
        handler = logging.FileHandler(log_file, mode="w", encoding="utf-8")
        # Use the SafeFormatter here!
        formatter = logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
        )
        handler.setFormatter(formatter)
        logger.addHandler(handler)

        crash_log_file = str(get_log_dir() / f"{PLUGIN_DIR_NAME}_crash.log")
        try:
            faulthandler.enable(open(crash_log_file, "w"))
            logger.info(
                f"faulthandler enabled, writing C++ crashes to {crash_log_file}"
            )
        except Exception as e:
            logger.warning(f"Could not enable faulthandler: {e}")

        def qt_message_handler(mode, context, message):
            if mode in (
                QtMsgType.QtWarningMsg,
                QtMsgType.QtCriticalMsg,
                QtMsgType.QtFatalMsg,
            ):
                # fetch logger lazily in case init order ever changes
                logging.getLogger(PLUGIN_DIR_NAME).error("Qt[%s] %s", mode, message)

        qInstallMessageHandler(qt_message_handler)
        logger.info("Qt message handler installed")

        logger.debug("XPlan plugin logger initialized at DEBUG level.")
        self.logger = logger
