"""
/***************************************************************************
 QRiS
                                 A QGIS plugin
 QGIS Riverscapes Studio (QRiS)
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2021-05-06
        git sha              : $Format:%H$
        copyright            : (C) 2021 by North Arrow Research
        email                : info@northarrowresearch.com
 ***************************************************************************/

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

from qgis.core import (
    QgsApplication,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsProject,
)
from qgis.gui import QgisInterface, QgsMapToolEmitPoint
from qgis.PyQt import QtCore, QtGui, QtWidgets
from qgis.PyQt.QtCore import QSettings, pyqtSlot

# Initialize Qt resources from file resources.py
from . import resources  # noqa: F401  # side-effect import: registers Qt resource paths
from .compat import (
    LEFT_DOCK,
    MESSAGE_LEVEL_CRITICAL,
    MESSAGE_LEVEL_WARNING,
    MSGBOX_ICON_WARNING,
    MSGBOX_NO,
    MSGBOX_YES,
    RIGHT_DOCK,
    TOOL_BTN_ICON_ONLY,
    TOOL_BTN_INSTANT_POPUP,
    TOOL_BTN_MENU_POPUP,
    TOOL_BTN_TEXT_BESIDE,
    TOOL_BTN_TEXT_ONLY,
)
from .gp.load_project_task import LoadProjectTask
from .gp.update_metadata import check_metadata, update_metadata
from .gp.watershed_attributes import WatershedAttributes
from .lib.data_exchange import browse_data_exchange as open_data_exchange
from .QRiS.path_utilities import safe_make_abspath, safe_make_relpath
from .QRiS.qrave_integration import QRaveIntegration
from .QRiS.settings import CONSTANTS, Settings
from .view.frm_about import FrmAboutDialog

# Import the code for the DockWidget
from .view.frm_dockwidget import QRiSDockWidget
from .view.frm_new_project import FrmNewProject
from .view.frm_settings import (
    DOCK_WIDGET_LOCATION,
    TELEMETRY_ENABLED_KEY,
    FrmSettings,
    default_dock_widget_location,
)
from .view.metadata_field_editor_widget import initialize_metadata_widget

LAST_PROJECT_FOLDER = "last_project_folder"
RECENT_PROJECT_LIST = "recent_projects"

dock_widget_locations = {
    "left": LEFT_DOCK,
    "right": RIGHT_DOCK,
}


class QRiSToolbar:
    """QGIS Plugin Implementation."""

    def __init__(self, iface: QgisInterface):
        """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
        self.qrave = None

        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)

        # initialize Metadata Widget
        initialize_metadata_widget()

        self.qproject = QgsProject.instance()
        self.qproject.cleared.connect(self.close_project)

        # initialize locale
        locale = QtCore.QSettings().value("locale/userLocale")[0:2]
        locale_path = os.path.join(self.plugin_dir, "i18n", f"qris_{locale}.qm")
        if os.path.exists(locale_path):
            self.translator = QtCore.QTranslator()
            self.translator.load(locale_path)
            QtCore.QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menus = []
        self.menu = self.tr("&QGIS Riverscapes Studio (QRiS)")
        self.toolbar = self.iface.addToolBar("QRiS")
        self.toolbar.setObjectName("QRiS")

        Settings(iface=self.iface)

        # Populated on load from a URL
        self.acknowledgements = None

        self.pluginIsActive = False
        self.dockwidget = None

    # 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 QtCore.QCoreApplication.translate("QRiS", message)

    def initGui(self):
        """Create a single QRiS menu button with icon and text, containing Project and Help submenus."""

        # Create the main menu for the QRiS toolbar button
        self.qris_menu = QtWidgets.QMenu()

        # Populate Project menu
        self.project_menu = self.qris_menu.addMenu(QtGui.QIcon(":/plugins/qris_toolbar/folder"), "Project")
        self.add_menu_action(
            self.project_menu,
            "new",
            "New QRiS Project",
            self.create_new_project_dialog,
            True,
            "Create a New QRiS Project",
        )
        self.add_menu_action(
            self.project_menu,
            "folder",
            "Open QRiS Project",
            self.open_existing_project,
            True,
            "Open Existing QRiS Project",
        )
        self.mru_menu = self.project_menu.addMenu(QtGui.QIcon(":/plugins/qris_toolbar/folder"), "Recent QRiS Projects")
        self.add_menu_action(
            self.project_menu,
            "close",
            "Close Project",
            self.close_project,
            True,
            "Close the Current QRiS Project",
        )
        self.load_mru_projects()

        # Show Panel action
        self.add_menu_action(
            self.qris_menu,
            "qris_icon",
            "Show QRiS Panel",
            self.toggle_widget,
            True,
            "Show the QRiS Dockable Panel",
        )
        self.add_menu_action(
            self.qris_menu,
            "data_exchange",
            "Browse Data Exchange Projects",
            lambda: open_data_exchange(self.iface.mapCanvas()),
            True,
            "Browse Riverscapes Data Exchange for Projects near the current map view",
        )

        # Populate Help menu
        self.help_menu = self.qris_menu.addMenu(QtGui.QIcon(":/plugins/qris_toolbar/help"), "Help")
        self.add_menu_action(
            self.help_menu,
            "help",
            "QRiS Online Help",
            lambda: QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://qris.riverscapes.net")),
            True,
            "Launch QRiS Online Help in default browser",
        )
        self.add_menu_action(
            self.help_menu,
            "settings",
            "Settings",
            self.show_settings,
            True,
            "QRiS Settings",
        )
        self.add_menu_action(
            self.help_menu,
            "qris_icon",
            "About QRiS",
            self.about_load,
            True,
            "Show Information About QRiS",
        )

        # Create the toolbar button with icon and text
        self.qris_button = QtWidgets.QToolButton(self.toolbar)
        self.qris_button.setText("  QRiS")
        self.qris_button.setIcon(QtGui.QIcon(":/plugins/qris_toolbar/qris_icon"))
        self.qris_button.setToolButtonStyle(TOOL_BTN_TEXT_BESIDE)
        self.qris_button.setMenu(self.qris_menu)
        self.qris_button.setPopupMode(TOOL_BTN_INSTANT_POPUP)  # Clicking anywhere opens the menu

        self.toolbar.addWidget(self.qris_button)

        canvas = self.iface.mapCanvas()
        self.watershed_html_tool = QgsMapToolEmitPoint(canvas)
        self.watershed_html_tool.canvasClicked.connect(self.html_watershed_metrics)

        self.watershed_json_tool = QgsMapToolEmitPoint(canvas)
        self.watershed_json_tool.canvasClicked.connect(self.json_watershed_metrics)

    def add_menu_action(
        self,
        menu: QtWidgets.QMenu,
        icon_name: str,
        label: str,
        callback,
        enabled: bool,
        status_tip: str,
    ):

        action = QtWidgets.QAction(
            QtGui.QIcon(f":/plugins/qris_toolbar/{icon_name}"),
            label,
            self.iface.mainWindow(),
        )
        action.triggered.connect(callback)
        action.setEnabled(enabled)

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

        menu.addAction(action)
        self.actions.append(action)

    def close_project(self):
        if self.dockwidget is not None:
            self.dockwidget.destroy_docwidget()

    def onClosePlugin(self):
        """Cleanup necessary items here when plugin dockwidget is closed.
        This occurs when the user clicks the X button on the top right of
        the dockable widget."""
        try:
            # disconnects
            self.dockwidget.closingPlugin.disconnect(self.onClosePlugin)
            self.close_project()

            # remove this statement if dockwidget is to remain
            # for reuse if plugin is reopened
            # Commented next statement since it causes QGIS crashe
            # when closing the docked window:
            # self.dockwidget = None

            self.pluginIsActive = False
        except Exception as ex:
            Settings().log(f"Error in onClosePlugin: {ex!s}", MESSAGE_LEVEL_CRITICAL)

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

        try:
            self.qproject.cleared.disconnect(self.close_project)
        except Exception:  # nosec B110 - Qt signal disconnect raises if signal was never connected; safe to ignore during plugin unload
            pass
        # Cleanup the main dockable window
        if self.dockwidget is not None:
            self.dockwidget.destroy_docwidget()
            self.iface.removeDockWidget(self.dockwidget)
            self.dockwidget.close()
            self.dockwidget.deleteLater()

        # Need to de-initialize the processing framework
        # QgsApplication.processingRegistry().removeProvider(self.provider)

        for action in self.actions:
            self.iface.removePluginMenu(self.tr("&QGIS Riverscapes Studio (QRiS)"), action)
            self.iface.removeToolBarIcon(action)

        self.dockwidget = None

        # remove the toolbar
        del self.toolbar
        del self.dockwidget

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

    def run(self):
        """Run method that loads and starts the plugin"""

        self.prepare_widget()

        # Only attempt to load the last used project for developers when the special text file is present.
        load_last_project_key = os.path.join(os.path.dirname(__file__), "..", "load_last_project.txt")
        if os.path.isfile(load_last_project_key):
            last_project_folder = Settings().getValue(LAST_PROJECT_FOLDER)
            if last_project_folder is not None and os.path.isdir(last_project_folder):
                # find the project gpkg file in the last project folder only
                project_file = None
                for file in os.listdir(last_project_folder):
                    if file.endswith(".gpkg"):
                        project_file = os.path.join(last_project_folder, file)
                        break
                # project_file = os.path.join(last_project_folder, f'{os.path.basename(last_project_folder)}.gpkg')
                if os.path.isfile(project_file):
                    self.open_qris_project(project_file)

    def prepare_widget(self):
        if not self.pluginIsActive:
            self.pluginIsActive = True

        # Load a version of the QRave code we can use for cross-plugin integration
        if self.toolbar is not None and isinstance(self.toolbar, QtWidgets.QToolBar):
            try:
                self.qrave = QRaveIntegration(self.toolbar)
            except Exception as ex:
                Settings().log(f"Error initializing QRaveIntegration: {ex!s}", MESSAGE_LEVEL_CRITICAL)
        else:
            Settings().log("QRiS toolbar is not valid when initializing QRaveIntegration.", MESSAGE_LEVEL_CRITICAL)

        if self.qrave.name is not None:
            Settings().setValue("symbologyDir", self.qrave.symbology_folders)
            Settings().setValue("protocolsDir", self.qrave.protocol_folder)
            Settings().setValue("climateEngineJson", self.qrave.climate_engine_json)
            Settings().setValue("lookupsJson", self.qrave.lookups_json)
        else:
            Settings().msg_bar(
                "Unable to load Required Riverscapes Viewer plugin. Some functions in QRiS may be disabled, including layer symbology and basemaps.",
                MESSAGE_LEVEL_CRITICAL,
            )
            self.iface.mainWindow().repaint()

        # Get the dockwidget location from the settings
        dock_location = Settings().getValue(DOCK_WIDGET_LOCATION) or default_dock_widget_location
        dock_area = dock_widget_locations[dock_location]

        # Create the dockwidget (after translation) and keep reference
        if self.dockwidget is None:
            self.dockwidget = QRiSDockWidget(self.iface)
            self.dockwidget.closingPlugin.connect(self.onClosePlugin)
            self.iface.addDockWidget(dock_area, self.dockwidget)
            self.dockwidget.qrave = self.qrave
            self.dockwidget.add_basemap_nodes()
        else:
            if self.dockwidget.qrave is None:
                self.dockwidget.qrave = self.qrave
            if self.dockwidget.qris_project is None:
                self.dockwidget.add_basemap_nodes()

        self.dockwidget.show()

        # Tabify QRiS with all other dock widgets in the same area
        main_window = self.iface.mainWindow()
        for dock_widget in main_window.findChildren(QtWidgets.QDockWidget):
            if dock_widget is not self.dockwidget and main_window.dockWidgetArea(dock_widget) == dock_area:
                main_window.tabifyDockWidget(self.dockwidget, dock_widget)

        # Bring QRiS to the front/top
        self.dockwidget.raise_()
        self.dockwidget.setFocus()

    def toggle_widget(self, forceOn=False):

        self.prepare_widget()
        if self.dockwidget.isHidden() and forceOn is True:
            self.dockwidget.show()

    def onProjectLoad(self):
        """Slot for QGIS signal when a QGIS project (QGZ) is being loaded.
        Check if the QGIS project references a QRiS project. Load it if does."""

        qris_project_path = self.get_project_path_settings()
        if qris_project_path is not None and len(qris_project_path) > 0 and os.path.exists(qris_project_path):
            self.open_qris_project(qris_project_path)

    def project_homePathChanged(self):
        """Trigger an event before saving the project so we have an opportunity to corrent the paths"""
        proj_path = self.get_project_path_settings()
        self.set_project_path_settings(proj_path)

    def set_project_path_settings(self, project_file: str):
        """Write the QRiS project filepath to the QgsProject file
        If the destination is a .qgz file then relative paths are used

        Args:
            project_file (str): _description_
        """
        qgs_path = self.qproject.absoluteFilePath()
        if os.path.isdir(os.path.dirname(qgs_path)):
            qgs_path_dir = os.path.dirname(qgs_path)
            # Swap all abspaths for relative ones
            project_file = safe_make_relpath(project_file, qgs_path_dir)

        self.qproject.writeEntry(CONSTANTS["settingsCategory"], CONSTANTS["qris_project_path"], project_file)

    def get_project_path_settings(self) -> str:
        """Fetch the QRiS project filepath from the QgsProject settings
        If it comes in as a relative path it is transformed and returned as an absolute path

        Returns:
            _type_: _description_
        """
        project_file = None
        try:
            project_file, type_conversion_ok = self.qproject.readEntry(CONSTANTS["settingsCategory"], CONSTANTS["qris_project_path"])

            if type_conversion_ok is False:
                project_file = None

        except Exception as e:
            Settings().log(f"Error loading project settings: {e}", MESSAGE_LEVEL_WARNING)

        qgs_project_path = self.qproject.absoluteFilePath()
        if os.path.isfile(qgs_project_path):
            qgs_path_dir = os.path.dirname(qgs_project_path)
            # Change all relative paths back to absolute ones
            project_file = safe_make_abspath(project_file, qgs_path_dir)

        return project_file

    def open_existing_project(self):
        """
        Browse for a project directory
        :return:
        """

        last_project_folder = Settings().getValue(LAST_PROJECT_FOLDER)

        dialog_return = QtWidgets.QFileDialog.getOpenFileName(
            self.dockwidget,
            "Open Existing QRiS Project",
            last_project_folder,
            self.tr("QRiS Project Files (*.gpkg)"),
        )
        if dialog_return is not None and dialog_return[0] != "" and os.path.isfile(dialog_return[0]):
            self.open_qris_project(dialog_return[0])

    def open_qris_project(self, db_path: str):

        if self.dockwidget is not None:
            self.close_project()

        # Disable menu and button
        self.qris_menu.setEnabled(False)
        self.qris_button.setEnabled(False)

        def on_project_loaded(project):

            # Re-enable menu and button
            self.qris_menu.setEnabled(True)
            self.qris_button.setEnabled(True)

            result = check_metadata(db_path)
            if result is False:
                # window dialog ask user if they want to update the metadata
                msg = QtWidgets.QMessageBox()
                msg.setIcon(MSGBOX_ICON_WARNING)
                msg.setText("Metadata Update Recommended")
                msg.setInformativeText("The QRiS project you are attempting to open has an older metadata format. Would you like to update the metadata?")
                msg.setWindowTitle("Metadata Update Recommended")
                msg.setStandardButtons(MSGBOX_YES | MSGBOX_NO)
                msg.setDefaultButton(MSGBOX_YES)
                retval = msg.exec()
                if retval == MSGBOX_YES:
                    update_metadata(db_path)

            self.toggle_widget(forceOn=True)
            self.set_project_path_settings(db_path)
            self.dockwidget.build_tree_view(project)
            self.qrave.qrave_to_qris.connect(self.dockwidget.qris_from_qrave)
            self.add_project_to_mru_list(db_path)
            self.set_map_srs()

            Settings().setValue(LAST_PROJECT_FOLDER, os.path.dirname(db_path))
            try:
                self.qrave.telemetry.send("Load_Project", Settings().getValue(TELEMETRY_ENABLED_KEY))
            except Exception as ex:
                Settings().log(f"Error sending telemetry event for Load_Project: {ex!s}", MESSAGE_LEVEL_WARNING)

        task = LoadProjectTask(db_path, on_project_loaded)
        QgsApplication.taskManager().addTask(task)

    def set_map_srs(self):
        # Set the map canvas to the project SRS
        default_crs = QSettings().value("Projections/layerDefaultCrs")
        default_crs_behavior = QSettings().value("app/projections/newProjectCrsBehavior")
        metadata = self.dockwidget.qris_project.metadata or {}
        system_metadata = metadata.get("system", {}) if isinstance(metadata.get("system", {}), dict) else {}
        project_srs = system_metadata.get("project_srs", None)

        # Parse the project CRS first, if available
        crs = None
        if project_srs is not None:
            project_srs = str(project_srs).strip()
            crs = QgsCoordinateReferenceSystem.fromOgcWmsCrs(project_srs)
            if not crs.isValid():
                crs = QgsCoordinateReferenceSystem()
                crs.createFromUserInput(project_srs)
            if not crs.isValid():
                Settings().log(f'Unable to resolve project_srs "{project_srs}" from project system metadata.', MESSAGE_LEVEL_WARNING)
                crs = None

        # Override QSettings so QGIS's auto-CRS behavior (triggered when the first
        # layer is added to an empty project) does NOT hijack the CRS.  The
        # QgsLayerTreeMapCanvasBridge defers the first-layer CRS matching to run
        # when the event loop next runs, so we must keep these overrides in place
        # until AFTER that deferred logic has fired.
        if crs is not None:
            QSettings().setValue("Projections/layerDefaultCrs", project_srs)
            QSettings().setValue("app/projections/newProjectCrsBehavior", "usePresetCrs")
            self.qproject.setCrs(crs)
            Settings().iface.mapCanvas().setDestinationCrs(crs)

        # Add basemap to ToC if empty
        if len(QgsProject.instance().mapLayers().values()) == 0:
            self.dockwidget.setup_blank_map(trigger_repaint=False)

        # Re-apply the project CRS after the basemap is added, in case auto-CRS
        # still fired despite the pre-set above.
        if crs is not None:
            current_crs = Settings().iface.mapCanvas().mapSettings().destinationCrs()
            if current_crs.authid() != crs.authid():
                self.qproject.setCrs(crs)
                Settings().iface.mapCanvas().setDestinationCrs(crs)
            Settings().iface.mapCanvas().refresh()
            Settings().iface.mapCanvas().refreshAllLayers()
            Settings().msg_bar(
                "Map CRS set",
                f"Map CRS set to {crs.description()}",
                duration=5,
            )

        # Defer QSettings restoration and project bounds zoom to after the event
        # loop has run, so QGIS's deferred first-layer CRS matching and
        # zoomToProjectExtent logic reads "usePresetCrs" and doesn't override
        # our extent.
        QtCore.QTimer.singleShot(0, lambda: self._restore_crs_settings(default_crs, default_crs_behavior, crs))
        QtCore.QTimer.singleShot(0, lambda: self.zoom_to_project_bounds())

    def _restore_crs_settings(self, default_crs, default_crs_behavior, project_crs):
        """Restore QSettings and re-apply project CRS if needed."""
        QSettings().setValue("Projections/layerDefaultCrs", default_crs)
        QSettings().setValue("app/projections/newProjectCrsBehavior", default_crs_behavior)
        if project_crs is not None:
            current_crs = Settings().iface.mapCanvas().mapSettings().destinationCrs()
            if current_crs.authid() != project_crs.authid():
                self.qproject.setCrs(project_crs)
                Settings().iface.mapCanvas().setDestinationCrs(project_crs)
                Settings().iface.mapCanvas().refresh()
                Settings().iface.mapCanvas().refreshAllLayers()

    def zoom_to_project_bounds(self):
        """Zoom to the extent of the project bounds AOI/Sample Frame, if one is set."""
        project = self.dockwidget.qris_project
        if project is None:
            return

        # Search all AOIs, sample frames, and valley bottoms for one with project_bounds=True
        bounds_item = None
        for item in list(project.aois.values()) + list(project.sample_frames.values()) + list(project.valley_bottoms.values()):
            if item.system_metadata.get("project_bounds", False) is True:
                bounds_item = item
                break

        if bounds_item is None:
            return

        # Load a temporary layer from the project GeoPackage to get the extent
        temp_layer = bounds_item.get_temp_layer(project.project_file)
        if temp_layer is None or not temp_layer.isValid() or temp_layer.featureCount() == 0:
            return

        extent = temp_layer.extent()
        if extent.isNull():
            return

        # Transform extent to the canvas CRS if needed
        canvas_crs = Settings().iface.mapCanvas().mapSettings().destinationCrs()
        layer_crs = temp_layer.crs()
        if layer_crs.authid() != canvas_crs.authid():
            transform = QgsCoordinateTransform(layer_crs, canvas_crs, QgsProject.instance())
            try:
                extent = transform.transformBoundingBox(extent)
            except Exception:
                pass

        # Buffer by 10% so the bounds aren't right at the edge
        extent = extent.buffered(extent.width() * 0.1)
        Settings().iface.mapCanvas().setExtent(extent)
        Settings().iface.mapCanvas().refresh()

    def load_mru_projects(self):

        mrus = Settings().getValue(RECENT_PROJECT_LIST) or []
        self.mru_menu.clear()
        self.mru_actions = []
        for mru in mrus:
            if os.path.exists(mru):
                self.add_menu_action(
                    self.mru_menu,
                    "qris_icon",
                    mru,
                    (lambda _checked=False, path=mru: self.open_qris_project(path)),
                    True,
                    "",
                )

    def add_project_to_mru_list(self, db_path: str):

        mrus = Settings().getValue(RECENT_PROJECT_LIST) or []
        if db_path in mrus:
            mrus.remove(db_path)
        if os.path.exists(db_path):
            mrus.insert(0, db_path)
        Settings().setValue(RECENT_PROJECT_LIST, mrus[:10])
        self.load_mru_projects()

    def create_new_project_dialog(self):

        last_parent_folder = os.path.dirname(Settings().getValue(LAST_PROJECT_FOLDER)) if Settings().getValue(LAST_PROJECT_FOLDER) is not None else None

        self.frm_new_project = FrmNewProject(self.iface.mainWindow(), last_parent_folder)
        self.frm_new_project.newProjectComplete.connect(self.on_new_project_complete)
        self.frm_new_project.exec()

    def on_new_project_complete(self, project_dir: str, db_path: str):

        self.open_qris_project(db_path)

    def activate_html_watershed_attributes(self):

        canvas = self.iface.mapCanvas()
        canvas.setMapTool(self.watershed_html_tool)

    def activate_json_watershed_attributes(self):

        canvas = self.iface.mapCanvas()
        canvas.setMapTool(self.watershed_json_tool)

    def json_watershed_metrics(self, point, button):
        """
        Display the watershed attribute results based on the point that the user clicked on the map
        """

        try:
            # The point is in the map data frame display units. Transform to WGS84
            transformed_point = self.transform_geometry(point, 4326)
            long_task = WatershedAttributes(transformed_point.y(), transformed_point.x(), False)
            long_task.process_complete.connect(self.on_watershed_attributes_complete)

            # Call the run command directly during development to run the process synchronousely.
            # DO NOT DEPLOY WITH run() UNCOMMENTED
            # long_task.run()

            # Call the addTask() method to run the process asynchronously. Deploy with this method uncommented.
            QgsApplication.taskManager().addTask(long_task)

        except Exception as ex:
            QtWidgets.QMessageBox.warning(None, "Error Retrieving Watershed Metrics", str(ex))

    def html_watershed_metrics(self, point, button):
        """
        Display the watershed attribute results based on the point that the user clicked on the map
        """

        try:
            # The point is in the map data frame display units. Transform to WGS84
            transformed_point = self.transform_geometry(point, 4326)
            long_task = WatershedAttributes(transformed_point.y(), transformed_point.x(), True)
            long_task.process_complete.connect(self.on_watershed_attributes_complete)

            # Call the run command directly during development to run the process synchronousely.
            # DO NOT DEPLOY WITH run() UNCOMMENTED
            # long_task.run()

            # Call the addTask() method to run the process asynchronously. Deploy with this method uncommented.
            QgsApplication.taskManager().addTask(long_task)

        except Exception as ex:
            QtWidgets.QMessageBox.warning(None, "Error Retrieving Watershed Metrics", str(ex))

    @pyqtSlot(str, bool)
    def on_watershed_attributes_complete(self, output_path: str, result: bool) -> None:

        if result:
            Settings().msg_bar("Watershed Attributes Complete.", f"Outputs at {output_path}")
        else:
            Settings().msg_bar("Watershed Attributes Error.", "Check the QGIS log for details.", MESSAGE_LEVEL_CRITICAL)

    def configure_watershed_attribute_menu(self):

        self.wat_button = QtWidgets.QToolButton()
        self.wat_button.setToolButtonStyle(TOOL_BTN_ICON_ONLY)
        self.wat_button.setIcon(QtGui.QIcon(":/plugins/qris_toolbar/watershed"))
        self.wat_button.setMenu(QtWidgets.QMenu())
        self.wat_button.setPopupMode(TOOL_BTN_MENU_POPUP)
        self.toolbar.addWidget(self.wat_button)
        self.wat_menu = self.wat_button.menu()

        self.wat_button = QtWidgets.QToolButton()
        self.wat_button.setToolButtonStyle(TOOL_BTN_TEXT_ONLY)
        self.wat_button.setMenu(self.wat_menu)
        self.wat_button.setPopupMode(TOOL_BTN_MENU_POPUP)

        self.wat_html_action = QtWidgets.QAction(
            QtGui.QIcon(":/plugins/qris_toolbar/watershed"),
            self.tr("Export Attributes to HTML Report"),
            self.iface.mainWindow(),
        )
        self.wat_html_action.triggered.connect(self.activate_html_watershed_attributes)
        self.wat_menu.addAction(self.wat_html_action)

        self.wat_json_action = QtWidgets.QAction(
            QtGui.QIcon(":/plugins/qris_toolbar/json"),
            self.tr("Export Attributes to JSON"),
            self.iface.mainWindow(),
        )
        self.wat_json_action.triggered.connect(self.activate_json_watershed_attributes)
        self.wat_menu.addAction(self.wat_json_action)

        self.wat_button.setDefaultAction(self.wat_html_action)

    def about_load(self):

        self.frm_about = FrmAboutDialog(self.iface.mainWindow())
        self.frm_about.exec()
        self.frm_about = None

    def show_settings(self):

        qris_project = None
        if self.dockwidget is not None:
            qris_project = self.dockwidget.qris_project

        self.settings_dialog = FrmSettings(qris_project)
        self.settings_dialog.exec()
        self.settings_dialog = None

    def transform_geometry(self, geometry, output_epsg: int):

        source_crs = self.iface.mapCanvas().mapSettings().destinationCrs()
        dest_crs = QgsCoordinateReferenceSystem.fromEpsgId(output_epsg)
        transform = QgsCoordinateTransform(source_crs, dest_crs, QgsProject.instance().transformContext())
        return transform.transform(geometry)
