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

from qgis.core import QgsApplication
from qgis.gui import QgisInterface
from qgis.PyQt import QtCore, QtGui, QtWidgets

from ..compat import DLG_ACCEPTED, DOCK_CLOSABLE, DOCK_FLOATABLE, DOCK_MOVABLE, MESSAGE_LEVEL_SUCCESS, MESSAGE_LEVEL_WARNING, SPSZ_EXPANDING, SPSZ_FIXED, SPSZ_MINIMUM, USER_ROLE
from ..gp.analysis_metrics_task import AnalysisMetricsTask
from ..model.analysis import Analysis
from ..model.db_item import DBItemModel
from ..model.event import AS_BUILT_EVENT_TYPE_ID, DCE_EVENT_TYPE_ID, DESIGN_EVENT_TYPE_ID, Event
from ..model.project import Project
from ..model.sample_frame import get_sample_frame_ids
from ..QRiS.settings import Settings
from .frm_analysis_explorer import FrmAnalysisExplorer
from .frm_analysis_properties import FrmAnalysisProperties
from .frm_export_metrics import FrmExportMetrics
from .frm_metric_calculate_all import FrmCalculateAllMetrics
from .frm_metric_value import FrmMetricValue
from .utilities import add_help_button
from .widgets.analysis_table import AnalysisTable


class FrmAnalysisDocWidget(QtWidgets.QDockWidget):
    closing = QtCore.pyqtSignal()

    def __init__(self, parent, iface: QgisInterface):

        super().__init__(parent)
        self.iface = iface
        self.setFeatures(DOCK_CLOSABLE | DOCK_MOVABLE | DOCK_FLOATABLE)
        # Store the connections so they can be disconnected when the form is closed
        self.connections = {}
        self.setupUi()
        self.resize(500, 1000)

        self.frmMetricValue = None
        self.metrics_task = None
        self.metrics_task_context = None
        self._calculate_button_default_text = "Calculate..."

        self.destroyed.connect(self.cleanup_connections)

    def configure_analysis(self, project: Project, analysis: Analysis, event: Event):

        self.qris_project = project
        self.analysis = analysis
        self.txtName.setText(analysis.name)

        # Set Sample Frames
        frame_ids = get_sample_frame_ids(self.qris_project.project_file, self.analysis.sample_frame.id)
        self.segments_model = DBItemModel(frame_ids)
        self.cboSampleFrame.setModel(self.segments_model)

        # Events
        valid_event_types = [DCE_EVENT_TYPE_ID, DESIGN_EVENT_TYPE_ID, AS_BUILT_EVENT_TYPE_ID]
        events = {event_id: event for event_id, event in self.qris_project.events.items() if event.event_type.id in valid_event_types}

        # Hide event controls for intrinsic analyses; no events to select.
        if analysis.is_simple_intrinsic_mode():
            self.lblEvent.setVisible(False)
            self.cboEvent.setVisible(False)
        else:
            # Filter events if selected_events metadata exists, but only keep those that still exist
            selected_event_ids = self.analysis.metadata.get("selected_events")
            if selected_event_ids:
                valid_event_ids = [eid for eid in selected_event_ids if eid in events]
                events = {eid: evt for eid, evt in events.items() if eid in valid_event_ids}
                # Optionally, update the analysis metadata to remove missing event IDs
                if len(valid_event_ids) != len(selected_event_ids):
                    self.analysis.metadata["selected_events"] = valid_event_ids

        self.events_model = DBItemModel(events)
        self.cboEvent.setModel(self.events_model)

        # Configure the table
        self.table.configure(project, analysis)
        self.table.set_filter_tools_visible(not analysis.is_simple_intrinsic_mode())
        self.table.build_table()  # Default to all metrics
        self.load_table_values()

    def build_table(self):
        # Delegate to table widget
        self.table.build_table()  # Default, internal state managed by table now
        self.load_table_values()

    def load_table_values(self):
        # Delegate to table widget. Intrinsic analyses pass event=None.
        event = None if self.analysis.is_simple_intrinsic_mode() else self.cboEvent.currentData(USER_ROLE)
        mask_feature_id = self.cboSampleFrame.currentData(USER_ROLE).id
        self.table.load_values(event, mask_feature_id)

    # set_status method is removed as it is now in AnalysisTable

    def cmdCalculate_clicked(self):

        frm = FrmCalculateAllMetrics(self)
        result = frm.exec()

        if result == DLG_ACCEPTED:
            sample_frame_features = [self.cboSampleFrame.itemData(i, USER_ROLE) for i in range(self.cboSampleFrame.count())] if frm.rdoAllSF.isChecked() else [self.cboSampleFrame.currentData(USER_ROLE)]
            data_capture_events = [self.cboEvent.itemData(i, USER_ROLE) for i in range(self.cboEvent.count())] if frm.rdoAllDCE.isChecked() else [self.cboEvent.currentData(USER_ROLE)]
            sample_frame_ids = [sf.id for sf in sample_frame_features if sf is not None]
            event_ids = [event.id for event in data_capture_events if event is not None]
            metric_ids = list(self.analysis.analysis_metrics.keys())

            if len(sample_frame_ids) < 1 or (len(event_ids) < 1 and not self.analysis.is_simple_intrinsic_mode()) or len(metric_ids) < 1:
                Settings().msg_bar("Metrics", "Nothing selected to calculate.", MESSAGE_LEVEL_WARNING)
                return

            if self.metrics_task is not None:
                Settings().msg_bar("Metrics", "A metric calculation task is already running.", MESSAGE_LEVEL_WARNING)
                return

            self.metrics_task = AnalysisMetricsTask(
                self.qris_project,
                self.analysis,
                sample_frame_ids,
                event_ids,
                metric_ids,
                overwrite_existing=frm.chkOverwrite.isChecked(),
                force_active=frm.chkForceActive.isChecked(),
            )
            self.metrics_task_context = {"mode": "bulk"}
            self.metrics_task.on_complete.connect(self.on_metrics_task_complete)
            self.metrics_task.progressChanged.connect(self.on_metrics_task_progress)

            self.cmdCalculate.setEnabled(False)
            self.cmdCalculate.setText("Calculating... 0%")
            Settings().msg_bar("Metrics", "Metric calculation started in the background.")
            QgsApplication.taskManager().addTask(self.metrics_task)

    def on_metrics_task_progress(self, progress: float):
        if self.metrics_task is not None and (self.metrics_task_context or {}).get("mode") == "bulk":
            self.cmdCalculate.setText(f"Calculating... {int(progress)}%")

    def on_metrics_task_complete(self, summary: dict):

        for message in summary.get("messages", []):
            Settings().log(message["text"], message["level"])

        task_context = self.metrics_task_context or {"mode": "bulk"}
        self.metrics_task_context = None
        self.metrics_task = None
        self.cmdCalculate.setEnabled(True)
        self.cmdCalculate.setText(self._calculate_button_default_text)

        if summary.get("canceled", False):
            Settings().msg_bar("Metrics", "Metric calculation canceled.", MESSAGE_LEVEL_WARNING)
            self.load_table_values()
            return

        if summary.get("exception", None) is not None:
            Settings().msg_bar("Metrics", "Metric calculation ended with an error. See log for details.", MESSAGE_LEVEL_WARNING)
            self.load_table_values()
            return

        if task_context.get("mode") == "single":
            has_missing = summary.get("missing_data", 0) > 0
            has_errors = summary.get("errors", 0) > 0
            if not has_missing and not has_errors:
                Settings().msg_bar("Metrics", "Metric successfully calculated.", MESSAGE_LEVEL_SUCCESS)
            else:
                if has_missing:
                    Settings().msg_bar("Metrics", "Metric was not calculated due to missing data requirements. See log for details.", MESSAGE_LEVEL_WARNING)
                if has_errors:
                    Settings().msg_bar("Metrics", "Metric was not calculated due to a processing error. See log for details.", MESSAGE_LEVEL_WARNING)
            self.load_table_values()
            return

        has_missing = summary.get("missing_data", 0) > 0
        has_errors = summary.get("errors", 0) > 0
        if not has_missing and not has_errors:
            Settings().msg_bar("Metrics", "All metrics successfully calculated.", MESSAGE_LEVEL_SUCCESS)
        else:
            if has_missing:
                Settings().msg_bar("Metrics", "One or more metrics were not calculated due to missing data requirements. See log for details.", MESSAGE_LEVEL_WARNING)
            if has_errors:
                Settings().msg_bar("Metrics", "One or more metrics were not calculated due to processing error(s). See log for details.", MESSAGE_LEVEL_WARNING)

        # Metrics task writes directly to SQLite; notify project change so WAL is checkpointed to the gpkg main file.
        self.qris_project.project_changed.emit()
        self.load_table_values()

    def export_table(self):

        # open modal dialog to select export file
        current_sample_frame = self.cboSampleFrame.currentData(USER_ROLE)
        current_data_capture_event = None if self.analysis.is_simple_intrinsic_mode() else self.cboEvent.currentData(USER_ROLE)
        frm = FrmExportMetrics(self, self.iface, self.qris_project, self.analysis, current_data_capture_event, current_sample_frame)
        frm.exec()

    def cmdProperties_clicked(self):

        frm = FrmAnalysisProperties(self, self.qris_project, self.analysis)
        result = frm.exec()
        if result is not None and result != 0:
            self.txtName.setText(frm.analysis.name)
            self.configure_analysis(self.qris_project, frm.analysis, None)
            self.build_table()

    def edit_metric_value(self, metric_value):

        event = None if self.analysis.is_simple_intrinsic_mode() else self.cboEvent.currentData(USER_ROLE)
        mask_feature = self.cboSampleFrame.currentData(USER_ROLE)

        frm = FrmMetricValue(self, self.qris_project, self.analysis, event, mask_feature.id, metric_value)
        result = frm.exec()
        if result is not None and result != 0:
            self.load_table_values()

    def calculate_metric_value(self, analysis_metric, metric_value):

        event = self.cboEvent.currentData(USER_ROLE)
        mask_feature = self.cboSampleFrame.currentData(USER_ROLE)

        # Intrinsic analyses have no real events; pass a synthetic event_id=0.
        is_intrinsic = self.analysis.is_simple_intrinsic_mode()
        if (event is None and not is_intrinsic) or mask_feature is None:
            return

        metric = analysis_metric.metric
        if metric.metric_function is None:
            return

        if self.metrics_task is not None:
            Settings().msg_bar("Metrics", "A metric calculation task is already running.", MESSAGE_LEVEL_WARNING)
            return

        self.metrics_task = AnalysisMetricsTask(
            self.qris_project,
            self.analysis,
            [mask_feature.id],
            [event.id] if not is_intrinsic else [],
            [metric.id],
            overwrite_existing=True,
            force_active=True,
        )
        self.metrics_task_context = {"mode": "single", "metric_id": metric.id}
        self.metrics_task.on_complete.connect(self.on_metrics_task_complete)
        self.metrics_task.progressChanged.connect(self.on_metrics_task_progress)

        self.cmdCalculate.setEnabled(False)
        QgsApplication.taskManager().addTask(self.metrics_task)

    def resizeEvent(self, event):
        try:
            if hasattr(self, "table") and isinstance(self.table, QtWidgets.QTableWidget):
                # Dynamically size the Metric column (index 1) to 80% of width
                if self.table.columnWidth(1) != int(self.table.width() * 0.8):
                    self.table.setColumnWidth(1, int(self.table.width() * 0.8))
        except RuntimeError:
            # This can happen if the widget has been deleted during undock
            pass
        super().resizeEvent(event)

    def closeEvent(self, event):
        if self.metrics_task is not None:
            self.metrics_task.cancel()
        self.closing.emit()
        for signal in list(self.connections.keys()):
            signal.disconnect(self.connections.pop(signal))
        QtWidgets.QDockWidget.closeEvent(self, event)

    def cleanup_connections(self, *args):
        for signal in list(self.connections.keys()):
            try:
                signal.disconnect(self.connections.pop(signal))
            except Exception:  # nosec B110 - Qt signal disconnect raises if already disconnected; safe to ignore during cleanup
                pass

    def setupUi(self):

        self.setWindowTitle("QRiS Analysis")
        self.dockWidgetContents = QtWidgets.QWidget(self)

        # Top level layout must include parent. Widgets added to this layout do not need parent.
        self.vert = QtWidgets.QVBoxLayout(self.dockWidgetContents)

        self.grid = QtWidgets.QGridLayout()
        self.vert.addLayout(self.grid)

        self.lblName = QtWidgets.QLabel("Analysis")
        self.grid.addWidget(self.lblName, 0, 0, 1, 1)

        self.horizName = QtWidgets.QHBoxLayout()
        self.grid.addLayout(self.horizName, 0, 1, 1, 1)

        self.txtName = QtWidgets.QLineEdit()
        self.txtName.setMaxLength(255)
        self.txtName.setReadOnly(True)
        self.horizName.addWidget(self.txtName)

        self.cmdProperties = QtWidgets.QPushButton("Properties")
        self.connections[self.cmdProperties.clicked] = self.cmdProperties.clicked.connect(self.cmdProperties_clicked)
        self.cmdProperties.setToolTip("Edit analysis properties, indcluding metrics and indicators")
        self.cmdProperties.setToolTipDuration(2000)
        self.horizName.addWidget(self.cmdProperties)

        self.cmdOpenSummary = QtWidgets.QPushButton()
        icon = QtGui.QIcon(":/plugins/qris_toolbar/analysis_summary")
        self.cmdOpenSummary.setIcon(icon)
        self.cmdOpenSummary.setToolTip("Open Analysis Summary")
        self.cmdOpenSummary.setToolTipDuration(2000)
        self.connections[self.cmdOpenSummary.clicked] = self.cmdOpenSummary.clicked.connect(lambda: FrmAnalysisExplorer(self, self.qris_project, self.analysis.id).exec())
        self.horizName.addWidget(self.cmdOpenSummary)

        self.lblEvent = QtWidgets.QLabel("Data Capture Event")
        self.grid.addWidget(self.lblEvent, 1, 0, 1, 1)

        self.horizEvent = QtWidgets.QHBoxLayout()
        self.grid.addLayout(self.horizEvent, 1, 1, 1, 1)

        self.cboEvent = QtWidgets.QComboBox()
        self.connections[self.cboEvent.currentIndexChanged] = self.cboEvent.currentIndexChanged.connect(self.load_table_values)
        self.horizEvent.addWidget(self.cboEvent)

        self.cmdCalculate = QtWidgets.QPushButton("Calculate...")
        self.connections[self.cmdCalculate.clicked] = self.cmdCalculate.clicked.connect(self.cmdCalculate_clicked)
        self.cmdCalculate.setSizePolicy(SPSZ_FIXED, SPSZ_FIXED)
        self.cmdCalculate.setToolTip("Options to calculate metrics and indicators for the selected event and sample frame")
        self.cmdCalculate.setToolTipDuration(2000)
        self.horizEvent.addWidget(self.cmdCalculate, 0)

        self.lblSegment = QtWidgets.QLabel("Mask Polygon")
        self.grid.addWidget(self.lblSegment, 2, 0, 1, 1)

        self.cboSampleFrame = QtWidgets.QComboBox()
        self.connections[self.cboSampleFrame.currentIndexChanged] = self.cboSampleFrame.currentIndexChanged.connect(self.load_table_values)
        self.grid.addWidget(self.cboSampleFrame, 2, 1, 1, 1)

        self.horiz = QtWidgets.QHBoxLayout()
        self.vert.addLayout(self.horiz)

        self.spacerItem = QtWidgets.QSpacerItem(40, 20, SPSZ_EXPANDING, SPSZ_MINIMUM)
        self.horiz.addItem(self.spacerItem)

        self.table = AnalysisTable()
        self.connections[self.table.metric_edit_requested] = self.table.metric_edit_requested.connect(self.edit_metric_value)
        self.connections[self.table.metric_calculate_requested] = self.table.metric_calculate_requested.connect(self.calculate_metric_value)
        self.vert.addWidget(self.table)

        # add export table button at the bottom right of the dock
        self.horizExport = QtWidgets.QHBoxLayout()
        self.vert.addLayout(self.horizExport)

        self.horizExport.addWidget(add_help_button(self, "analyses"))

        self.cmdUnits = QtWidgets.QPushButton("Display Units")
        self.cmdUnits.setMenu(self.table.units_menu)
        self.horizExport.addWidget(self.cmdUnits)

        self.spacerItem = QtWidgets.QSpacerItem(40, 20, SPSZ_EXPANDING, SPSZ_MINIMUM)
        self.horizExport.addItem(self.spacerItem)

        self.cmdExport = QtWidgets.QPushButton()
        self.cmdExport.setText("Export Metrics Table")
        self.connections[self.cmdExport.clicked] = self.cmdExport.clicked.connect(self.export_table)
        self.cmdExport.setToolTip("Export the table to a csv, json, or Excel file")
        self.horizExport.addWidget(self.cmdExport, 0)

        self.setWidget(self.dockWidgetContents)
