"""
/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os
from qgis.PyQt.QtWidgets import (
    QWidget,
    QApplication,
    QDialog,
    QDialogButtonBox,
    QTableWidgetItem,
    QFileDialog,
    QMessageBox,
    QMenu,
    QSpinBox,
)
from qgis.PyQt.uic import loadUiType
from qgis.core import QgsProject, QgsVectorLayer, Qgis
from qgis.PyQt.QtCore import Qt, QUrl
from qgis.PyQt.QtGui import QDesktopServices, QColor

import json
import ssl
import re

from .utils.url_security import safe_urlopen, safe_urlretrieve, validate_http_url

try:
    from vgrid.utils.constants import DGGAL_TYPES
except ImportError:
    DGGAL_TYPES = {}

FORM_CLASS, _ = loadUiType(os.path.join(os.path.dirname(__file__), "ui/dggsclient.ui"))


class DGGSClientWidget(QDialog, FORM_CLASS):
    def __init__(self, vgridtools, settingsDialog, iface, parent):
        super(DGGSClientWidget, self).__init__(parent)
        self.setupUi(self)
        # Make dialog resizable
        self.setMinimumSize(500, 400)
        self.iface = iface
        self.canvas = iface.mapCanvas()
        self.vgridtools = vgridtools
        self.settings = settingsDialog
        self.set_status_bar(self.status, self.LblStatus)

        self.BtnApplyClose.button(QDialogButtonBox.StandardButton.Close).setAutoDefault(
            False
        )
        self.Filter.setFocus()
        QWidget.setTabOrder(self.Filter, self.TblZones)
        self.BtnOutputFolder.clicked.connect(self.browse_outfile)
        self.BtnConnect.clicked.connect(self.readdggs)

        project = QgsProject.instance()
        home_path = project.homePath()
        if not home_path:
            home_path = os.path.expanduser("~")
        self.LinOutputFolder.setText(home_path)

        columns = ["Zone ID", "Data", "DGGS-JSON", "Zone depth", "Zone geometry"]
        self.TblZones.setColumnCount(len(columns))
        self.TblZones.setHorizontalHeaderLabels(columns)
        self.TblZones.resizeColumnsToContents()
        self.TblZones.resizeRowsToContents()
        self.TblZones.horizontalHeader().setStretchLastSection(True)

        self.Filter.setEnabled(False)
        self.Filter.valueChanged.connect(self.updateDGGSTable)

        self.cboServerName.currentIndexChanged.connect(self.updateURL)
        self.cboServerName.setCurrentIndex(-1)

        # Connect collection and DGGS combobox handlers
        self.cboCollections.currentIndexChanged.connect(self.onCollectionChanged)
        self.cboDGGS.currentIndexChanged.connect(self.onDGGSChanged)

        # Flag to prevent handlers from firing during programmatic changes
        self._updating_combos = False

        self.cboServerName.setStyleSheet(
            "QComboBox {combobox-popup: 0; }"
        )  # To enable the setMaxVisibleItems
        self.cboServerName.setMaxVisibleItems(10)
        self.cboServerName.view().setVerticalScrollBarPolicy(
            Qt.ScrollBarPolicy.ScrollBarAsNeeded
        )

        self.TblZones.itemSelectionChanged.connect(self.onZoneSelectionChanged)
        self.TblZones.itemClicked.connect(self.onTableItemClicked)
        # Enable context menu
        self.TblZones.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.TblZones.customContextMenuRequested.connect(self.showContextMenu)
        self.BtnApplyClose.button(
            QDialogButtonBox.StandardButton.Close
        ).clicked.connect(self.closeDialog)

        self.dggs_servers = [
            "GNOSIS Map Server",
            "CRIM",
            "GeoInsight",
            "Geolynx",
            "Geomatys",
            "Safe Software",
            "Custom",
        ]

        self.dggs_urls = [
            "https://maps.gnosis.earth/ogcapi",
            "https://hirondelle.crim.ca/dggs-api",
            "https://dggs.geoinsight.ai/",
            "https://dggs.geokuup.ee/dggs-api",
            "https://server.examind.com/examind/WS/dggs/ogcaipilot",
            "https://ogc-dggs-testing.fmecloud.com/api/dggs",
            "",
        ]
        self.cboServerName.addItems(self.dggs_servers)

    def _reset_dialog_state(self):
        # Clear UI elements when dialog is closed
        self.cboCollections.clear()
        self.cboDGGS.clear()
        self.TxtAPI.clear()
        self.TblZones.setRowCount(0)
        self.LblStatus.clear()
        self.set_status_bar(self.status, self.LblStatus)

        # Restore cursor
        QApplication.setOverrideCursor(Qt.CursorShape.ArrowCursor)
        QApplication.restoreOverrideCursor()

    def closeDialog(self):
        self._reset_dialog_state()
        self.close()

    def closeEvent(self, event):
        # Handle window "X" close to keep state consistent
        self._reset_dialog_state()
        event.accept()

    def updateDGGSTable(self):
        name = self.Filter.text().lower()
        if name != "" and name is not None:
            visible_row = 0
            for row in range(self.TblZones.rowCount()):
                # Search across all columns (Zone ID, Map, Data, Zone Geometry)
                match = False
                for col in range(self.TblZones.columnCount()):
                    item = self.TblZones.item(row, col)
                    if item and name in item.text().lower():
                        match = True
                        break
                # Hide row if search term not found in any column
                self.TblZones.setRowHidden(row, not match)
                if match:
                    visible_row += 1
            if visible_row < self.TblZones.rowCount():
                self.LblZones.setText(str(visible_row) + " zones filtered")
        else:
            for row in range(self.TblZones.rowCount()):
                self.TblZones.setRowHidden(row, False)
            self.LblZones.setText(str(self.TblZones.rowCount()) + " zones loaded")

    def updateURL(self):
        self.TblZones.setRowCount(0)
        self.Filter.clear()
        self.TxtURL.clear()
        self.TxtAPI.clear()
        self.LblStatus.clear()
        self.LblZones.clear()
        self.set_status_bar(self.status, self.LblStatus)
        # Clear comboboxes
        self._updating_combos = True
        self.cboCollections.clear()
        self.cboDGGS.clear()
        self._updating_combos = False
        idx = self.cboServerName.currentIndex()
        if self.cboServerName.currentIndex() > -1:
            self.TxtURL.setText(self.dggs_urls[idx])

    def readdggs(self):
        try:
            collections_url = (
                self.TxtURL.text().strip().rstrip("/") + "/collections?f=json"
            )
            self.TxtAPI.setText(collections_url)

            self.TblZones.setRowCount(0)
            self.LblStatus.clear()
            self.set_status_bar(self.status, self.LblStatus)
            self.BtnConnect.setEnabled(False)
            # Clear comboboxes
            self._updating_combos = True
            self.cboCollections.clear()
            self.cboDGGS.clear()
            self._updating_combos = False
            # Fetch collections and populate cboCollections
            url = self.TxtURL.text().strip()
            self.load_collections(url)
            # Update API endpoint URL
            self.BtnConnect.setEnabled(True)
        except Exception:
            self.BtnConnect.setEnabled(True)
            return

    def fill_table_widget(self, table_widget, url, status_callback=None):
        table_widget.setRowCount(0)

        self.Filter.clear()
        self.LblZones.clear()
        # url_landingpage = self.ogc_urls[idx]
        url_landingpage = validate_http_url(url)
        # url_landingpage = url + '/?f=json'
        url_collections = url_landingpage.rstrip("/") + "/collections?f=json"
        try:
            collections_response = safe_urlopen(url_collections)
            if collections_response is not None:
                # storing the JSON response from url in data
                data_json = json.loads(collections_response.read())
                # Get only first-level collections from href, exclude nested ones like /collections/NaturalEarth/dggs
                all_collections = data_json.get("collections", [])
                collections = []
                for col in all_collections:
                    # Get href from collection (could be in 'links' array or directly as 'href')
                    href = None
                    if "href" in col:
                        href = col["href"]
                    elif "links" in col:
                        # Look for self link in links array
                        for link in col["links"]:
                            if (
                                link.get("rel") == "self"
                                or link.get("rel") == "collection"
                            ):
                                href = link.get("href")
                                break

                    if href:
                        # Extract collection ID from href like "/ogcapi/collections/NaturalEarth"
                        # Remove query parameters and fragments if present
                        href_clean = href.split("?")[0].split("#")[0].rstrip("/")
                        href_parts = [
                            p for p in href_clean.split("/") if p
                        ]  # Remove empty parts

                        # Find 'collections' in the path and get the next segment as collection ID
                        # href format: /ogcapi/collections/NaturalEarth (first-level)
                        # vs: /ogcapi/collections/NaturalEarth/dggs (nested - exclude)
                        try:
                            collections_idx = href_parts.index("collections")
                            if collections_idx + 1 < len(href_parts):
                                collection_id = href_parts[collections_idx + 1]
                                # Only include if it's first-level:
                                # 1. No additional path segments after collection name
                                # 2. Collection ID doesn't contain ':' (exclude NaturalEarth:raster, etc.)
                                is_top_level = collections_idx + 2 >= len(href_parts)
                                if (
                                    is_top_level
                                    and collection_id
                                    and ":" not in collection_id
                                ):
                                    collections.append(
                                        {"id": collection_id, "href": href}
                                    )
                        except ValueError:
                            # 'collections' not found in path, skip
                            pass

                if len(collections) > 0:
                    i = 0
                    for i in range(len(collections)):
                        table_widget.insertRow(i)
                        collection_id = collections[i]["id"]
                        # Display collection ID in the table
                        id_item = QTableWidgetItem(collection_id)
                        # Store collection ID as data (using UserRole) for consistency
                        id_item.setData(Qt.ItemDataRole.UserRole, collection_id)
                        id_item.setFlags(
                            Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
                        )
                        table_widget.setItem(i, 0, id_item)
                        status_callback(((i + 1) / len(collections)) * 100, None)
                    message = str(i + 1) + " layers loaded"
                    MessageBar = self.iface.messageBar()
                    MessageBar.pushMessage("Info", message, level=Qgis.Info, duration=2)
                    self.LblZones.setText(message)
                    self.Filter.setEnabled(True)
                    self.Filter.setFocus(True)
                else:
                    message = " 0 layer loaded"
                    self.LblZones.setText(message)
                    MessageBar = self.iface.messageBar()
                    MessageBar.pushMessage("Info", message, level=Qgis.Info, duration=2)
                    self.Filter.setEnabled(False)
                    self.Filter.setFocus(False)

            else:
                return
        except Exception as e:
            QMessageBox.warning(None, "Connection Error", str(e))
            return
        return

    def load_collections(self, url):
        """Load first-level collections into cboCollections combobox"""
        # Set cursor to wait
        QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)

        # Update status - loading
        self.LblStatus.setText("Loading collections...")
        try:
            base = validate_http_url(url)
            url_collections = base.rstrip("/") + "/collections?f=json"
            collections_response = safe_urlopen(url_collections)
            if collections_response is not None:
                data_json = json.loads(collections_response.read())
                all_collections = data_json.get("collections", [])
                collections = []
                for col in all_collections:
                    if not isinstance(col, dict):
                        continue
                    # Prefer direct collection id from the collection object
                    collection_id = col.get("id")
                    href = None
                    # Fallback: derive id from collection links if needed
                    if not collection_id:
                        if "href" in col:
                            href = col["href"]
                        elif "links" in col:
                            for link in col["links"]:
                                if (
                                    link.get("rel") == "self"
                                    or link.get("rel") == "collection"
                                ):
                                    href = link.get("href")
                                    break
                        if href:
                            href_clean = href.split("?")[0].split("#")[0].rstrip("/")
                            href_parts = [p for p in href_clean.split("/") if p]
                            try:
                                collections_idx = href_parts.index("collections")
                                if collections_idx + 1 < len(href_parts):
                                    collection_id = href_parts[collections_idx + 1]
                            except ValueError:
                                collection_id = None

                    if collection_id:
                        # Extract title from collection, if available
                        title = col.get("title", "")
                        collections.append(
                            {"id": collection_id, "href": href, "title": title}
                        )

                # Populate cboCollections
                self._updating_combos = True
                self.cboCollections.clear()
                total_collections = len(collections)
                for i, col in enumerate(collections):
                    self.cboCollections.addItem(
                        col["id"], col["id"]
                    )  # Store ID as data
                    # Set title as tooltip if available
                    if col.get("title"):
                        self.cboCollections.setItemData(
                            i, col["title"], Qt.ItemDataRole.ToolTipRole
                        )
                    # Update progress
                    if total_collections > 0:
                        percent = int(((i + 1) / total_collections) * 100)
                        self.status_bar.setValue(percent)
                self._updating_combos = False
                self.cboCollections.setCurrentIndex(-1)

                if len(collections) > 0:
                    message = str(len(collections)) + " collections loaded"
                    MessageBar = self.iface.messageBar()
                    MessageBar.pushMessage("Info", message, level=Qgis.Info, duration=2)
                    # Update status - completed
                    self.LblStatus.setText("Collections loaded")
                    self.status_bar.setValue(100)
                    self.status_bar.setFormat("100%")
                else:
                    # Update status - no collections
                    self.LblStatus.setText("No collections found")
                    self.status_bar.setValue(0)
                    self.status_bar.setFormat("0%")
        except Exception as e:
            # Update status - error
            self.LblStatus.setText("Error loading collections")
            self.status_bar.setValue(0)
            self.status_bar.setFormat("Error")
            QMessageBox.warning(
                None, "Connection Error", "Failed to load collections:\n" + str(e)
            )
        finally:
            # Restore cursor to default
            QApplication.setOverrideCursor(Qt.CursorShape.ArrowCursor)

    def onCollectionChanged(self, index):
        """Handle collection selection change - load DGGS items into cboDGGS"""
        if self._updating_combos or index < 0:
            return

        collection_id = self.cboCollections.itemData(index)
        if not collection_id:
            collection_id = self.cboCollections.currentText()

        # Clear DGGS combobox and zones table
        self.cboDGGS.clear()
        self.TblZones.setRowCount(0)
        self.LblZones.clear()

        # Fetch /dggs endpoint
        try:
            base_url = validate_http_url(self.TxtURL.text().strip())
        except ValueError:
            return

        dggs_url = (
            base_url.rstrip("/") + "/collections/" + collection_id + "/dggs?f=json"
        )
        # Update API endpoint URL
        self.TxtAPI.setText(dggs_url)

        # Update status - loading
        self.LblStatus.setText("Loading DGGS items...")
        self.set_status_bar(self.status, self.LblStatus)

        try:
            response = safe_urlopen(dggs_url)
            if response is not None:
                data_json = json.loads(response.read())
                # Extract DGGS items - could be in 'dggrs' or 'items' or similar
                dggs_items = []
                if "dggrs" in data_json:
                    dggs_items = data_json["dggrs"]
                elif "items" in data_json:
                    dggs_items = data_json["items"]
                elif isinstance(data_json, list):
                    dggs_items = data_json

                # Populate cboDGGS
                self._updating_combos = True
                total_items = len(dggs_items)
                for i, item in enumerate(dggs_items):
                    # Get DGGS ID - could be 'id', 'name', or in 'properties'
                    dggs_id = None
                    title = None
                    if isinstance(item, dict):
                        dggs_id = item.get("id") or item.get("name")
                        if not dggs_id and "properties" in item:
                            dggs_id = item["properties"].get("id") or item[
                                "properties"
                            ].get("name")
                        # Extract title from item, if available
                        title = item.get("title", "")
                        if not title and "properties" in item:
                            title = item["properties"].get("title", "")
                    elif isinstance(item, str):
                        dggs_id = item

                    if dggs_id:
                        self.cboDGGS.addItem(dggs_id, dggs_id)
                        # Set title as tooltip if available
                        if title:
                            self.cboDGGS.setItemData(
                                i, title, Qt.ItemDataRole.ToolTipRole
                            )

                    # Update progress
                    if total_items > 0:
                        percent = int(((i + 1) / total_items) * 100)
                        self.status_bar.setValue(percent)

                self._updating_combos = False
                self.cboDGGS.setCurrentIndex(-1)

                # Update status - completed
                self.LblStatus.setText("DGGS items loaded")
                self.status_bar.setValue(100)
                self.status_bar.setFormat("100%")
        except Exception as e:
            self.LblStatus.setText("Error loading DGGS items")
            self.status_bar.setValue(0)
            self.status_bar.setFormat("Error")
            QMessageBox.warning(
                None, "Connection Error", "Failed to load DGGS items:\n" + str(e)
            )

    def onDGGSChanged(self, index):
        """Handle DGGS selection change - load zones into TblZones"""
        if self._updating_combos or index < 0:
            return

        dggs_id = self.cboDGGS.itemData(index)
        if not dggs_id:
            dggs_id = self.cboDGGS.currentText()

        collection_id = self.cboCollections.itemData(self.cboCollections.currentIndex())
        if not collection_id:
            collection_id = self.cboCollections.currentText()

        # Clear zones table
        self.TblZones.setRowCount(0)
        self.LblZones.clear()

        # Fetch /zones endpoint
        try:
            base_url = validate_http_url(self.TxtURL.text().strip())
        except ValueError:
            return

        zones_url = (
            base_url.rstrip("/")
            + "/collections/"
            + collection_id
            + "/dggs/"
            + dggs_id
            + "/zones?f=json"
        )
        # Update API endpoint URL
        self.TxtAPI.setText(zones_url)

        # Update status - loading
        self.LblStatus.setText("Loading zones...")
        self.set_status_bar(self.status, self.LblStatus)
        try:
            response = safe_urlopen(zones_url)
            if response is not None:
                data_json = json.loads(response.read())
                # Extract zones - could be in 'zones', 'items', 'features', or direct array
                zones = []
                if "zones" in data_json:
                    zones = data_json["zones"]
                elif "items" in data_json:
                    zones = data_json["items"]
                elif "features" in data_json:
                    zones = data_json["features"]
                elif isinstance(data_json, list):
                    zones = data_json

                # Populate TblZones with 5 columns: Zone ID, Data, DGGS-JSON, Zone depth, Zone Geometry
                total_zones = len(zones)
                if len(zones) > 0:
                    for i, zone in enumerate(zones):
                        self.TblZones.insertRow(i)

                        # Extract data from zone
                        zone_id = None

                        if isinstance(zone, dict):
                            # Get Zone ID
                            zone_id = (
                                zone.get("id")
                                or zone.get("zone_id")
                                or zone.get("zoneId")
                            )
                            if not zone_id and "properties" in zone:
                                props = zone["properties"]
                                zone_id = (
                                    props.get("id")
                                    or props.get("zone_id")
                                    or props.get("zoneId")
                                )
                        elif isinstance(zone, str):
                            zone_id = zone

                        # Set default values if not found
                        if not zone_id:
                            zone_id = "Zone " + str(i + 1)

                        # Create items for each column
                        # Column 0: Zone ID (as hyperlink)
                        zone_url = (
                            base_url.rstrip("/")
                            + "/collections/"
                            + collection_id
                            + "/dggs/"
                            + dggs_id
                            + "/zones/"
                            + zone_id
                            + "?f=json"
                        )
                        zone_id_item = QTableWidgetItem(str(zone_id))
                        zone_id_item.setFlags(
                            Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
                        )
                        # Store zone URL in item data
                        zone_id_item.setData(Qt.ItemDataRole.UserRole, zone_url)
                        # Also store full zone data if it's a dict (for backward compatibility)
                        if isinstance(zone, dict):
                            zone_id_item.setData(Qt.ItemDataRole.UserRole + 1, zone)
                        else:
                            zone_id_item.setData(Qt.ItemDataRole.UserRole + 1, zone_id)
                        # Style as link (blue color)
                        zone_id_item.setForeground(QColor(0, 0, 255))  # Blue color
                        self.TblZones.setItem(i, 0, zone_id_item)

                        # Column 1: Data (as hyperlink)
                        data_url = (
                            base_url.rstrip("/")
                            + "/collections/"
                            + collection_id
                            + "/dggs/"
                            + dggs_id
                            + "/zones/"
                            + zone_id
                            + "/data"
                        )
                        data_item = QTableWidgetItem("Data")
                        data_item.setFlags(
                            Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
                        )
                        # Store URL in item data
                        data_item.setData(Qt.ItemDataRole.UserRole, data_url)
                        # Style as link (blue color)
                        data_item.setForeground(QColor(0, 0, 255))  # Blue color
                        self.TblZones.setItem(i, 1, data_item)

                        # Column 2: DGGS-JSON (as hyperlink)
                        dggs_json_url = (
                            base_url.rstrip("/")
                            + "/collections/"
                            + collection_id
                            + "/dggs/"
                            + dggs_id
                            + "/zones/"
                            + zone_id
                            + "/data.json"
                        )
                        dggs_json_item = QTableWidgetItem("DGGS-JSON")
                        dggs_json_item.setFlags(
                            Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
                        )
                        # Store base URL (without zone-depth parameter) in item data for later updates
                        dggs_json_item.setData(Qt.ItemDataRole.UserRole, dggs_json_url)
                        # Store collection_id, dggs_id, zone_id for URL updates
                        dggs_json_item.setData(
                            Qt.ItemDataRole.UserRole + 2,
                            {
                                "collection_id": collection_id,
                                "dggs_id": dggs_id,
                                "zone_id": zone_id,
                                "base_url": base_url,
                            },
                        )
                        # Style as link (blue color)
                        dggs_json_item.setForeground(QColor(0, 0, 255))  # Blue color
                        self.TblZones.setItem(i, 2, dggs_json_item)

                        # Column 3: Zone depth (as embedded spinbox)
                        zone_depth_spinbox = QSpinBox()
                        zone_depth_spinbox.setMinimum(-1)  # -1 represents blank/empty
                        # Get min_res and max_res from DGGAL_TYPES based on selected DGGS
                        dggs_type_lower = dggs_id.lower()
                        if DGGAL_TYPES and dggs_type_lower in DGGAL_TYPES:
                            DGGAL_TYPES[dggs_type_lower]["min_res"]
                            max_res = DGGAL_TYPES[dggs_type_lower]["max_res"]
                            zone_depth_spinbox.setMaximum(max_res)
                        else:
                            # Fallback to default range if not found
                            zone_depth_spinbox.setMaximum(10)
                        zone_depth_spinbox.setSpecialValueText(
                            ""
                        )  # Display empty string when value is -1
                        zone_depth_spinbox.setValue(-1)  # Default to blank
                        zone_depth_spinbox.setMinimumWidth(80)
                        # Connect value change signal to update DGGS-JSON URL
                        zone_depth_spinbox.valueChanged.connect(
                            lambda value, row=i, col=2: self.onZoneDepthChanged(
                                value, row, col
                            )
                        )
                        self.TblZones.setCellWidget(i, 3, zone_depth_spinbox)

                        # Column 4: Zone Geometry (as hyperlink)
                        geometry_url = (
                            base_url.rstrip("/")
                            + "/collections/"
                            + collection_id
                            + "/dggs/"
                            + dggs_id
                            + "/zones/"
                            + zone_id
                            + ".geojson"
                        )
                        geometry_item = QTableWidgetItem("GeoJSON")
                        geometry_item.setFlags(
                            Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled
                        )
                        # Store URL in item data
                        geometry_item.setData(Qt.ItemDataRole.UserRole, geometry_url)
                        # Style as link (blue color)
                        geometry_item.setForeground(QColor(0, 0, 255))  # Blue color
                        self.TblZones.setItem(i, 4, geometry_item)

                        # Update progress
                        if total_zones > 0:
                            percent = int(((i + 1) / total_zones) * 100)
                            self.status_bar.setValue(percent)

                    message = str(len(zones)) + " zones loaded"
                    self.LblZones.setText(message)
                    MessageBar = self.iface.messageBar()
                    MessageBar.pushMessage("Info", message, level=Qgis.Info, duration=2)
                    self.Filter.setEnabled(True)
                    # Update status - completed
                    self.LblStatus.setText("Zones loaded")
                    self.status_bar.setValue(100)
                    self.status_bar.setFormat("100%")
                else:
                    self.LblZones.setText("0 zones loaded")
                    self.Filter.setEnabled(False)
                    # Update status - no zones
                    self.LblStatus.setText("No zones found")
                    self.status_bar.setValue(0)
                    self.status_bar.setFormat("0%")
        except Exception as e:
            self.LblStatus.setText("Error loading zones")
            self.status_bar.setValue(0)
            self.status_bar.setFormat("Error")
            QMessageBox.warning(
                None, "Connection Error", "Failed to load zones:\n" + str(e)
            )
            self.LblZones.setText("Error loading zones")

    def browse_outfile(self):
        newname = QFileDialog.getExistingDirectory(
            None, "Output Folder", self.LinOutputFolder.displayText()
        )
        if newname is not None:
            self.LinOutputFolder.setText(newname)

    def onZoneDepthChanged(self, value, row, col):
        """Update DGGS-JSON URL when zone depth value changes"""
        # Get the DGGS-JSON item from column 2 (col parameter)
        dggs_json_item = self.TblZones.item(row, col)
        if dggs_json_item is None:
            return

        # Get stored URL components
        url_data = dggs_json_item.data(Qt.ItemDataRole.UserRole + 2)
        if url_data:
            base_url = url_data["base_url"]
            collection_id = url_data["collection_id"]
            dggs_id = url_data["dggs_id"]
            zone_id = url_data["zone_id"]

            # Build base URL without zone-depth parameter
            zone_path = (
                f"/collections/{collection_id}/dggs/{dggs_id}/zones/{zone_id}/data.json"
            )
            base_dggs_json_url = base_url.rstrip("/") + zone_path

            # Get max_res from DGGAL_TYPES for validation
            dggs_type_lower = dggs_id.lower()
            max_res = 10  # Default fallback
            if DGGAL_TYPES and dggs_type_lower in DGGAL_TYPES:
                max_res = DGGAL_TYPES[dggs_type_lower]["max_res"]

            # Add zone-depth parameter if value is not blank (0 to max_res)
            if value >= 0 and value <= max_res:
                dggs_json_url = base_dggs_json_url + "?zone-depth=" + str(value)
            else:
                # Blank value: use base URL without zone-depth parameter
                dggs_json_url = base_dggs_json_url

            # Update the stored URL
            dggs_json_item.setData(Qt.ItemDataRole.UserRole, dggs_json_url)
        else:
            # Fallback: get base URL from item data and update zone-depth parameter
            current_url = dggs_json_item.data(Qt.ItemDataRole.UserRole)
            if current_url:
                # Remove existing zone-depth parameter if present
                # Remove zone-depth parameter from URL
                base_url = re.sub(r"[?&]zone-depth=\d+", "", current_url)
                # Clean up: if base_url ends with ? or has &&, fix it
                base_url = re.sub(r"[?&]+$", "", base_url)
                base_url = base_url.replace("&&", "&").replace("?&", "?")

                # Get max_res from DGGAL_TYPES for validation (fallback case)
                # Try to extract dggs_id from URL if available
                max_res = 10  # Default fallback
                # Parse dggs_id from URL if possible
                if "/dggs/" in current_url:
                    parts = current_url.split("/dggs/")
                    if len(parts) > 1:
                        dggs_part = parts[1].split("/")[0].split("?")[0]
                        dggs_type_lower = dggs_part.lower()
                        if DGGAL_TYPES and dggs_type_lower in DGGAL_TYPES:
                            max_res = DGGAL_TYPES[dggs_type_lower]["max_res"]

                # Add zone-depth parameter if value is not blank (0 to max_res)
                if value >= 0 and value <= max_res:
                    # Check if base URL already has parameters
                    if "?" in base_url:
                        dggs_json_url = base_url + "&zone-depth=" + str(value)
                    else:
                        dggs_json_url = base_url + "?zone-depth=" + str(value)
                else:
                    dggs_json_url = base_url

                dggs_json_item.setData(Qt.ItemDataRole.UserRole, dggs_json_url)
            else:
                dggs_json_url = None

        # Update TxtAPI based on currently selected row's zone depth
        current_row = self.TblZones.currentRow()
        # Also check selected ranges as fallback
        selected_ranges = self.TblZones.selectedRanges()
        selected_row = -1
        if selected_ranges and len(selected_ranges) > 0:
            selected_row = selected_ranges[0].topRow()
        # Use currentRow if available, otherwise use selected ranges
        active_row = current_row if current_row >= 0 else selected_row

        # If the changed row is the selected row, update TxtAPI
        if active_row >= 0 and active_row == row:
            # Get the max_res from DGGAL_TYPES for validation
            dggs_json_item = self.TblZones.item(row, col)
            max_res = 10  # Default fallback
            if dggs_json_item:
                url_data = dggs_json_item.data(Qt.ItemDataRole.UserRole + 2)
                if url_data and "dggs_id" in url_data:
                    dggs_type_lower = url_data["dggs_id"].lower()
                    if DGGAL_TYPES and dggs_type_lower in DGGAL_TYPES:
                        max_res = DGGAL_TYPES[dggs_type_lower]["max_res"]

            # Only update if zone depth is not blank (0 to max_res)
            if value >= 0 and value <= max_res:
                current_api_url = self.TxtAPI.text()
                if current_api_url:
                    # Remove existing zone-depth parameter if present
                    if "&zone-depth=" in current_api_url:
                        base_url = current_api_url.split("&zone-depth=")[0].split(
                            "?zone-depth="
                        )[0]
                    elif "?zone-depth=" in current_api_url:
                        base_url = current_api_url.split("?zone-depth=")[0]
                    else:
                        base_url = current_api_url

                    # Add zone-depth parameter to URL
                    if "?" in base_url:
                        updated_url = base_url + "&zone-depth=" + str(value)
                    else:
                        updated_url = base_url + "?zone-depth=" + str(value)
                    self.TxtAPI.setText(updated_url)
            # If value is -1 (blank), keep TxtAPI as is (do nothing)

    def onTableItemClicked(self, item):
        """Handle clicks on table items - open URLs for Zone ID, Data, DGGS-JSON and GeoJSON columns"""
        column = item.column()
        # Check if clicked on Zone ID (column 0), Data (column 1), DGGS-JSON (column 2) or GeoJSON (column 4)
        if column == 0 or column == 1 or column == 2 or column == 4:
            url = item.data(Qt.ItemDataRole.UserRole)
            if url:
                url_obj = QUrl(url)
                if url_obj.isValid():
                    QDesktopServices.openUrl(url_obj)

    def onZoneSelectionChanged(self):
        """Handle zone row selection - update TxtAPI with zone URL"""
        self.set_status_bar(self.status, self.LblStatus)
        selected_items = self.TblZones.selectedItems()
        if selected_items and len(selected_items) > 0:
            # Get the first selected row
            row = selected_items[0].row()
            item = self.TblZones.item(row, 0)
            if item is not None:
                zone_id = item.text()
                # Get collection_id and dggs_id
                collection_id = self.cboCollections.itemData(
                    self.cboCollections.currentIndex()
                )
                if not collection_id:
                    collection_id = self.cboCollections.currentText()

                dggs_id = self.cboDGGS.itemData(self.cboDGGS.currentIndex())
                if not dggs_id:
                    dggs_id = self.cboDGGS.currentText()

                base_url = self.TxtURL.text().strip()
                if base_url and collection_id and dggs_id and zone_id:
                    # Construct zone URL: /collections/{collection_id}/dggs/{dggs_id}/zones/{zone_id}
                    zone_url = (
                        base_url.rstrip("/")
                        + "/collections/"
                        + collection_id
                        + "/dggs/"
                        + dggs_id
                        + "/zones/"
                        + zone_id
                        + "?f=json"
                    )
                    self.TxtAPI.setText(zone_url)

    def showContextMenu(self, position):
        """Show context menu for table zones"""
        item = self.TblZones.itemAt(position)
        if item is None:
            return

        row = item.row()
        zone_id_item = self.TblZones.item(row, 0)
        if zone_id_item is None:
            return

        zone_id = zone_id_item.text()

        # Get collection_id and dggs_id
        collection_id = self.cboCollections.itemData(self.cboCollections.currentIndex())
        if not collection_id:
            collection_id = self.cboCollections.currentText()

        dggs_id = self.cboDGGS.itemData(self.cboDGGS.currentIndex())
        if not dggs_id:
            dggs_id = self.cboDGGS.currentText()

        try:
            base_url = validate_http_url(self.TxtURL.text().strip())
        except ValueError:
            return
        if not collection_id or not dggs_id:
            return

        # Create context menu
        menu = QMenu(self)
        # Download zone geometry and Load into QGIS
        action_download_load = menu.addAction("Download zone geometry")
        action_download_load.triggered.connect(
            lambda: self.downloadAndLoadZone(zone_id, collection_id, dggs_id, base_url)
        )

        # Show menu at cursor position
        menu.exec(self.TblZones.viewport().mapToGlobal(position))

    def downloadAndLoadZone(self, zone_id, collection_id, dggs_id, base_url):
        """Download zone geometry and load into QGIS"""
        self.set_status_bar(self.status, self.LblStatus)
        try:
            geometry_url = (
                base_url.rstrip("/")
                + "/collections/"
                + collection_id
                + "/dggs/"
                + dggs_id
                + "/zones/"
                + zone_id
                + ".geojson"
            )

            # Save to output folder
            outdir = self.LinOutputFolder.displayText()
            if not outdir:
                outdir = os.path.expanduser("~")
            filename = os.path.join(outdir, zone_id + ".geojson")

            # Download the file (optional unverified SSL for self-signed OGC servers)
            ssl_ctx = ssl.create_default_context()
            ssl_ctx.check_hostname = False
            ssl_ctx.verify_mode = ssl.CERT_NONE
            safe_urlretrieve(geometry_url, filename, context=ssl_ctx)

            # Load the layer in QGIS
            layer = QgsVectorLayer(filename, zone_id, "ogr")
            layer.dataProvider().setEncoding("UTF-8")
            if layer.isValid():
                QgsProject.instance().addMapLayer(layer)
                self.iface.zoomToActiveLayer()
                self.LblStatus.setText("Zone " + zone_id + " loaded")
                MessageBar = self.iface.messageBar()
                MessageBar.pushMessage(
                    "Info",
                    "Zone " + zone_id + " downloaded and loaded",
                    level=Qgis.Info,
                    duration=2,
                )
            else:
                QMessageBox.warning(None, "Error", "Failed to load zone GeoJSON file")
        except Exception as e:
            QMessageBox.warning(
                None, "Download Error", "Failed to download and load zone:\n" + str(e)
            )

    def set_status_bar(self, status_bar, status_lable):
        status_bar.setMinimum(0)
        status_bar.setMaximum(100)
        status_bar.setValue(0)
        status_bar.setFormat("Ready")
        self.status_bar = status_bar
        self.status_lable = status_lable

    def status_callback(self, percent_complete, lable):
        try:
            self.status_lable.setText(lable)
            message = str(int(round(percent_complete))) + "%"
            self.status_bar.setFormat(message)

            if percent_complete < 0:
                self.status_bar.setValue(0)
            elif percent_complete > 100:
                self.status_bar.setValue(100)
            else:
                self.status_bar.setValue(int(round(percent_complete)))

            self.iface.statusBarIface().showMessage(message)

            # print("status_callback(" + message + ")")
        except BaseException:
            print(message)

        # add handling of "Close" button
        return 0
