"""Shoreliser right-side dock panel.

Four tabs, one per pipeline step. Each tab collects parameters with native QGIS
widgets (layer pickers, field-name dropdowns, an on-canvas extent selector),
shows a progress bar, validates input, runs the matching Processing algorithm,
auto-loads the (temporary) result layers, and pre-fills the next tab.

Scalar inputs (numbers, dates, checkboxes, field names) are remembered between
sessions via QgsSettings.
"""

import processing
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (
    QDockWidget, QWidget, QTabWidget, QVBoxLayout, QFormLayout,
    QPushButton, QLabel, QCheckBox, QDoubleSpinBox, QSpinBox, QLineEdit,
    QMessageBox, QProgressBar, QScrollArea, QApplication,
)
from qgis.core import (
    QgsProject, QgsMapLayer, QgsMapLayerProxyModel, QgsFieldProxyModel,
    QgsSettings, QgsProcessingFeedback, QgsCoordinateTransform,
    QgsLineSymbol, QgsMarkerSymbol, QgsPalLayerSettings, QgsTextFormat,
    QgsTextBufferSettings, QgsVectorLayerSimpleLabeling,
    QgsMessageLog, Qgis,
)
from qgis.gui import QgsMapLayerComboBox, QgsFieldComboBox, QgsExtentGroupBox

from ..build_config import PUBLISH_MODE, PUBLISH_LONGI_SCALE

SETTINGS_PREFIX = 'shoreliser/'


class _PanelFeedback(QgsProcessingFeedback):
    """Routes algorithm progress to a QProgressBar and keeps the UI responsive."""

    def __init__(self, progress_bar):
        super().__init__()
        self._bar = progress_bar

    def setProgress(self, progress):
        self._bar.setValue(int(progress))
        QApplication.processEvents()
        super().setProgress(progress)


class ShorelineDockWidget(QDockWidget):
    def __init__(self, iface, parent=None):
        super().__init__('Shoreliser', parent)
        self.iface = iface
        self.settings = QgsSettings()
        self._persisted = {}
        self.setObjectName('ShorelineHovmollerDock')

        self.tabs = QTabWidget()
        self.setWidget(self.tabs)

        self.tabs.addTab(self._scroll(self._build_tab1()), '1 · Prep')
        self.tabs.addTab(self._scroll(self._build_tab2()), '2 · Hovmöller')
        self.tabs.addTab(self._scroll(self._build_tab3()), '3 · Stats')
        self.tabs.addTab(self._scroll(self._build_tab4()), '4 · Cube')

    # ------------------------------------------------------------------ helpers
    @staticmethod
    def _scroll(widget):
        area = QScrollArea()
        area.setWidgetResizable(True)
        area.setWidget(widget)
        return area

    @staticmethod
    def _layer_combo(filt, allow_empty=False):
        cb = QgsMapLayerComboBox()
        cb.setFilters(filt)
        if allow_empty:
            cb.setAllowEmptyLayer(True)
        return cb

    @staticmethod
    def _bind_field(layer_combo, field_combo, default=None, filt=None):
        """Keep a field dropdown in sync with its source layer combo."""
        if filt is not None:
            field_combo.setFilters(filt)

        def _update(layer):
            field_combo.setLayer(layer)
            if default:
                field_combo.setField(default)

        layer_combo.layerChanged.connect(_update)
        _update(layer_combo.currentLayer())

    @staticmethod
    def _heading(text):
        lbl = QLabel(text)
        lbl.setWordWrap(True)
        lbl.setTextFormat(Qt.RichText)
        return lbl

    @staticmethod
    def _spin(decimals, minimum, maximum, value):
        sb = QDoubleSpinBox()
        sb.setDecimals(decimals)
        sb.setRange(minimum, maximum)
        sb.setValue(value)
        return sb

    # -- persistence ----------------------------------------------------------
    def _persist(self, key, widget):
        """Register *widget* for settings persistence and restore its value."""
        self._persisted[key] = widget
        val = self.settings.value(SETTINGS_PREFIX + key, None)
        if val is not None:
            try:
                if isinstance(widget, QCheckBox):
                    widget.setChecked(str(val).lower() in ('1', 'true', 'yes'))
                elif isinstance(widget, QSpinBox):
                    widget.setValue(int(float(val)))
                elif isinstance(widget, QDoubleSpinBox):
                    widget.setValue(float(val))
                elif isinstance(widget, QLineEdit):
                    widget.setText(str(val))
            except (ValueError, TypeError):
                QgsMessageLog.logMessage(
                    f'Ignoring invalid saved value for setting "{key}"',
                    'Shoreliser', Qgis.Info)
        return widget

    def _save_persisted(self):
        for key, widget in self._persisted.items():
            if isinstance(widget, QCheckBox):
                v = widget.isChecked()
            elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
                v = widget.value()
            elif isinstance(widget, QLineEdit):
                v = widget.text()
            else:
                continue
            self.settings.setValue(SETTINGS_PREFIX + key, v)

    # -- run / outputs --------------------------------------------------------
    def _run(self, alg_id, params, bar, button, load=True):
        """Run an algorithm with progress + error handling. Returns result dict
        or None on failure."""
        self._save_persisted()
        bar.setValue(0)
        bar.setVisible(True)
        button.setEnabled(False)
        QApplication.setOverrideCursor(Qt.WaitCursor)
        feedback = _PanelFeedback(bar)
        try:
            if load:
                result = processing.runAndLoadResults(alg_id, params, feedback=feedback)
            else:
                result = processing.run(alg_id, params, feedback=feedback)
        except Exception as exc:  # noqa: BLE001 - surface any algorithm error
            QApplication.restoreOverrideCursor()
            button.setEnabled(True)
            bar.setVisible(False)
            QMessageBox.critical(self, f'{alg_id} failed', str(exc))
            return None
        finally:
            QApplication.restoreOverrideCursor()
        bar.setValue(100)
        button.setEnabled(True)
        return result

    @staticmethod
    def _resolve(value):
        """Resolve an algorithm output value to a loaded QgsMapLayer (or None)."""
        if isinstance(value, QgsMapLayer):
            return value
        if isinstance(value, str):
            lyr = QgsProject.instance().mapLayer(value)
            if lyr:
                return lyr
            matches = QgsProject.instance().mapLayersByName(value)
            if matches:
                return matches[0]
        return None

    @staticmethod
    def _style_axis_grid(lines, points):
        """Style the Hovmöller axis overlay: dashed gray grid lines, and label
        points rendered as text only (years along the bottom, longi segment
        numbers along the left edge)."""
        try:
            if lines is not None:
                sym = QgsLineSymbol.createSimple({
                    'line_color': '70,70,70,150', 'line_width': '0.3',
                    'line_style': 'dash'})
                lines.renderer().setSymbol(sym)
                lines.triggerRepaint()
            if points is not None:
                sym = QgsMarkerSymbol.createSimple({
                    'name': 'circle', 'size': '0.5',
                    'color': '0,0,0,0', 'outline_style': 'no'})
                points.renderer().setSymbol(sym)
                pal = QgsPalLayerSettings()
                pal.fieldName = 'label'
                fmt = QgsTextFormat()
                fmt.setSize(9)
                buf = QgsTextBufferSettings()
                buf.setEnabled(True)
                buf.setSize(1.2)
                fmt.setBuffer(buf)
                pal.setFormat(fmt)
                points.setLabelsEnabled(True)
                points.setLabeling(QgsVectorLayerSimpleLabeling(pal))
                points.triggerRepaint()
        except Exception as exc:  # noqa: BLE001 - styling is cosmetic, never fatal
            QgsMessageLog.logMessage(
                f'Axis grid styling failed: {exc}', 'Shoreliser', Qgis.Warning)

    def _zoom_to(self, layer):
        """Zoom the map canvas to *layer*'s extent (handles CRS differences)."""
        if layer is None:
            return
        canvas = self.iface.mapCanvas()
        try:
            ext = layer.extent()
            dest_crs = canvas.mapSettings().destinationCrs()
            if layer.crs().isValid() and layer.crs() != dest_crs:
                tr = QgsCoordinateTransform(layer.crs(), dest_crs, QgsProject.instance())
                ext = tr.transformBoundingBox(ext)
            if ext.isEmpty():
                return
            ext.scale(1.05)
            canvas.setExtent(ext)
            canvas.refresh()
        except Exception as exc:  # noqa: BLE001 - zooming is best-effort
            QgsMessageLog.logMessage(
                f'Auto-zoom to the diagram failed: {exc}', 'Shoreliser', Qgis.Warning)

    @staticmethod
    def _require(condition, message, parent):
        if not condition:
            QMessageBox.warning(parent, 'Missing input', message)
            return False
        return True

    # ================================================================== TAB 1
    def _build_tab1(self):
        w = QWidget()
        form = QFormLayout(w)
        form.addRow(self._heading(
            '<b>Step 1 — Clip &amp; split baseline</b><br>'
            '<small>Select one feature in the beach boundary layer, then run. '
            'Clips the baseline and shorelines to it, splits the baseline into '
            'fixed-length segments numbered in a new <code>longi</code> field, and '
            'adds a normalized <code>date_clean</code> field.</small>'))

        self.t1_beach = self._layer_combo(QgsMapLayerProxyModel.PolygonLayer)
        self.t1_baseline = self._layer_combo(QgsMapLayerProxyModel.LineLayer)
        self.t1_shorelines = self._layer_combo(QgsMapLayerProxyModel.LineLayer)
        self.t1_datefield = QgsFieldComboBox()
        self._bind_field(self.t1_shorelines, self.t1_datefield, default='fecha',
                         filt=QgsFieldProxyModel.String | QgsFieldProxyModel.Numeric)
        self.t1_segment = self._persist('t1_segment', self._spin(1, 1, 100000, 50))

        form.addRow('Beach boundary (select 1 feature!)', self.t1_beach)
        form.addRow('Baseline', self.t1_baseline)
        form.addRow('Shorelines', self.t1_shorelines)
        form.addRow('Raw date field', self.t1_datefield)
        form.addRow('Baseline segment length (m)', self.t1_segment)

        self.t1_bar = QProgressBar(); self.t1_bar.setVisible(False)
        self.t1_btn = QPushButton('Run Step 1 →')
        self.t1_btn.clicked.connect(self._run_tab1)
        form.addRow(self.t1_bar)
        form.addRow(self.t1_btn)
        return w

    def _run_tab1(self):
        beach = self.t1_beach.currentLayer()
        if not self._require(beach is not None, 'Choose a beach boundary layer.', self):
            return
        if not self._require(beach.selectedFeatureCount() > 0,
                             'Select a feature in the beach boundary layer first '
                             "(use the 'Select Features' tool).", self):
            return
        if not self._require(self.t1_baseline.currentLayer() and self.t1_shorelines.currentLayer(),
                             'Choose both baseline and shorelines layers.', self):
            return
        params = {
            'BEACH_BOUNDARY': beach,
            'BASELINE': self.t1_baseline.currentLayer(),
            'SHORELINES': self.t1_shorelines.currentLayer(),
            'DATE_FIELD': self.t1_datefield.currentField(),
            'SEGMENT_LENGTH': self.t1_segment.value(),
            'OUTPUT_BASELINE': 'TEMPORARY_OUTPUT',
            'OUTPUT_SHORELINES': 'TEMPORARY_OUTPUT',
        }
        res = self._run('shoreliser:shoreline_data_prep_fixed', params,
                        self.t1_bar, self.t1_btn)
        if res is None:
            return
        bl = self._resolve(res.get('OUTPUT_BASELINE'))
        sh = self._resolve(res.get('OUTPUT_SHORELINES'))
        if bl:
            self.t2_baseline.setLayer(bl)
        if sh:
            self.t2_shorelines.setLayer(sh)
        self.tabs.setCurrentIndex(1)

    # ================================================================== TAB 2
    def _build_tab2(self):
        w = QWidget()
        form = QFormLayout(w)
        form.addRow(self._heading(
            '<b>Step 2 — Hovmöller diagram</b><br>'
            '<small>Casts transects from the baseline and builds the width and '
            'change rasters plus the raw/grid point layers.</small>'))

        self.t2_shorelines = self._layer_combo(QgsMapLayerProxyModel.LineLayer)
        self.t2_baseline = self._layer_combo(QgsMapLayerProxyModel.LineLayer)
        self.t2_datefield = QgsFieldComboBox()
        self._bind_field(self.t2_shorelines, self.t2_datefield, default='date_clean',
                         filt=QgsFieldProxyModel.String | QgsFieldProxyModel.Numeric)
        self.t2_longifield = QgsFieldComboBox()
        self._bind_field(self.t2_baseline, self.t2_longifield, default='longi',
                         filt=QgsFieldProxyModel.Numeric)

        self.t2_searchdist = self._persist('t2_searchdist', self._spin(1, 1, 100000, 500))
        self.t2_smooth = QSpinBox(); self.t2_smooth.setRange(0, 1000); self.t2_smooth.setValue(10)
        self.t2_smooth = self._persist('t2_smooth', self.t2_smooth)
        self.t2_pixel = self._persist('t2_pixel', self._spin(2, 0.01, 10000, 10))
        # Advanced fields: hidden in the publish build, where the longi scale is
        # pinned to PUBLISH_LONGI_SCALE and the reference raster stays empty.
        if PUBLISH_MODE:
            self.t2_longiscale = self._spin(2, 0.01, 1000, PUBLISH_LONGI_SCALE)
        else:
            self.t2_longiscale = self._persist('t2_longiscale', self._spin(2, 0.01, 1000, 1.0))
        self.t2_invert = self._persist('t2_invert', QCheckBox('Invert Y axis for Hovmöller diagram'))
        self.t2_refdate = self._persist('t2_refdate', QLineEdit('1984-01-01'))
        self.t2_refraster = self._layer_combo(QgsMapLayerProxyModel.RasterLayer, allow_empty=True)
        self.t2_grid_year = QSpinBox(); self.t2_grid_year.setRange(1, 100); self.t2_grid_year.setValue(1)
        self.t2_grid_year = self._persist('t2_grid_year', self.t2_grid_year)
        self.t2_grid_longi = QSpinBox(); self.t2_grid_longi.setRange(1, 100000); self.t2_grid_longi.setValue(10)
        self.t2_grid_longi = self._persist('t2_grid_longi', self.t2_grid_longi)

        form.addRow('Clipped shorelines', self.t2_shorelines)
        form.addRow('Clipped baseline', self.t2_baseline)
        form.addRow('Date field', self.t2_datefield)
        form.addRow('Longi field', self.t2_longifield)
        form.addRow('Transect length (m)', self.t2_searchdist)
        form.addRow('Direction smoothing', self.t2_smooth)
        form.addRow('Pixel size', self.t2_pixel)
        if not PUBLISH_MODE:
            form.addRow('Longi scale', self.t2_longiscale)
        form.addRow(self.t2_invert)
        form.addRow('Reference date (YYYY-MM-DD)', self.t2_refdate)
        if not PUBLISH_MODE:
            form.addRow('Reference raster (opt.)', self.t2_refraster)
        form.addRow('Axis grid: year interval', self.t2_grid_year)
        form.addRow('Axis grid: longi interval', self.t2_grid_longi)

        self.t2_bar = QProgressBar(); self.t2_bar.setVisible(False)
        self.t2_btn = QPushButton('Run Step 2 →')
        self.t2_btn.clicked.connect(self._run_tab2)
        form.addRow(self.t2_bar)
        form.addRow(self.t2_btn)
        return w

    def _run_tab2(self):
        if not self._require(self.t2_shorelines.currentLayer() and self.t2_baseline.currentLayer(),
                             'Choose clipped shorelines and baseline layers.', self):
            return
        if not self._require(self.t2_datefield.currentField() and self.t2_longifield.currentField(),
                             'Choose the date and longi fields.', self):
            return
        params = {
            'INPUT_LINES': self.t2_shorelines.currentLayer(),
            'INPUT_BASELINE': self.t2_baseline.currentLayer(),
            'DATE_FIELD': self.t2_datefield.currentField(),
            'LONGI_FIELD': self.t2_longifield.currentField(),
            'SEARCH_DIST': self.t2_searchdist.value(),
            'SMOOTH_WINDOW': self.t2_smooth.value(),
            'PIXEL_SIZE': self.t2_pixel.value(),
            'INVERT_Y': self.t2_invert.isChecked(),
            'LONGI_SCALE': self.t2_longiscale.value(),
            'REF_DATE': self.t2_refdate.text(),
            'REF_RASTER': self.t2_refraster.currentLayer(),
            'GRID_YEAR_INTERVAL': self.t2_grid_year.value(),
            'GRID_LONGI_INTERVAL': self.t2_grid_longi.value(),
            'OUTPUT_RAW_PTS': 'TEMPORARY_OUTPUT',
            'OUTPUT_GRID_PTS': 'TEMPORARY_OUTPUT',
            'OUTPUT_WIDTH_RASTER': 'TEMPORARY_OUTPUT',
            'OUTPUT_CHANGE_RASTER': 'TEMPORARY_OUTPUT',
            'OUTPUT_AXIS_GRID': 'TEMPORARY_OUTPUT',
            'OUTPUT_AXIS_LABELS': 'TEMPORARY_OUTPUT',
        }
        res = self._run('shoreliser:hovmoller_step2_v23', params, self.t2_bar, self.t2_btn)
        if res is None:
            return
        raw = self._resolve(res.get('OUTPUT_RAW_PTS'))
        grid = self._resolve(res.get('OUTPUT_GRID_PTS'))
        width = self._resolve(res.get('OUTPUT_WIDTH_RASTER'))
        self._style_axis_grid(self._resolve(res.get('OUTPUT_AXIS_GRID')),
                              self._resolve(res.get('OUTPUT_AXIS_LABELS')))
        if grid:
            self.t3_grid.setLayer(grid)
            self.t4_layer.setLayer(grid)
        if raw:
            self.t3_raw.setLayer(raw)
        if self.t2_shorelines.currentLayer():
            self.t3_lines.setLayer(self.t2_shorelines.currentLayer())
        self.t3_refdate.setText(self.t2_refdate.text())
        self.t4_basedate.setText(self.t2_refdate.text())
        # Zoom to the freshly created Hovmöller diagram so the user can draw the
        # Step 3 selection straight away. Prefer the width raster, fall back to grid.
        self._zoom_to(width or grid)
        self.tabs.setCurrentIndex(2)

    # ================================================================== TAB 3
    def _build_tab3(self):
        w = QWidget()
        form = QFormLayout(w)
        form.addRow(self._heading(
            '<b>Step 3 — Extract stats &amp; zoom</b><br>'
            '<small>Draw an extent over the Hovmöller diagram, then run to '
            'compute DSAS statistics (SCE, NSM, EPR, LRR, R² …) for the '
            'selected transects and zoom to them.</small>'))

        self.t3_grid = self._layer_combo(QgsMapLayerProxyModel.PointLayer)
        self.t3_raw = self._layer_combo(QgsMapLayerProxyModel.PointLayer)
        self.t3_lines = self._layer_combo(QgsMapLayerProxyModel.LineLayer)
        self.t3_datefield = QgsFieldComboBox()
        self._bind_field(self.t3_lines, self.t3_datefield, default='date_clean',
                         filt=QgsFieldProxyModel.String | QgsFieldProxyModel.Numeric)
        self.t3_refdate = self._persist('t3_refdate', QLineEdit('1984-01-01'))

        self.t3_extent = QgsExtentGroupBox()
        self.t3_extent.setMapCanvas(self.iface.mapCanvas())
        self.t3_extent.setTitle('Area of interest (extent on diagram)')

        form.addRow('Grid points', self.t3_grid)
        form.addRow(self.t3_extent)
        form.addRow('Raw points', self.t3_raw)
        form.addRow('Clipped shorelines', self.t3_lines)
        form.addRow('Date field', self.t3_datefield)
        form.addRow('Reference date (YYYY-MM-DD)', self.t3_refdate)

        self.t3_bar = QProgressBar(); self.t3_bar.setVisible(False)
        self.t3_btn = QPushButton('Run Step 3 →')
        self.t3_btn.clicked.connect(self._run_tab3)
        form.addRow(self.t3_bar)
        form.addRow(self.t3_btn)
        return w

    def _run_tab3(self):
        if not self._require(self.t3_grid.currentLayer() and self.t3_raw.currentLayer()
                             and self.t3_lines.currentLayer(),
                             'Choose grid points, raw points and shorelines layers.', self):
            return
        ext = self.t3_extent.outputExtent()
        if not self._require(ext is not None and not ext.isEmpty(),
                             'Define an area of interest extent first.', self):
            return
        crs = self.t3_extent.outputCrs()
        extent_str = (f'{ext.xMinimum()},{ext.xMaximum()},'
                      f'{ext.yMinimum()},{ext.yMaximum()} [{crs.authid()}]')
        params = {
            'INPUT_GRID': self.t3_grid.currentLayer(),
            'AREA_OF_INTEREST': extent_str,
            'INPUT_RAW': self.t3_raw.currentLayer(),
            'INPUT_SHORELINES': self.t3_lines.currentLayer(),
            'DATE_FIELD': self.t3_datefield.currentField(),
            'REF_DATE': self.t3_refdate.text(),
            'OUTPUT_PTS': 'TEMPORARY_OUTPUT',
            'OUTPUT_LINES': 'TEMPORARY_OUTPUT',
        }
        res = self._run('shoreliser:hovmoller_step3_stats_extractor', params,
                        self.t3_bar, self.t3_btn)
        if res is None:
            return
        pts = self._resolve(res.get('OUTPUT_PTS'))
        if pts:
            self.t4_layer.setLayer(pts)
        self.tabs.setCurrentIndex(3)

    # ================================================================== TAB 4
    def _build_tab4(self):
        w = QWidget()
        form = QFormLayout(w)
        form.addRow(self._heading(
            '<b>Step 4 — Interactive space-time cube</b><br>'
            '<small>Generates an offline Plotly HTML (3D cube + 2D OSM map) and '
            'opens it in your browser.</small>'))

        self.t4_layer = self._layer_combo(QgsMapLayerProxyModel.PointLayer)
        self.t4_id = QgsFieldComboBox(); self.t4_x = QgsFieldComboBox()
        self.t4_y = QgsFieldComboBox(); self.t4_z = QgsFieldComboBox()
        self._bind_field(self.t4_layer, self.t4_id, default='longi')
        self._bind_field(self.t4_layer, self.t4_x, default='longi')
        self._bind_field(self.t4_layer, self.t4_y, default='dist_m',
                         filt=QgsFieldProxyModel.Numeric)
        self._bind_field(self.t4_layer, self.t4_z, default='days')
        self.t4_basedate = self._persist('t4_basedate', QLineEdit('1984-01-01'))

        form.addRow('Points layer', self.t4_layer)
        form.addRow('Transect ID field', self.t4_id)
        form.addRow('Along-shore field', self.t4_x)
        form.addRow('Width field', self.t4_y)
        form.addRow('Time (days) field', self.t4_z)
        form.addRow('Base date (YYYY-MM-DD)', self.t4_basedate)

        self.t4_bar = QProgressBar(); self.t4_bar.setVisible(False)
        self.t4_btn = QPushButton('Generate HTML cube')
        self.t4_btn.clicked.connect(self._run_tab4)
        form.addRow(self.t4_bar)
        form.addRow(self.t4_btn)
        return w

    def _run_tab4(self):
        if not self._require(self.t4_layer.currentLayer() is not None,
                             'Choose a points layer.', self):
            return
        if not self._require(all([self.t4_id.currentField(), self.t4_x.currentField(),
                                  self.t4_y.currentField(), self.t4_z.currentField()]),
                             'Choose all four fields (ID, along-shore, width, time).', self):
            return
        params = {
            'INPUT_LAYER': self.t4_layer.currentLayer(),
            'FIELD_ID': self.t4_id.currentField(),
            'FIELD_X': self.t4_x.currentField(),
            'FIELD_Y': self.t4_y.currentField(),
            'FIELD_Z': self.t4_z.currentField(),
            'BASE_DATE': self.t4_basedate.text(),
        }
        self._run('shoreliser:space_time_cube_interactive', params,
                  self.t4_bar, self.t4_btn, load=False)
