# -*- coding: utf-8 -*-
"""
/***************************************************************************
 NetworkStorePlugin
                                 A QGIS plugin
 export layers to kisters network store
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2022-01-18
        git sha              : $Format:%H$
        copyright            : (C) 2022 by Attila Bibok
        email                : Attila.bibok@kisters.net
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

__author__ = "Attila Bibok / KISTERS North America"
__date__ = "2025-11-07"
__copyright__ = "(C) 2022 by Attila Bibok / KISTERS North America"

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = "$Format:%H$"

import os
import sys
import sip
import inspect
import json
import re
import shutil
import subprocess
from pathlib import Path
from contextlib import contextmanager
from urllib.parse import urlparse

try:
    from qgis.PyQt.QtWebEngineWidgets import (
        QWebEngineView,
        QWebEngineSettings,
        QWebEngineProfile,
    )

    _JSONEDITOR_WEBENGINE_AVAILABLE = True
except Exception:  # noqa: BLE001
    QWebEngineView = None  # type: ignore
    QWebEngineSettings = None  # type: ignore
    QWebEngineProfile = None  # type: ignore
    _JSONEDITOR_WEBENGINE_AVAILABLE = False

# Qt WebChannel (kept via qgis.PyQt for Qt5/Qt6 compatibility)
try:
    from qgis.PyQt.QtWebChannel import QWebChannel
except Exception:  # noqa: BLE001
    try:
        # Fallback if the qgis shim was built without QtWebChannel
        from PyQt5.QtWebChannel import QWebChannel  # type: ignore
    except Exception:  # noqa: BLE001
        QWebChannel = None  # type: ignore
        _JSONEDITOR_WEBENGINE_AVAILABLE = False

from qgis.PyQt.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply


# from PyQt5.QtWebEngineWidgets import QWebEngineSettings
from qgis.core import (
    QgsApplication,
    QgsFeatureRequest,
    Qgis,
    QgsMessageLog,
    QgsProject,
    QgsVectorLayer,
    QgsLayerTreeGroup,
    QgsLayerTreeLayer,
    QgsRasterLayer,
    QgsMapLayer,
)
from .kisters_processing_provider import kisters_processingProvider
from .utils import (
    load_icons,
    clean_icons,
)
from .network_store_dialog_dock_jsoneditor import (
    feature_to_json,
    derive_app_name,
)  # reuse your helpers


from qgis.PyQt.QtCore import (
    QSettings,
    QTranslator,
    QCoreApplication,
    Qt,
    QUrl,
    QEventLoop,
    QTimer,
)
from qgis.PyQt.QtGui import QIcon, QPalette, QColor
from qgis.PyQt.QtWidgets import (
    QAction,
    QApplication,
    QFileDialog,
    QInputDialog,
    QLineEdit,
    QMenu,
    QMessageBox,
)

# Initialize Qt resources from file resources.py
from .resources import *  # noqa

# Import the code for the dialog
from .network_store_dialog import NetworkStorePluginDialog
from .network_store_dialog_export import NetworkStorePluginDialogExport
from .network_store_dialog_post import NetworkStorePluginDialogPost
from .network_store_core import (
    list_style_sets,
    style_set_dir,
    apply_style_set_to_network,
)
from .network_store_dialog_get import NetworkStorePluginDialogGet
from .network_store_dialog_delete import NetworkStorePluginDialogDelete
from .network_store_dialog_dock_jsoneditor import NetworkStoreDockableJsonEditor
from .network_store_dialog_styles import NetworkStoreStyleManagerDialog
from .arraystorage_dialog_load_raster import ArrayStorageDialogLoadRaster
from .arraystorage_dialog_list_timeseries import ArrayStorageDialogListTimeseries
from .arraystorage_dialog_delete import ArrayStorageDialogDelete
from .datasphere_dialog_load_raster_bulk import DatasphereDialogLoadRasterBulk
from .datasphere_dialog_load_locations import DatasphereDialogLoadLocations
from .arraystore_classifications import raster_style_to_classification

import os.path

from .jsoneditor_bridge import JSONEditorBridge

# Clear cache before loading the page (only if WebEngine is available)
if _JSONEDITOR_WEBENGINE_AVAILABLE:
    try:
        QWebEngineProfile.defaultProfile().clearHttpCache()
    except Exception:
        pass

cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0]
pluginPath = os.path.dirname(__file__)

if cmd_folder not in sys.path:
    sys.path.insert(0, cmd_folder)


class ArraystoreDialogLoad:
    pass


class ArraystoreDialogDelete:
    pass


class ArraystoreDialogSettings:
    pass


class ArraystoreTableDock:
    pass


class NetworkStorePlugin:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # copy icons to their location

        # copy the .SVGs to profiles/$profile/svg
        load_icons()
        # initialize locale
        locale = QSettings().value("locale/userLocale")[0:2]
        locale_path = os.path.join(
            self.plugin_dir, "i18n", "NetworkStorePlugin_{}.qm".format(locale)
        )

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr("&Network Store Plugin")
        self.toolbar_networkstore = None
        self.toolbar_arraystore = None
        self.toolbar_datasphere = None
        self.action_network_store_service = None
        self.dlg = None
        self.dlg_networkstore_add = None
        self.dlg_networkstore_load = None
        self.dlg_networkstore_delete = None
        self.dlg_networkstore_styles = None
        self.dlg_datasphere_locations = None
        self.dlg_datasphere_bulk_load = None
        self.dlg_arraystore_load = None
        self.dlg_arraystore_delete = None
        self.dlg_arraystore_list = None

        # Dockable json editor
        self.dock_widget = None
        self._jsoneditor_available = _JSONEDITOR_WEBENGINE_AVAILABLE

        # Check if plugin was started the first time in current QGIS session
        # Must be set in initGui() to survive plugin reloads
        self.first_start = None
        self._last_hooked_layer = None

        # processing provider
        self.provider = None
        self._layer_tree_menu_hooked = False

    # ------------------------------------------------------------------ #
    # Local network-store service control
    # ------------------------------------------------------------------ #

    def _resolve_network_store_cli(self) -> list[str] | None:
        """
        Return the command prefix to run network-store, or None if not found.

        Priority (distrobox-safe):
        1) host exec (preferred)
        2) container PATH
        3) pyenv shim (last resort)
        """
        # 1) Call host command from inside distrobox
        host_exec = shutil.which("distrobox-host-exec")
        if host_exec:
            host_shim = os.path.expanduser("~/.pyenv/shims/network-store")
            if os.path.exists(host_shim) and os.access(host_shim, os.X_OK):
                # run the host shim directly on the host
                return [host_exec, host_shim]
            # fallback if no shim
            return [host_exec, "network-store"]

        # 2) Regular PATH inside container
        cli = shutil.which("network-store")
        if cli:
            return [cli]

        # 3) pyenv shim in home (may be broken in container)
        shim = os.path.expanduser("~/.pyenv/shims/network-store")
        if os.path.exists(shim) and os.access(shim, os.X_OK):
            return [shim]

        return None

    def _is_host_exec_prefix(self, prefix: list[str]) -> bool:
        return bool(prefix) and os.path.basename(prefix[0]) == "distrobox-host-exec"

    def _run_network_store(
        self,
        prefix: list[str],
        args: list[str],
        *,
        timeout: int | None = None,
        detach: bool = False,
    ):
        """
        Run network-store with either:
        - direct exec (container PATH / shim)
        - host exec through `bash -lc` to get host env

        If detach=True, starts process and returns immediately.
        """
        cmd = prefix + args

        if detach:
            subprocess.Popen(
                cmd,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True,
            )
            return None

        return subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
        )

    def _network_store_status(self) -> tuple[bool, str]:
        """
        Call 'network-store status' and return (running, info_text).

        running == True  -> service is considered up
        running == False -> not running / unavailable
        """

        prefix = self._resolve_network_store_cli()
        if not prefix:
            info = self.tr(
                "network-store CLI not found in container PATH, "
                "~/.pyenv/shims, or via distrobox-host-exec."
            )
            return False, info

        try:
            proc = self._run_network_store(prefix, ["status"], timeout=5)
        except Exception as e:
            return False, self.tr(f"Error running status: {e}")

        out = (proc.stdout or "").strip()
        err = (proc.stderr or "").strip()
        info = out + ("\n" if out and err else "") + err
        text_lower = info.lower()

        if (
            proc.returncode != 0
            or "not running" in text_lower
            or "stopped" in text_lower
        ):
            return False, info or self.tr("network-store is not running.")
        return True, info or self.tr("network-store appears to be running.")

    def _update_network_store_service_action(self) -> None:
        """
        Update icon, text, and tooltip of the service action based on status.
        """
        if not hasattr(self, "action_network_store_service"):
            return
        if self.action_network_store_service is None:
            return

        running, info = self._network_store_status()

        if running:
            self.action_network_store_service.setIcon(self._icon_service_stop)
            self.action_network_store_service.setText(
                self.tr("Stop network-store service")
            )
            self.action_network_store_service.setToolTip(
                (info + "\n\n" if info else "") + self.tr("Click to stop the service.")
            )
        else:
            self.action_network_store_service.setIcon(self._icon_service_start)
            self.action_network_store_service.setText(
                self.tr("Start network-store service")
            )
            self.action_network_store_service.setToolTip(
                (info + "\n\n" if info else "") + self.tr("Click to start the service.")
            )

    def _toggle_network_store_service(self) -> None:
        """
        Start or stop the local network-store service depending on current status.
        """

        prefix = self._resolve_network_store_cli()
        if not prefix:
            msg = self.tr(
                "network-store CLI not found. "
                "Install it in the container or enable host exec."
            )
            self.iface.messageBar().pushWarning("Network Store", msg)
            self._update_network_store_service_action()
            return

        running, _info = self._network_store_status()

        if running:
            # STOP is short-lived; run normally and show output
            try:
                proc = self._run_network_store(prefix, ["stop"], timeout=15)
            except Exception as e:
                self.iface.messageBar().pushCritical(
                    "Network Store", self.tr(f"Error stopping network-store: {e}")
                )
                self._update_network_store_service_action()
                return

            out = (proc.stdout or "").strip()
            err = (proc.stderr or "").strip()
            msg = out + ("\n" if out and err else "") + err

            if proc.returncode != 0:
                self.iface.messageBar().pushCritical(
                    "Network Store", msg or "stop failed"
                )
            else:
                self.iface.messageBar().pushInfo("Network Store", msg or "stopped")

        else:
            # START is long-running; detach so QGIS doesn't kill it.
            try:
                self._run_network_store(
                    prefix,
                    [
                        "start",
                        "--store",
                        "filesystem",
                        "--store-type",
                        "analytics",
                        "--port",
                        "8888",
                    ],
                    detach=True,
                )
            except Exception as e:
                self.iface.messageBar().pushCritical(
                    "Network Store", self.tr(f"Error starting network-store: {e}")
                )
                self._update_network_store_service_action()
                return

            self.iface.messageBar().pushInfo("Network Store", "starting…")

        # Re-check status and refresh icon/tooltip
        self._update_network_store_service_action()

    # ------------------------------------------------------------------ #
    # Helper methods
    # ------------------------------------------------------------------ #

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate("NetworkStorePlugin", message)

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None,
        toolbar: str | None = "networkstore",
    ):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            if toolbar == "networkstore" and self.toolbar_networkstore is not None:
                self.toolbar_networkstore.addAction(action)
            elif toolbar == "arraystore" and self.toolbar_arraystore is not None:
                self.toolbar_arraystore.addAction(action)
            elif toolbar == "datasphere" and self.toolbar_datasphere is not None:
                self.toolbar_datasphere.addAction(action)
            else:
                self.iface.addToolBarIcon(action)

        if add_to_menu:
            self.iface.addPluginToMenu(self.menu, action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        # icon_kisters = os.path.join(pluginPath, "icon.png")
        # icon_datasphere = os.path.join(pluginPath, "icon_datasphere.png")
        icon_datasphere_raster = os.path.join(
            pluginPath, "icon_datasphere_with_raster.png"
        )
        icon_datasphere_locations = os.path.join(pluginPath, "icon_datasphere.png")
        icon_datasphere_raster_bulk = icon_datasphere_raster
        icon_networkstore_add = os.path.join(pluginPath, "icon_networkstore_add.png")
        icon_networkstore_load = os.path.join(pluginPath, "icon_networkstore_load.png")
        icon_networkstore_export = os.path.join(
            pluginPath, "icon_networkstore_export.png"
        )
        icon_networkstore_delete = os.path.join(
            pluginPath, "icon_networkstore_delete.png"
        )
        icon_networkstore_styles = os.path.join(
            pluginPath, "icon_networkstore_style.png"
        )
        icon_networkstore_jsoneditor = os.path.join(
            pluginPath, "icon_networkstore_jsoneditor2.png"
        )
        icon_service_start = os.path.join(pluginPath, "icon_networkstore_start.png")
        icon_service_stop = os.path.join(pluginPath, "icon_networkstore_stop.png")

        icon_arraystore_delete = os.path.join(pluginPath, "icon_arraystore_delete.png")
        icon_arraystore_download = os.path.join(
            pluginPath, "icon_arraystore_download.png"
        )
        icon_arraystore_settings = os.path.join(
            pluginPath, "icon_arraystore_settings.png"
        )
        icon_arraystore_table = os.path.join(
            pluginPath, "icon_arraystore_table_dock.png"
        )
        icon_arraystore_create = os.path.join(pluginPath, "icon_arraystore_create.png")

        self._icon_service_start = QIcon(icon_service_start)
        self._icon_service_stop = QIcon(icon_service_stop)

        # Add dedicated toolbars
        self.toolbar_networkstore = self.iface.addToolBar("Network Store")
        self.toolbar_arraystore = self.iface.addToolBar("Array Store")
        self.toolbar_datasphere = self.iface.addToolBar("Datasphere")

        self.toolbar_networkstore.setObjectName("NetworkStoreToolbar")
        self.toolbar_arraystore.setObjectName("ArrayStoreToolbar")
        self.toolbar_datasphere.setObjectName("DatasphereToolbar")

        # Network Store toolbar actions
        self.add_action(
            icon_datasphere_locations,
            text=self.tr("Load Datasphere locations"),
            callback=self.run_datasphere_load_locations,
            parent=self.iface.mainWindow(),
            toolbar="datasphere",
        )
        self.add_action(
            icon_datasphere_raster_bulk,
            text=self.tr("Bulk load rasters from Datasphere"),
            callback=self.run_datasphere_bulk_load_raster,
            parent=self.iface.mainWindow(),
            toolbar="datasphere",
        )
        self.add_action(
            icon_networkstore_delete,
            text=self.tr("Delete network from store"),
            callback=self.run_networkstore_delete,
            parent=self.iface.mainWindow(),
            toolbar="networkstore",
        )

        # Start/stop local network-store service
        self.action_network_store_service = self.add_action(
            icon_service_start,
            text=self.tr("Start network-store service"),
            callback=self._toggle_network_store_service,
            parent=self.iface.mainWindow(),
        )

        self.add_action(
            icon_networkstore_export,
            text=self.tr("Export layers to JSON"),
            callback=self.run_networkstore_export,
            # add_to_toolbar = False,
            parent=self.iface.mainWindow(),
            toolbar="networkstore",
        )
        self.add_action(
            icon_networkstore_add,
            text=self.tr("Create new network"),
            callback=self.run_networkstore_add,
            # add_to_toolbar = False,
            parent=self.iface.mainWindow(),
            toolbar="networkstore",
        )
        self.add_action(
            icon_networkstore_load,
            text=self.tr("Get layers from network store"),
            callback=self.run_networkstore_load,
            # add_to_toolbar = False,
            parent=self.iface.mainWindow(),
            toolbar="networkstore",
        )
        self.action_networkstore_styles = self.add_action(
            icon_networkstore_styles,
            text=self.tr("Manage network styles"),
            callback=self.run_networkstore_styles,
            parent=self.iface.mainWindow(),
            toolbar="networkstore",
        )

        # Create the dockable widget
        self.toggle_dock_action = self.add_action(
            icon_networkstore_jsoneditor,
            text=self.tr("Show JSON Editor"),
            callback=self.run_json_editor,
            parent=self.iface.mainWindow(),
            add_to_toolbar=True,
            toolbar="networkstore",
        )
        if not self._jsoneditor_available:
            msg = (
                "PyQtWebEngine is not available. JSON editor disabled. "
                "On Windows, install the 'qtwebengine' / 'qtwebengineview' package via OSGeo4W setup."
            )
            self.toggle_dock_action.setEnabled(False)
            self.toggle_dock_action.setToolTip(msg)
            try:
                self.iface.messageBar().pushWarning("Network Store", msg)
            except Exception:
                pass
        else:
            self.toggle_dock_action.triggered.connect(self.run_json_editor)
        # self.iface.addPluginToMenu("&Network Store Plugin", self.toggle_dock_action)

        # Initialize its icon & tooltip based on current status
        self._update_network_store_service_action()

        # Refresh status when user hovers the action (tooltip time)
        self.action_network_store_service.hovered.connect(
            self._update_network_store_service_action
        )

        # Also refresh periodically so icon stays right even without hover
        self._service_timer = QTimer(self.iface.mainWindow())
        self._service_timer.setInterval(10000)  # 10 seconds; tweak if you want
        self._service_timer.timeout.connect(self._update_network_store_service_action)
        self._service_timer.start()

        # Arraystore Toolbar actions
        self.action_arraystore_download = self.add_action(
            icon_arraystore_download,
            text=self.tr("Load a raster layer from ArrayStore"),
            callback=self.run_arraystore_load,
            parent=self.iface.mainWindow(),
            toolbar="arraystore",
        )
        self.action_arraystore_delete = self.add_action(
            icon_arraystore_delete,
            text=self.tr("Delete a raster layer from ArrayStore"),
            callback=self.run_arraystore_delete,
            parent=self.iface.mainWindow(),
            toolbar="arraystore",
        )
        self.action_arraystore_table = self.add_action(
            icon_arraystore_table,
            text=self.tr("Show available raster timeseries in ArrayStore"),
            callback=self.run_arraystore_toggle_table,
            parent=self.iface.mainWindow(),
            toolbar="arraystore",
        )
        self.action_arraystore_settings = self.add_action(
            icon_arraystore_settings,
            text=self.tr("Edit local ArrayStore configuration"),
            callback=self.run_arraystore_settings,
            parent=self.iface.mainWindow(),
            toolbar="arraystore",
        )
        self.action_arraystore_create = self.add_action(
            icon_arraystore_create,
            text=self.tr("Create new raster timeseries"),
            callback=self.run_arraystore_create,
            parent=self.iface.mainWindow(),
            toolbar="arraystore",
        )

        for action in (
            self.action_arraystore_download,
            self.action_arraystore_delete,
            self.action_arraystore_table,
            self.action_arraystore_settings,
            self.action_arraystore_create,
        ):
            action.setEnabled(
                action
                in (
                    self.action_arraystore_download,
                    self.action_arraystore_table,
                    self.action_arraystore_delete,
                )
            )

        self._hook_layer_tree_menu()

        # will be set False in run()
        self.first_start = True

        # Processing Provider
        self.initProcessing()

    def initProcessing(self):
        """Init Processing provider for QGIS >= 3.8."""
        self.provider = kisters_processingProvider()
        QgsApplication.processingRegistry().addProvider(self.provider)

    def _hook_layer_tree_menu(self) -> None:
        view = self.iface.layerTreeView()
        if view is None:
            return
        if self._layer_tree_menu_hooked:
            return
        self._layer_tree_menu_hooked = True
        view.contextMenuAboutToShow.connect(self._on_layer_tree_context_menu)

    # JSON editor wiring ------------------------------------------------------------------------------
    def _is_network_layer(self, vl: QgsVectorLayer) -> bool:
        return all(
            vl.customProperty(k, None) is not None
            for k in (
                "network:typename_full",
                "network:namespace",
                "network:layer",
                "network:bucket",
                "network:id",
            )
        )

    def _layer_base_url(self, vl: QgsVectorLayer) -> str | None:
        """
        Return the base URL for a network-store layer from custom props or by
        parsing the WFS source.
        """
        prop = str(vl.customProperty("network:base_url", "")).strip()
        if prop:
            return prop.rstrip("/")

        src = vl.source() or ""
        try:
            parsed = urlparse(src)
        except Exception:
            return None

        if parsed.scheme and parsed.netloc:
            base = f"{parsed.scheme}://{parsed.netloc}"
            path = parsed.path or ""
            idx = path.lower().find("/network-store")
            if idx != -1:
                base += path[:idx]
            return base.rstrip("/") or None

        return None

    def _fetch_all_versions(self, base_url: str, app_name: str) -> dict[int, dict]:
        nam = QNetworkAccessManager()
        req = QNetworkRequest(
            QUrl(f"{base_url.rstrip('/')}/network-store/schemas/{app_name}")
        )
        loop = QEventLoop()
        reply = nam.get(req)
        reply.finished.connect(loop.quit)
        loop.exec_()
        if reply.error() != QNetworkReply.NoError:
            raise RuntimeError(f"Schema fetch error: {reply.errorString()}")
        obj = json.loads(bytes(reply.readAll()).decode("utf-8"))
        return {int(k): v for k, v in obj.items()}

    def _best_version(self, versions: dict[int, dict]) -> int | None:
        return max(versions) if versions else None

    # JSON editor event handling ---------------------------------------------------------------

    def _on_active_layer_changed(self, layer):
        try:
            self._last_hooked_layer.selectionChanged.disconnect(
                self._on_selection_changed
            )  # type: ignore
        except Exception:
            pass
        self._last_hooked_layer = None
        if isinstance(layer, QgsVectorLayer):
            layer.selectionChanged.connect(self._on_selection_changed)
            self._last_hooked_layer = layer
        self._render_selected_into_editor()

    def _on_selection_changed(self, *args):
        self._render_selected_into_editor()

    # ---- render the selected feature into the dock ----

    def _render_selected_into_editor(self):
        vl = self.iface.activeLayer()
        if not isinstance(vl, QgsVectorLayer):
            self.dock_widget.show_warning("No active vector layer.")
            return
        if not self._is_network_layer(vl):
            self.dock_widget.show_status("Select a network layer to edit.")
            return
        if not vl.isEditable():
            self.dock_widget.show_warning(
                "Layer is not in edit mode. Toggle editing to modify attributes."
            )
            return

        ids = vl.selectedFeatureIds()
        if not ids:
            self.dock_widget.show_status("Select one feature to edit.")
            return
        if len(ids) > 1:
            self.dock_widget.show_warning(
                f"{len(ids)} features selected. Select exactly one element."
            )
            return

        feat = next(vl.getFeatures(QgsFeatureRequest(ids[0])), None)
        if not feat:
            self.dock_widget.show_error("Selected feature not found.")
            return

        payload = feature_to_json(feat)
        app_name = derive_app_name(payload, vl)

        base_url = self._layer_base_url(vl)
        if not base_url:
            QgsMessageLog.logMessage(
                "Could not determine base URL from layer; defaulting to http://localhost:8888",
                "NetworkStore",
                Qgis.Warning,
            )
            base_url = "http://localhost:8888"

        try:
            versions = self._fetch_all_versions(base_url, app_name)
            chosen = self._best_version(versions)  # noqa
        except Exception as e:
            versions, chosen = {}, None  # noqa
            QgsMessageLog.logMessage(
                f"Schema fetch error: {e}", "NetworkStore", Qgis.Warning
            )

        self.dock_widget.load_auto(
            payload["config_obj"], versions, {"app_name": app_name}
        )
        # if versions and chosen is not None:
        #     self.dock_widget.load_all(payload, versions, {"app_name": app_name, "version": chosen})
        # else:
        #     self.dock_widget.load_single(payload, {}, {"app_name": app_name, "version": -1})

    # ---- save back to attributes ----
    @contextmanager
    def _edit_session(self, vl: QgsVectorLayer):
        started = False
        if not vl.isEditable():
            if not vl.startEditing():
                yield (False, "Cannot start editing.")
                return
            started = True
        try:
            yield (True, "")
        finally:
            if started:
                if not vl.commitChanges():
                    vl.rollBack()

    def _apply_json_to_feature(
        self, vl: QgsVectorLayer, feat, data: dict
    ) -> tuple[bool, str]:
        updates = {}

        def set_if_exists(field, value):
            idx = vl.fields().indexOf(field)
            if idx >= 0:
                updates[idx] = value

        # top-level
        for k, v in data.items():
            if k == "config_obj":
                continue
            if k == "app_id" and isinstance(v, dict) and "adapter" in v:
                set_if_exists("adapter", v["adapter"])
            else:
                set_if_exists(k, v)

        # config_obj → same-named fields if exist
        for k, v in (data.get("config_obj") or {}).items():
            set_if_exists(k, v)

        if not updates:
            return False, "No matching fields to update."

        with self._edit_session(vl) as (ok, err):
            if not ok:
                return False, err
            for idx, value in updates.items():
                feat[idx] = value
            if not vl.updateFeature(feat):
                return False, "QGIS - JSONEditor sync failed.❌"
        return True, "QGIS - JSONEditor sync ok.✅"

    def _on_json_save(self, data: dict):
        vl = self.iface.activeLayer()
        if not isinstance(vl, QgsVectorLayer):
            self.dock_widget.show_error("No active vector layer.")
            return
        ids = vl.selectedFeatureIds()
        if len(ids) != 1:
            self.dock_widget.show_warning("Select exactly one feature to save.")
            return
        feat = next(vl.getFeatures(QgsFeatureRequest(ids[0])), None)
        if not feat:
            self.dock_widget.show_error("Selected feature not found.")
            return

        ok, msg = self._apply_json_to_feature(vl, feat, data)
        (self.dock_widget.show_status(msg) if ok else self.dock_widget.show_error(msg))

    # Theme handling QGIS - JSONEditor
    @staticmethod
    def _qcolor_hex(c: QColor) -> str:
        return "#{:02x}{:02x}{:02x}".format(c.red(), c.green(), c.blue())

    def _get_qgis_theme_palette(self) -> dict[str, str]:
        pal: QPalette = self.iface.mainWindow().palette()
        bg = pal.color(QPalette.Window)
        fg = pal.color(QPalette.WindowText)
        panel_bg = pal.color(QPalette.Base)
        panel_fg = pal.color(QPalette.Text)
        border = pal.color(QPalette.AlternateBase)
        accent = pal.color(QPalette.Highlight)
        accent_fg = pal.color(QPalette.HighlightedText)
        muted = pal.color(QPalette.Disabled, QPalette.Text)

        theme = {
            "bg": self._qcolor_hex(bg),
            "fg": self._qcolor_hex(fg),
            "panel": self._qcolor_hex(panel_bg),
            "panelText": self._qcolor_hex(panel_fg),
            "border": self._qcolor_hex(border),
            "accent": self._qcolor_hex(accent),
            "accentText": self._qcolor_hex(accent_fg),
            "muted": self._qcolor_hex(muted),
            # optional hint for JS (you already computed is_dark elsewhere):
            "isDark": bg.lightness() < 128,
        }
        return theme

    def send_qgis_theme_to_webview(self):
        js = f"window.applyTheme && window.applyTheme({json.dumps(self._get_qgis_theme_palette())});"
        self.web_engine_view.page().runJavaScript(js)

    def run_json_editor(self):
        """Run method that opens the dockable JSON editor."""
        if not self._jsoneditor_available:
            msg = "PyQtWebEngine is not available. Install 'qtwebengine' / 'qtwebengineview' via OSGeo4W to enable the JSON editor."
            try:
                self.iface.messageBar().pushWarning("Network Store", msg)
            except Exception:
                pass
            return
        if self.first_start:
            self.dock_widget = NetworkStoreDockableJsonEditor(
                parent=self.iface.mainWindow()
            )

            # check if it's dark them and choose the ace editor them accordingly
            # TODO: do the same styling for code and text view as well.
            # TODO: style th header (blue-white) akkording to the theme choosen

            bg_color = (
                QgsApplication.instance()
                .palette()
                .color(QgsApplication.instance().palette().Window)
            )
            is_dark = bg_color.lightness() < 128

            html_folder = "dist-dark" if is_dark else "dist-light"
            local_html_path = (
                Path(self.plugin_dir) / "jsoneditor-static" / html_folder / "index.html"
            )

            # Initialize QWebEngineView
            self.web_engine_view = QWebEngineView()
            self.web_engine_view.loadFinished.connect(
                lambda ok: self.send_qgis_theme_to_webview()
            )
            self.dock_widget.verticalLayout.replaceWidget(
                self.dock_widget.webView, self.web_engine_view
            )
            # QgsApplication.setAttribute(Qt.AA_DisableHighDpiScaling, True)
            # QgsApplication.setAttribute(
            #     Qt.AA_SynthesizeMouseForUnhandledTouchEvents, False
            # )
            # QgsApplication.setAttribute(
            #     Qt.AA_SynthesizeTouchForUnhandledMouseEvents, False
            # )

            self.web_engine_view.setUrl(
                QUrl.fromLocalFile(str(local_html_path.resolve()))
            )
            self.web_engine_view.setCursor(Qt.ArrowCursor)
            self.dock_widget.webView.deleteLater()  # Remove the old QWebView widget

            # Enable JavaScript and developer tools
            self.web_engine_view.settings().setAttribute(
                QWebEngineSettings.TouchIconsEnabled, False
            )
            self.web_engine_view.settings().setAttribute(
                QWebEngineSettings.LocalContentCanAccessRemoteUrls, True
            )
            self.web_engine_view.settings().setAttribute(
                QWebEngineSettings.LocalContentCanAccessFileUrls, True
            )
            self.web_engine_view.settings().setAttribute(
                QWebEngineSettings.JavascriptEnabled, True
            )
            self.web_engine_view.settings().setAttribute(
                QWebEngineSettings.PluginsEnabled, True
            )
            # Apply the current qgis theme colors
            # self.dock_widget.webView.settings().setAttribute(QWebEngineSettings.WebAttribute.DeveloperExtrasEnabled, True)

            # Add the dock widget to the QGIS main window
            self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock_widget)

            # WEB DEV TOOLS: Open developer tools in a separate window
            # self.dev_tools = QWebEngineView()
            # self.web_engine_view.page().setDevToolsPage(self.dev_tools.page())
            # self.dev_tools.show()

            self.dock_widget.web_engine_view = (
                self.web_engine_view
            )  # allow dock to call JS on the correct view

            self.channel = QWebChannel()
            self.bridge = JSONEditorBridge()
            self.channel.registerObject("backend", self.bridge)
            self.web_engine_view.page().setWebChannel(self.channel)
            self.send_qgis_theme_to_webview()
            # self.bridge.applyTheme.emit(self._get_qgis_theme_palette()) # use this to update the theme live

            # Connect saving from the web widget
            self.bridge.saveRequested.connect(self._on_json_save)

            # connect save from JS -> Python
            self.dock_widget.saveRequested.connect(self._on_json_save)

            # listen for layer/selection changes so the panel refreshes
            self.iface.layerTreeView().currentLayerChanged.connect(
                self._on_active_layer_changed
            )
            layer = self.iface.activeLayer()
            if isinstance(layer, QgsVectorLayer):
                layer.selectionChanged.connect(self._on_selection_changed)

            # Set the theme
            self.send_qgis_theme_to_webview()
            self.first_start = False
        else:
            self.web_engine_view.reload()

        self.dock_widget.show()

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""

        # 1) Disconnect signals that may call back into this instance
        try:
            self.iface.layerTreeView().currentLayerChanged.disconnect(
                self._on_active_layer_changed
            )
        except (TypeError, RuntimeError):
            # not connected or already disconnected
            pass

        try:
            layer = self.iface.activeLayer()
            if isinstance(layer, QgsVectorLayer):
                layer.selectionChanged.disconnect(self._on_selection_changed)
        except (TypeError, RuntimeError):
            pass

        # 2) Remove dock widget once, if it still exists
        dock = getattr(self, "dock_widget", None)
        if dock is not None and not sip.isdeleted(dock):
            try:
                self.iface.removeDockWidget(dock)
            except RuntimeError:
                # underlying C++ already removed it
                pass
            dock.deleteLater()
            self.dock_widget = None

        # 3a) Remove actions (menu and toolbar)
        toolbars = [
            tb
            for tb in (
                self.toolbar_networkstore,
                self.toolbar_arraystore,
                self.toolbar_datasphere,
            )
            if tb is not None
        ]
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            for toolbar in toolbars:
                toolbar.removeAction(action)

        # 3b) Remove the toolbar itself
        if self.toolbar_networkstore is not None:
            del self.toolbar_networkstore
            self.toolbar_networkstore = None
        if self.toolbar_arraystore is not None:
            del self.toolbar_arraystore
            self.toolbar_arraystore = None
        if self.toolbar_datasphere is not None:
            del self.toolbar_datasphere
            self.toolbar_datasphere = None

        # 4 Remove assets, styles and icons
        clean_icons()
        # TODO: clean assets
        QgsApplication.processingRegistry().removeProvider(self.provider)

        try:
            view = self.iface.layerTreeView()
            if view is not None and self._layer_tree_menu_hooked:
                view.contextMenuAboutToShow.disconnect(self._on_layer_tree_context_menu)
        except Exception:
            pass
        self._layer_tree_menu_hooked = False

    def _on_layer_tree_context_menu(self, menu: QMenu) -> None:
        view = self.iface.layerTreeView()
        if view is None:
            return
        if menu is None:
            return

        node = self._current_layer_tree_node(view)

        layer = view.currentLayer()
        if isinstance(layer, QgsRasterLayer):
            if menu.findChild(QMenu, "networkstore_arraystorage_menu") is not None:
                return
            menu.addSeparator()
            submenu = menu.addMenu(self.tr("Array Storage"))
            submenu.setObjectName("networkstore_arraystorage_menu")
            save_action = QAction(
                self.tr("Save raster symbology as classification JSON..."),
                submenu,
            )
            copy_action = QAction(
                self.tr("Copy raster symbology as classification JSON"),
                submenu,
            )
            save_action.setObjectName("networkstore_save_classification")
            copy_action.setObjectName("networkstore_copy_classification")
            save_action.triggered.connect(
                lambda _=False, lyr=layer: self._save_raster_classification_json(lyr)
            )
            copy_action.triggered.connect(
                lambda _=False, lyr=layer: self._copy_raster_classification_json(lyr)
            )
            submenu.addAction(save_action)
            submenu.addAction(copy_action)

        if isinstance(node, QgsLayerTreeGroup) and self._is_network_root_group(node):
            if menu.findChild(QMenu, "networkstore_main_menu") is None:
                menu.addSeparator()
                submenu = menu.addMenu(self.tr("Network Store"))
                submenu.setObjectName("networkstore_main_menu")

                save_action = QAction(self.tr("Save network styles..."), submenu)
                save_action.setObjectName("networkstore_save_style_set")
                save_action.triggered.connect(
                    lambda _=False, grp=node: self._save_network_style_set(grp)
                )
                submenu.addAction(save_action)

                apply_action = QAction(self.tr("Apply network style..."), submenu)
                apply_action.setObjectName("networkstore_apply_style_set")
                apply_action.triggered.connect(
                    lambda _=False, grp=node: self._apply_network_style_set(grp)
                )
                submenu.addAction(apply_action)

                export_action = QAction(self.tr("Export network (TODO)"), submenu)
                export_action.setObjectName("networkstore_export_network")
                export_action.setEnabled(False)
                submenu.addAction(export_action)

        if isinstance(layer, QgsVectorLayer) and self._is_network_layer(layer):
            if menu.findChild(QAction, "networkstore_save_layer_style") is None:
                menu.addSeparator()
                action = QAction(self.tr("Save layer style to style set..."), menu)
                action.setObjectName("networkstore_save_layer_style")
                action.triggered.connect(
                    lambda _=False, lyr=layer: self._save_layer_style_to_set(lyr)
                )
                menu.addAction(action)

    def _current_layer_tree_node(self, view):
        try:
            node = view.currentNode()
            if node is not None:
                return node
        except Exception:
            pass
        try:
            model = view.layerTreeModel()
            index = view.currentIndex()
            if model is None or not index.isValid():
                return None
            return model.index2node(index)
        except Exception:
            return None

    def _is_network_root_group(self, group: QgsLayerTreeGroup) -> bool:
        required = {"Nodes", "Links", "Polygons", "Controls"}
        child_groups = {
            child.name()
            for child in group.children()
            if isinstance(child, QgsLayerTreeGroup)
        }
        return required.issubset(child_groups)

    def _collect_group_layers(self, group: QgsLayerTreeGroup) -> list[QgsVectorLayer]:
        layers: list[QgsVectorLayer] = []
        for child in group.children():
            if isinstance(child, QgsLayerTreeGroup):
                layers.extend(self._collect_group_layers(child))
                continue
            if isinstance(child, QgsLayerTreeLayer):
                layer = child.layer()
                if isinstance(layer, QgsVectorLayer) and self._is_network_layer(layer):
                    layers.append(layer)
        return layers

    def _prompt_style_set_name(self, title: str, label: str) -> str | None:
        items = list_style_sets()
        raw, ok = QInputDialog.getItem(
            self.iface.mainWindow(), title, label, items, 0, True
        )
        if not ok:
            return None
        raw = str(raw).strip()
        if not raw:
            QMessageBox.warning(
                self.iface.mainWindow(),
                self.tr("Invalid name"),
                self.tr("Style set name is required."),
            )
            return None
        if raw in items:
            return raw
        name = self._sanitize_style_set_name(raw)
        if not name:
            QMessageBox.warning(
                self.iface.mainWindow(),
                self.tr("Invalid name"),
                self.tr("Style set name is required."),
            )
            return None
        return name

    def _prompt_existing_style_set(self, title: str, label: str) -> str | None:
        items = list_style_sets()
        name, ok = QInputDialog.getItem(
            self.iface.mainWindow(), title, label, items, 0, False
        )
        if not ok:
            return None
        return str(name).strip() or None

    def _sanitize_style_set_name(self, raw: str) -> str:
        name = re.sub(r"\s+", "_", (raw or "").strip())
        name = re.sub(r"[^A-Za-z0-9_-]+", "_", name)
        name = re.sub(r"_+", "_", name).strip("_")
        return name

    def _save_network_style_set(self, group: QgsLayerTreeGroup) -> None:
        layers = self._collect_group_layers(group)
        if not layers:
            QMessageBox.warning(
                self.iface.mainWindow(),
                self.tr("No layers"),
                self.tr("No network layers found in this group."),
            )
            return
        style_set = self._prompt_style_set_name(
            self.tr("Save network styles"), self.tr("Style set name:")
        )
        if not style_set:
            return
        dest_dir = style_set_dir(style_set)
        if os.path.isdir(dest_dir) and os.listdir(dest_dir):
            res = QMessageBox.question(
                self.iface.mainWindow(),
                self.tr("Overwrite style set"),
                self.tr(
                    f"Style set '{style_set}' already exists. Overwrite matching styles?"
                ),
                QMessageBox.Yes | QMessageBox.No,
            )
            if res != QMessageBox.Yes:
                return
        os.makedirs(dest_dir, exist_ok=True)
        saved = 0
        for layer in layers:
            if self._save_layer_style(layer, style_set):
                saved += 1
        self.iface.messageBar().pushInfo(
            "Network Store",
            self.tr(f"Saved {saved} style(s) to '{style_set}'."),
        )

    def _apply_network_style_set(self, group: QgsLayerTreeGroup) -> None:
        layers = self._collect_group_layers(group)
        if not layers:
            QMessageBox.warning(
                self.iface.mainWindow(),
                self.tr("No layers"),
                self.tr("No network layers found in this group."),
            )
            return
        style_set = self._prompt_existing_style_set(
            self.tr("Apply network style"),
            self.tr("Style set:"),
        )
        if not style_set:
            return
        network_id = ""
        for layer in layers:
            nid = str(layer.customProperty("network:id", "") or "").strip()
            if nid:
                network_id = nid
                break
        if not network_id:
            network_id = group.name()
        styled = apply_style_set_to_network(network_id, style_set)
        self.iface.messageBar().pushInfo(
            "Network Store",
            self.tr(
                f"Applied style set '{style_set}' to {styled} layer(s) in '{network_id}'."
            ),
        )

    def _save_layer_style_to_set(self, layer: QgsVectorLayer) -> None:
        style_set = self._prompt_existing_style_set(
            self.tr("Save layer style"),
            self.tr("Save to style set:"),
        )
        if not style_set:
            return
        if self._save_layer_style(layer, style_set):
            self.iface.messageBar().pushInfo(
                "Network Store",
                self.tr(f"Saved style for '{layer.name()}' to '{style_set}'."),
            )
        else:
            self.iface.messageBar().pushWarning(
                "Network Store",
                self.tr(f"Failed to save style for '{layer.name()}'."),
            )

    def _save_layer_style(self, layer: QgsVectorLayer, style_set: str) -> bool:
        display_name = (
            str(layer.customProperty("network:layer", "") or "").strip()
            or layer.name()
            or layer.id()
        )
        filename = f"{display_name}.qml"
        if os.sep:
            filename = filename.replace(os.sep, "_")
        if os.altsep:
            filename = filename.replace(os.altsep, "_")
        dest_dir = style_set_dir(style_set)
        os.makedirs(dest_dir, exist_ok=True)
        path = os.path.join(dest_dir, filename)
        categories = self._style_save_categories()
        if categories is None:
            result = layer.saveNamedStyle(path)
        else:
            try:
                result = layer.saveNamedStyle(path, categories=categories)
            except TypeError:
                result = layer.saveNamedStyle(path)
        ok = result[0] if isinstance(result, tuple) else bool(result)
        msg = result[1] if isinstance(result, tuple) and len(result) > 1 else ""
        if not ok:
            QgsMessageLog.logMessage(
                f"Failed to save style for '{layer.name()}': {msg}",
                "NetworkStore",
                Qgis.Warning,
            )
        return ok

    def _style_save_categories(self):
        if hasattr(QgsMapLayer, "StyleCategory"):
            sc = QgsMapLayer.StyleCategory
            if all(
                hasattr(sc, name) for name in ("Symbology", "Symbology3D", "Labeling")
            ):
                return sc.Symbology | sc.Symbology3D | sc.Labeling
        if all(
            hasattr(QgsMapLayer, name)
            for name in (
                "StyleCategorySymbology",
                "StyleCategorySymbology3D",
                "StyleCategoryLabeling",
            )
        ):
            return (
                QgsMapLayer.StyleCategorySymbology
                | QgsMapLayer.StyleCategorySymbology3D
                | QgsMapLayer.StyleCategoryLabeling
            )
        return None

    def _prompt_classification_name(self, layer: QgsRasterLayer) -> str | None:
        default_name = self._default_classification_name(layer)
        name, ok = QInputDialog.getText(
            self.iface.mainWindow(),
            self.tr("ArrayStorage Classification"),
            self.tr("Classification name:"),
            QLineEdit.Normal,
            default_name,
        )
        if not ok:
            return None
        name = str(name).strip()
        if not name:
            self.iface.messageBar().pushWarning(
                "Network Store", self.tr("Classification name is required.")
            )
            return None
        return name

    def _default_classification_name(self, layer: QgsRasterLayer) -> str:
        group_name = ""
        try:
            root = QgsProject.instance().layerTreeRoot()
            node = root.findLayer(layer.id())
            if node is not None:
                parent = node.parent()
                last_group = None
                while parent is not None and parent != root:
                    last_group = parent
                    parent = parent.parent()
                if last_group is not None:
                    group_name = str(last_group.name() or "").strip()
        except Exception:
            group_name = ""

        name = group_name or (layer.name() or "").strip()
        if not name:
            name = "classification"

        name = re.sub(r"\s+", "_", name)
        name = name.replace("/", "_")
        name = re.sub(r"_+", "_", name).strip("_")
        return name or "classification"

    def _build_raster_classification(self, layer: QgsRasterLayer, name: str) -> dict:
        return raster_style_to_classification(layer, name)

    def _save_raster_classification_json(self, layer: QgsRasterLayer) -> None:
        if not isinstance(layer, QgsRasterLayer) or not layer.isValid():
            self.iface.messageBar().pushWarning(
                "Network Store", self.tr("Select a valid raster layer.")
            )
            return
        name = self._prompt_classification_name(layer)
        if not name:
            return
        try:
            classification = self._build_raster_classification(layer, name)
        except Exception as e:  # noqa: BLE001
            self.iface.messageBar().pushWarning(
                "Network Store",
                self.tr(f"Failed to build classification from symbology: {e}"),
            )
            return

        default_path = f"{name}.json"
        path, _ = QFileDialog.getSaveFileName(
            self.iface.mainWindow(),
            self.tr("Save ArrayStorage classification"),
            default_path,
            self.tr("JSON (*.json)"),
        )
        if not path:
            return
        if not path.lower().endswith(".json"):
            path += ".json"
        try:
            with open(path, "w", encoding="utf-8") as f:
                json.dump(classification, f, indent=2)
        except Exception as e:  # noqa: BLE001
            self.iface.messageBar().pushWarning(
                "Network Store", self.tr(f"Failed to write JSON: {e}")
            )
            return

        self.iface.messageBar().pushInfo(
            "Network Store",
            self.tr(f"Saved classification JSON to {path}"),
        )

    def _copy_raster_classification_json(self, layer: QgsRasterLayer) -> None:
        if not isinstance(layer, QgsRasterLayer) or not layer.isValid():
            self.iface.messageBar().pushWarning(
                "Network Store", self.tr("Select a valid raster layer.")
            )
            return
        name = self._prompt_classification_name(layer)
        if not name:
            return
        try:
            classification = self._build_raster_classification(layer, name)
        except Exception as e:  # noqa: BLE001
            self.iface.messageBar().pushWarning(
                "Network Store",
                self.tr(f"Failed to build classification from symbology: {e}"),
            )
            return

        QApplication.clipboard().setText(json.dumps(classification, indent=2))
        self.iface.messageBar().pushInfo(
            "Network Store", self.tr("Classification JSON copied to clipboard.")
        )

    def get_layer_in_group(
        self, project_name, group_name, layer_name
    ) -> QgsVectorLayer:
        root = QgsProject.instance().layerTreeRoot()
        print(root.children())
        for child in root.children():
            if isinstance(child, QgsLayerTreeGroup):
                if child.name() == project_name:
                    print(child.children())
                    for gchild in child.children():
                        if isinstance(gchild, QgsLayerTreeGroup):
                            if gchild.name() == group_name:
                                print(gchild.children())
                                for ggchild in gchild.children():
                                    if ggchild.name() == layer_name:
                                        return ggchild.layer()
        raise ValueError(
            f"Layer ref not found in {project_name}/{group_name}/{layer_name}"
        )

    def run(self):
        """Generate a new blank model from the base dialog"""

        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        if self.first_start:
            # self.first_start = False
            self.dlg = NetworkStorePluginDialog()

        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            pass

    def run_datasphere_load_locations(self):
        dlg = self.dlg_datasphere_locations
        if dlg is None or sip.isdeleted(dlg):
            dlg = DatasphereDialogLoadLocations()
            self.dlg_datasphere_locations = dlg
        dlg.show()
        dlg.exec_()

    def run_datasphere_bulk_load_raster(self):
        dlg = self.dlg_datasphere_bulk_load
        if dlg is None or sip.isdeleted(dlg):
            dlg = DatasphereDialogLoadRasterBulk()
            self.dlg_datasphere_bulk_load = dlg
        dlg.show()
        dlg.exec_()

    def run_networkstore_export(self):
        """Run method that performs all the real work"""

        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        if self.first_start:
            # self.first_start = False
            self.dlg = NetworkStorePluginDialogExport()

        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            self.dlg.export_network()

    def run_networkstore_add(self):
        """Run method that performs all the real work"""

        dlg = self.dlg_networkstore_add
        if dlg is None or sip.isdeleted(dlg):
            dlg = NetworkStorePluginDialogPost()
            self.dlg_networkstore_add = dlg
        dlg._reload_configs()
        dlg._reload_style_sets()

        # show the dialog
        dlg.show()
        # Run the dialog event loop
        result = dlg.exec_()
        # See if OK was pressed
        if result:
            dlg.create_network()

    def run_networkstore_load(self):
        """Run method to get a network from the network store"""

        dlg = self.dlg_networkstore_load
        if dlg is None or sip.isdeleted(dlg):
            dlg = NetworkStorePluginDialogGet()
            self.dlg_networkstore_load = dlg
        dlg._reload_configs()
        dlg._reload_style_sets()

        # show the dialog
        dlg.show()
        # Run the dialog event loop
        result = dlg.exec_()
        # See if OK was pressed
        if result:
            nodes, links, polygons, controls, successful = dlg.get_network()
            if successful:
                return None

    def run_networkstore_delete(self):
        dlg = self.dlg_networkstore_delete
        if dlg is None or sip.isdeleted(dlg):
            dlg = NetworkStorePluginDialogDelete()
            self.dlg_networkstore_delete = dlg

        dlg.show()
        dlg.exec_()

    def run_networkstore_styles(self):
        dlg = self.dlg_networkstore_styles
        if dlg is None or sip.isdeleted(dlg):
            dlg = NetworkStoreStyleManagerDialog(self.iface.mainWindow())
            self.dlg_networkstore_styles = dlg
        dlg.show()
        dlg.exec_()

    def run_arraystore_load(self):
        """Run method to get a network from the network store"""

        dlg = self.dlg_arraystore_load
        if dlg is None or sip.isdeleted(dlg):
            dlg = ArrayStorageDialogLoadRaster()
            self.dlg_arraystore_load = dlg
        dlg.show()
        dlg.exec_()

    def run_arraystore_delete(self):
        """Run method to get a network from the network store"""

        dlg = self.dlg_arraystore_delete
        if dlg is None or sip.isdeleted(dlg):
            dlg = ArrayStorageDialogDelete()
            self.dlg_arraystore_delete = dlg
        dlg.show()
        dlg.exec_()

    def run_arraystore_settings(self):
        """Run method to get a network from the network store"""

        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        # if self.first_start:
        #     # self.first_start = False
        #     self.dlg = NetworkStorePluginDialogGet()

        # # show the dialog
        # self.dlg.show()
        # # Run the dialog event loop
        # result = self.dlg.exec_()
        # # See if OK was pressed
        # if result:
        #     nodes, links, polygons, controls, successful = self.dlg.get_network()
        #     if successful:
        #         return None

        pass

    def run_arraystore_toggle_table(self):
        """Run method to get a network from the network store"""

        dlg = self.dlg_arraystore_list
        if dlg is None or sip.isdeleted(dlg):
            dlg = ArrayStorageDialogListTimeseries()
            self.dlg_arraystore_list = dlg
        dlg.show()
        dlg.exec_()

    def run_arraystore_create(self):
        """Run method to get a network from the network store"""

        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        # if self.first_start:
        #     # self.first_start = False
        #     self.dlg = NetworkStorePluginDialogGet()

        # # show the dialog
        # self.dlg.show()
        # # Run the dialog event loop
        # result = self.dlg.exec_()
        # # See if OK was pressed
        # if result:
        #     nodes, links, polygons, controls, successful = self.dlg.get_network()
        #     if successful:
        #         return None

        pass
