# /***************************************************************************
#  SecInterp
#                                  A QGIS plugin
#  Data extraction for geological interpretation.
#  Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/.
#                               -------------------
#         begin                : 2025-11-15
#         git sha              : $Format:%H$
#         copyright            : (C) 2025 by Juan M Bernales
#         email                : juanbernales@gmail.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.                                   *
#  *                                                                         *
#  ***************************************************************************/

"""Main Plugin Module.

Orchestrates the lifecycle of the SecInterp QGIS plugin.
"""

from __future__ import annotations

import contextlib
from pathlib import Path
from typing import Optional

from qgis.core import (
    QgsMapLayer,
    QgsProject,
)
from qgis.PyQt.QtCore import (
    QCoreApplication,
    QSettings,
    QTranslator,
)
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction, QDialogButtonBox

from sec_interp.core.controller import ProfileController
from sec_interp.core.exceptions import SecInterpError
from sec_interp.core.services.export_service import ExportService
from sec_interp.core.types import PreviewParams
from sec_interp.gui.main_dialog import SecInterpDialog
from sec_interp.gui.preview_renderer import PreviewRenderer
from sec_interp.gui.utils import show_user_message
from sec_interp.logger_config import get_logger


logger = get_logger(__name__)


# DataCache has been moved to core/data_cache.py


class SecInterp:
    """QGIS Plugin Implementation for Geological Data Extraction.

    This class implements the main logic of the SecInterp plugin, handling
    initialization, UI integration, and orchestration of data processing tasks.
    It connects the QGIS interface with the plugin's dialog and processing algorithms.
    """

    def __init__(self, iface):
        """Initialize the plugin.

        Args:
            iface (QgsInterface): 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.

        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = Path(__file__).resolve().parent
        # initialize locale
        user_locale = QSettings().value("locale/userLocale", "en")

        # Try full locale first (e.g., pt_BR)
        locale_path = self.plugin_dir / f"i18n/SecInterp_{user_locale}.qm"

        if not locale_path.exists() and user_locale and len(user_locale) > 2:
            # Fallback to language code (e.g., pt)
            locale_short = user_locale[0:2]
            locale_path = self.plugin_dir / f"i18n/SecInterp_{locale_short}.qm"

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

        # Prepare core services BEFORE the dialog (required by PreviewManager)
        # Create preview renderer
        self.preview_renderer = PreviewRenderer()
        # Initialize controller
        self.controller = ProfileController()
        # Initialize export service
        self.export_service = ExportService(self.controller)

        # Create the dialog (after services and translation) and keep reference
        self.dlg = SecInterpDialog(self.iface, self)
        self.dlg.plugin_instance = self

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr("&Sec Interp")
        # Create custom toolbar and make it visible
        self.toolbar = self.iface.addToolBar(self.tr("Sec Interp"))
        self.toolbar.setObjectName("SecInterp")
        self.toolbar.setVisible(True)  # Ensure toolbar is visible

        # 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

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

        We implement this ourselves since we do not inherit QObject.

        Args:
            message (str): String for translation.

        Returns:
            str: Translated string (or original if no translation found).

        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate("SecInterp", 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,
    ):
        """Add a toolbar icon to the toolbar.

        Args:
            icon_path (str): Path to the icon for this action.
            text (str): Text that should be shown in menu items for this action.
            callback (function): Function to be called when the action is triggered.
            enabled_flag (bool): A flag indicating if the action should be enabled
                by default. Defaults to True.
            add_to_menu (bool): Flag indicating whether the action should also
                be added to the menu. Defaults to True.
            add_to_toolbar (bool): Flag indicating whether the action should also
                be added to the toolbar. Defaults to True.
            status_tip (str): Optional text to show in a popup when mouse pointer
                hovers over the action.
            whats_this (str): Optional text to show in the status bar when the
                mouse pointer hovers over the action.
            parent (QWidget): Parent widget for the new action. Defaults None.

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

        """
        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:
            # Add to custom toolbar
            self.toolbar.addAction(action)
            # Also add to Plugins toolbar for visibility
            self.iface.addToolBarIcon(action)

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

        self.actions.append(action)

        return action

    def initGui(self):  # noqa: N802
        """Create the menu entries and toolbar icons inside the QGIS GUI."""
        # Use absolute path to icon file to ensure it loads in toolbar
        icon_path = str(self.plugin_dir / "icon.png")
        self.add_action(
            icon_path,
            text=self.tr("Geological data extraction"),
            callback=self.run,
            parent=self.iface.mainWindow(),
        )

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

    def unload(self):
        """Remove the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(self.tr("&Sec Interp"), action)
            self.iface.removeToolBarIcon(action)

        # Remove custom toolbar
        if self.toolbar:
            del self.toolbar
            self.toolbar = None

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

        This method initializes the dialog, connects signals and slots,
        and executes the main event loop for the plugin 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 is True:
            self.first_start = False
            self.dlg = SecInterpDialog(self.iface, self)
            # Update preview renderer with the new dialog's canvas
            self.preview_renderer.canvas = self.dlg.preview_widget.canvas

            # Disconnect default accepted signal once
            with contextlib.suppress(TypeError):
                self.dlg.button_box.accepted.disconnect()

            # Connect OK button to process and close
            self.dlg.button_box.button(QDialogButtonBox.Ok).clicked.connect(self.process_data)
            self.dlg.button_box.button(QDialogButtonBox.Ok).clicked.connect(self.dlg.accept)

            # Connect Save button to save only
            self.dlg.button_box.button(QDialogButtonBox.Save).clicked.connect(
                self.save_profile_line
            )

        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # Do something useful here - delete the line containing pass and
            # substitute with your code.
            pass

    def _resolve_layer_obj(self, value, placeholder_text=""):
        """Resolve layer object from UI value."""
        if isinstance(value, QgsMapLayer):
            return value
        if isinstance(value, str) and value:
            if placeholder_text and value == placeholder_text:
                return None
            # Try resolving by ID first (most robust)
            layer = QgsProject.instance().mapLayer(value)
            if not layer:
                # Fallback to name search
                for lyr in QgsProject.instance().mapLayers().values():
                    if lyr.name() == value:
                        layer = lyr
                        break
            return layer
        return None

    def _get_and_validate_inputs(self) -> PreviewParams | None:
        """Retrieve and validate inputs from the dialog.

        Returns:
            PreviewParams: Validated parameters, or None if validation fails.

        """
        # Get values from the dialog pages
        values = self.dlg.get_selected_values()
        # Get preview/LOD options from the preview widget
        preview_options = self.dlg.get_preview_options()

        # 1. Resolve Layer Objects
        raster_layer = self._resolve_layer_obj(
            values.get("raster_layer"), self.tr("Select a raster layer")
        )
        line_layer = self._resolve_layer_obj(
            values.get("crossline_layer"), self.tr("Select a crossline layer")
        )
        outcrop_layer = self._resolve_layer_obj(values.get("outcrop_layer"))
        structural_layer = self._resolve_layer_obj(values.get("structural_layer"))

        # 2. Build PreviewParams
        try:
            params = PreviewParams(
                raster_layer=raster_layer,
                line_layer=line_layer,
                band_num=values.get("selected_band", 1),
                buffer_dist=values.get("buffer_distance", 100.0),
                outcrop_layer=outcrop_layer,
                outcrop_name_field=values.get("outcrop_name_field"),
                struct_layer=structural_layer,
                dip_field=values.get("dip_field"),
                strike_field=values.get("strike_field"),
                dip_scale_factor=values.get("dip_scale_factor", 1.0),
                collar_layer=self._resolve_layer_obj(values.get("collar_layer_obj")),
                collar_id_field=values.get("collar_id_field"),
                collar_use_geometry=values.get("collar_use_geometry", True),
                collar_x_field=values.get("collar_x_field"),
                collar_y_field=values.get("collar_y_field"),
                collar_z_field=values.get("collar_z_field"),
                collar_depth_field=values.get("collar_depth_field"),
                survey_layer=self._resolve_layer_obj(values.get("survey_layer_obj")),
                survey_id_field=values.get("survey_id_field"),
                survey_depth_field=values.get("survey_depth_field"),
                survey_azim_field=values.get("survey_azim_field"),
                survey_incl_field=values.get("survey_incl_field"),
                interval_layer=self._resolve_layer_obj(values.get("interval_layer_obj")),
                interval_id_field=values.get("interval_id_field"),
                interval_from_field=values.get("interval_from_field"),
                interval_to_field=values.get("interval_to_field"),
                interval_lith_field=values.get("interval_lith_field"),
                max_points=preview_options.get("max_points", 1000),
                auto_lod=preview_options.get("auto_lod", True),
                canvas_width=self.dlg.preview_widget.canvas.width(),
            )
            # Internal Native Validation
            params.validate()
        except SecInterpError as e:
            self.dlg.handle_error(e, self.tr("Configuration Error"))
            return None
        except Exception as e:
            self.dlg.handle_error(e, self.tr("Unexpected Error"))
            return None

        # 3. Connect Layer Notifications for Cache Invalidation
        active_layers = [
            l
            for l in [
                params.raster_layer,
                params.line_layer,
                params.outcrop_layer,
                params.struct_layer,
                params.collar_layer,
                params.survey_layer,
                params.interval_layer,
            ]
            if l
        ]
        self.controller.connect_layer_notifications(active_layers)

        # Store output path temporarily or return as tuple if needed
        # but for now we only return params. Callers need the path separately.
        return params

    def process_data(self, inputs: dict | None = None) -> tuple | None:
        """Process profile data by delegating to the dialog's preview manager.

        Args:
            inputs: Pre-validated inputs (optional)

        Returns:
            Tuple of (profile_data, geol_data, struct_data) or None

        """
        # Simply delegate to the dialog's preview manager if it exists
        if hasattr(self, "dlg") and self.dlg:
            success, message = self.dlg.preview_manager.generate_preview()
            if not success:
                logger.warning(f"Data processing failed: {message}")
                return None

            # Return cached data from manager for backward compatibility if needed
            cache = self.dlg.preview_manager.cached_data
            return cache["topo"], cache["geol"], cache["struct"]

        return None

    def save_profile_line(self):
        """Save profile data by delegating to the dialog's export manager."""
        if hasattr(self, "dlg") and self.dlg:
            self.dlg.export_manager.export_data()

    def draw_preview(
        self,
        topo_data: list,
        geol_data: list | None = None,
        struct_data: list | None = None,
        drillhole_data: list | None = None,
        max_points: int = 1000,
        **kwargs,
    ) -> None:
        """Draw enhanced interactive preview using native PyQGIS renderer.

        Args:
            topo_data: List of (dist, elev) tuples for topographic profile
            geol_data: Optional list of (dist, elev, geology_name) tuples
            struct_data: Optional list of (dist, app_dip) tuples
            drillhole_data: Optional list of (hole_id, traces, segments) tuples
            max_points (int): Maximum number of points for simplified preview (LOD)
            **kwargs: Additional arguments passed to renderer (e.g. preserve_extent)

        """
        logger.debug("draw_preview called with:")
        logger.debug("  - topo_data: %d points", len(topo_data) if topo_data else 0)
        logger.debug("  - geol_data: %d points", len(geol_data) if geol_data else 0)
        logger.debug("  - struct_data: %d points", len(struct_data) if struct_data else 0)
        logger.debug("  - drillhole_data: %d holes", len(drillhole_data) if drillhole_data else 0)
        logger.debug("  - max_points: %d", max_points)

        # Store data in dialog for re-rendering when checkboxes change
        self.dlg.current_topo_data = topo_data
        self.dlg.current_geol_data = geol_data
        self.dlg.current_struct_data = struct_data
        self.dlg.current_drillhole_data = drillhole_data

        # Get preview options from dialog checkboxes
        options = self.dlg.get_preview_options()
        logger.debug("Preview options: %s", options)

        # Filter data based on checkbox states
        filtered_topo = topo_data if options.get("show_topo", True) else None
        filtered_geol = geol_data if options.get("show_geol", True) else None
        filtered_struct = struct_data if options.get("show_struct", True) else None
        filtered_drill = drillhole_data if options.get("show_drillholes", True) else None
        filtered_interp = (
            self.dlg.interpretations if options.get("show_interpretations", True) else None
        )

        logger.debug("Filtered data:")
        logger.debug("  - filtered_topo: %d points", len(filtered_topo) if filtered_topo else 0)
        logger.debug("  - filtered_geol: %d points", len(filtered_geol) if filtered_geol else 0)
        logger.debug(
            "  - filtered_struct: %d points",
            len(filtered_struct) if filtered_struct else 0,
        )
        logger.debug(
            "  - filtered_interp: %d polygons",
            len(filtered_interp) if filtered_interp else 0,
        )

        # Get vertical exaggeration from dialog

        # Use new numeric method or direct accessor from Page
        vert_exag = self.dlg.page_dem.vertexag_spin.value()

        logger.debug("Vertical exaggeration: %.2f", vert_exag)

        # Calculate dip line length based on scale factor and raster resolution
        dip_line_length = None
        if filtered_struct:
            dip_scale = self.dlg.page_struct.scale_spin.value()

            if dip_scale > 0:
                raster_layer = self.dlg.page_dem.raster_combo.currentLayer()
                if raster_layer and raster_layer.isValid():
                    res = raster_layer.rasterUnitsPerPixelX()
                    if res > 0:
                        dip_line_length = res * dip_scale
            logger.debug("Dip line length: %s (scale: %.2f)", dip_line_length, dip_scale)

        # Render using native PyQGIS
        canvas, layers = self.preview_renderer.render(
            topo_data=filtered_topo,
            geol_data=filtered_geol,
            struct_data=filtered_struct,
            vert_exag=vert_exag,
            dip_line_length=dip_line_length,
            max_points=max_points,
            preserve_extent=kwargs.get("preserve_extent", False),
            drillhole_data=filtered_drill,
            interp_data=filtered_interp,
        )

        # Store canvas and layers for export
        self.dlg.current_canvas = canvas
        self.dlg.current_layers = layers

        # Update legend
        if hasattr(self.dlg, "legend_widget"):
            self.dlg.legend_widget.update_legend(self.preview_renderer)

        logger.debug("Preview rendered with %d layers", len(layers) if layers else 0)
