# -*- coding: utf-8 -*-
"""Tool 3: Label sample points over Google Satellite imagery.

Two-level labeling following the Cambodia LU2003 system:
  Level 1 - 7 broad classes (used for classification)
  Level 2 - 40 detailed LU2003 classes (optional detail per point)
"""

from qgis.PyQt.QtCore import Qt, QVariant
from qgis.PyQt.QtWidgets import (
    QDockWidget, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton,
    QLabel, QCheckBox, QMessageBox, QSpinBox, QComboBox, QFileDialog,
    QInputDialog,
)
from qgis.gui import QgsMapLayerComboBox
from qgis.core import (
    QgsMapLayerProxyModel, QgsField, QgsCoordinateTransform, QgsProject,
)

from .. import gee_utils as gu
from .help_texts import help_button

LEVEL1_ONLY, LEVEL12 = 0, 1


def _is_dark(hex_color):
    """True if a colour needs white text for contrast."""
    try:
        h = hex_color.lstrip("#")
        r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
        # relative luminance
        return (0.299 * r + 0.587 * g + 0.114 * b) < 140
    except (ImportError, ModuleNotFoundError, RuntimeError):  # GEE/Import
        return False


class LabelingDock(QDockWidget):
    def __init__(self, iface, parent=None):
        super().__init__("3. Label Sample Points", parent)
        self.iface = iface
        self.fids = []
        self.pos = 0
        self._build_ui()

    def _build_ui(self):
        w = QWidget()
        lay = QVBoxLayout(w)

        self.cb_layer = QgsMapLayerComboBox()
        self.cb_layer.setFilters(QgsMapLayerProxyModel.Filter.PointLayer)
        self.cb_layer.layerChanged.connect(self.load_layer)
        lay.addWidget(QLabel("Points layer:"))
        lay.addWidget(self.cb_layer)

        hg = QHBoxLayout()
        self.btn_gcp = QPushButton("Load GCP shapefile ...")
        self.btn_gcp.setToolTip(
            "Load field-surveyed ground control points. They are shown on "
            "the canvas and the nearest GCP class appears as a hint.")
        self.btn_gcp.clicked.connect(self.load_gcp)
        self.btn_gcp_apply = QPushButton("Apply GCP labels")
        self.btn_gcp_apply.setToolTip(
            "Copy the class from each nearby GCP onto the sample points.")
        self.btn_gcp_apply.setEnabled(False)
        self.btn_gcp_apply.clicked.connect(self.apply_gcp_labels)
        btn_gcp_help = help_button("gcp_table", self)
        btn_gcp_help.setToolTip(
            "Show what the sample-point and GCP attribute tables should "
            "look like, and which column to fill in.")
        hg.addWidget(btn_gcp_help)
        hg.addWidget(self.btn_gcp); hg.addWidget(self.btn_gcp_apply)
        lay.addLayout(hg)
        self.gcp_layer = None
        self.gcp_field = None

        hq = QHBoxLayout()
        hq.addWidget(help_button("labeling", self))
        self.btn_quad = QPushButton("Open 4-view labeling window")
        self.btn_quad.setStyleSheet("font-weight: bold;")
        self.btn_quad.clicked.connect(self.open_quad)
        hq.addWidget(self.btn_quad, 1)
        lay.addLayout(hq)

        h1 = QHBoxLayout()
        self.btn_prev = QPushButton("< Prev")
        self.btn_next = QPushButton("Next >")
        self.btn_prev.clicked.connect(lambda: self.step(-1))
        self.btn_next.clicked.connect(lambda: self.step(1))
        self.lbl_pos = QLabel("0 / 0")
        self.lbl_pos.setAlignment(Qt.AlignmentFlag.AlignCenter)
        h1.addWidget(self.btn_prev); h1.addWidget(self.lbl_pos)
        h1.addWidget(self.btn_next)
        lay.addLayout(h1)

        h2 = QHBoxLayout()
        h2.addWidget(QLabel("Zoom 1:"))
        self.sp_scale = QSpinBox()
        self.sp_scale.setRange(200, 50000); self.sp_scale.setValue(1500)
        self.sp_scale.setSingleStep(100)
        h2.addWidget(self.sp_scale)
        self.chk_auto = QCheckBox("Auto-advance")
        self.chk_auto.setChecked(True)
        h2.addWidget(self.chk_auto)
        lay.addLayout(h2)

        self.chk_sig = QCheckBox(
            "Show pixel signature at point (needs the local stack raster)")
        lay.addWidget(self.chk_sig)
        self.lbl_sig = QLabel("")
        self.lbl_sig.setWordWrap(True)
        self.lbl_sig.setStyleSheet("color:#555; font-size:11px;")
        lay.addWidget(self.lbl_sig)

        # classification scheme
        self.cb_scheme = QComboBox()
        self.cb_scheme.addItems([
            "LULC (7 broad classes)",
            "ESA WorldCover 2021 (11 classes)",
            "Custom classes ...",
        ])
        self.cb_scheme.currentIndexChanged.connect(self._scheme_changed)
        lay.addWidget(QLabel("Classification scheme:"))
        lay.addWidget(self.cb_scheme)
        self.btn_edit_custom = QPushButton("Edit custom classes ...")
        self.btn_edit_custom.clicked.connect(self._edit_custom)
        self.btn_edit_custom.setVisible(False)
        lay.addWidget(self.btn_edit_custom)

        # labeling level (LU2003 detail only applies to the LULC scheme)
        self.cb_level = QComboBox()
        self.cb_level.addItems([
            "Level 1",
            "Level 1 + Level 2 (LU2003 detail)",
        ])
        self.cb_level.currentIndexChanged.connect(self._level_changed)
        lay.addWidget(self.cb_level)

        self.lbl_info = QLabel("point: -")
        self.lbl_info.setWordWrap(True)
        lay.addWidget(self.lbl_info)

        # class buttons live in a container we can rebuild per scheme
        from qgis.PyQt.QtWidgets import QWidget as _W
        self.class_box = _W()
        self.class_grid = QGridLayout(self.class_box)
        self.class_grid.setSpacing(3)
        lay.addWidget(self.class_box)
        self._build_class_buttons()

        # Level 2 combo (LU2003)
        self.lbl_l2 = QLabel("Level 2 (LU2003):")
        self.cb_l2 = QComboBox()
        self.cb_l2.activated.connect(self._assign_l2)
        lay.addWidget(self.lbl_l2)
        lay.addWidget(self.cb_l2)

        h3 = QHBoxLayout()
        btn_skip = QPushButton("Skip / next unlabeled")
        btn_skip.clicked.connect(self.next_unlabeled)
        btn_clear = QPushButton("Clear label")
        btn_clear.clicked.connect(self.clear_label)
        h3.addWidget(btn_skip); h3.addWidget(btn_clear)
        lay.addLayout(h3)

        self.lbl_progress = QLabel("")
        lay.addWidget(self.lbl_progress)

        h4 = QHBoxLayout()
        btn_reset = QPushButton("Reset (go to point 1)")
        btn_reset.clicked.connect(self.reset_position)
        self.btn_close = QPushButton("Close")
        self.btn_close.clicked.connect(self.hide)
        h4.addWidget(btn_reset); h4.addWidget(self.btn_close)
        lay.addLayout(h4)
        lay.addStretch()
        self.setWidget(w)
        self._level_changed(0)
        st = getattr(self, "state", None)
        if isinstance(st, dict):
            st.setdefault("scheme", "lulc")

    # ------------------------------------------------------------ GCPs
    def load_gcp(self):
        """Load a GCP (ground control point) shapefile as reference."""
        from qgis.core import QgsVectorLayer, QgsMarkerSymbol
        path, _ = QFileDialog.getOpenFileName(
            self, "Choose GCP shapefile", "",
            "Vector (*.shp *.gpkg *.geojson *.csv)")
        if not path:
            return
        lyr = QgsVectorLayer(path, "GCPs", "ogr")
        if not lyr.isValid():
            QMessageBox.warning(self, "GCP", "Could not load that file.")
            return
        fields = [f.name() for f in lyr.fields()]
        field, ok = QInputDialog.getItem(
            self, "GCP class field",
            "Which field holds the land cover class?\n"
            "(text names like 'Forest', or codes 1-7)",
            fields, 0, False)
        if not ok:
            return
        self.gcp_layer = lyr
        self.gcp_field = field
        sym = QgsMarkerSymbol.createSimple(
            {"name": "star", "size": "3.5", "color": "0,255,255",
             "outline_color": "black", "outline_width": "0.3"})
        lyr.renderer().setSymbol(sym)
        QgsProject.instance().addMapLayer(lyr)
        gu.raise_points_above(("GCPs", "sample_points"))
        self.btn_gcp_apply.setEnabled(True)
        self.lbl_sig.setText(
            "GCPs loaded: {} points, class field '{}'. The nearest GCP "
            "now shows as a hint while you label.".format(
                lyr.featureCount(), field))
        self._refresh()

    def _gcp_code(self, raw):
        """Map a GCP field value (name or code) to a Level-1 class code."""
        if raw is None:
            return 0, ""
        s = str(raw).strip()
        try:
            c = int(float(s))
            if 1 <= c <= len(gu.LULC_CLASSES):
                return c, gu.l1_name(c)
        except (TypeError, ValueError):
            pass
        low = s.lower()
        for code, name, _ in gu.LULC_CLASSES:
            if low == name.lower() or low in name.lower():
                return code, name
        return 0, s

    def _nearest_gcp(self, feat):
        """(class_code, label, distance_m) of the closest GCP, or None."""
        if self.gcp_layer is None or self.gcp_field is None:
            return None
        from qgis.core import (QgsCoordinateTransform, QgsDistanceArea,
                               QgsProject as _P)
        lyr = self.current_layer()
        try:
            ct = QgsCoordinateTransform(lyr.crs(), self.gcp_layer.crs(),
                                        _P.instance())
            pt = ct.transform(feat.geometry().asPoint())
        except (AttributeError, TypeError, RuntimeError):  # QGIS geometry
            return None
        da = QgsDistanceArea()
        da.setSourceCrs(self.gcp_layer.crs(), _P.instance().transformContext())
        da.setEllipsoid("WGS84")
        best, best_d = None, None
        for g in self.gcp_layer.getFeatures():
            if not g.hasGeometry():
                continue
            try:
                dist = da.measureLine(pt, g.geometry().asPoint())
            except (AttributeError, TypeError, RuntimeError):  # QGIS geometry
                continue
            if best_d is None or dist < best_d:
                best_d, best = dist, g
        if best is None:
            return None
        code, label = self._gcp_code(best[self.gcp_field])
        return code, label, best_d

    def apply_gcp_labels(self):
        """Copy GCP classes onto sample points within a chosen radius."""
        lyr = self.current_layer()
        if lyr is None or self.gcp_layer is None:
            return
        radius, ok = QInputDialog.getInt(
            self, "Apply GCP labels",
            "Label a sample point when a GCP is within (metres):",
            30, 1, 1000)
        if not ok:
            return
        self.load_layer()
        idx_code = lyr.fields().indexOf("class_code")
        idx_name = lyr.fields().indexOf("class_name")
        changes = {}
        n = 0
        for f in lyr.getFeatures():
            res = self._nearest_gcp(f)
            if not res:
                continue
            code, label, dist = res
            if code > 0 and dist <= radius:
                changes[f.id()] = {idx_code: code,
                                   idx_name: gu.l1_name(code)}
                n += 1
        if changes:
            lyr.dataProvider().changeAttributeValues(changes)
            lyr.triggerRepaint()
        QMessageBox.information(
            self, "GCP labels",
            "Labeled {} sample point(s) from GCPs within {} m.\n"
            "Review them before classifying - a GCP is only reliable for "
            "the exact spot it was surveyed.".format(n, radius))
        self._refresh()

    def open_quad(self):
        from .quad_label_window import QuadLabelWindow
        if self.current_layer() is None:
            from qgis.PyQt.QtWidgets import QMessageBox
            QMessageBox.information(
                self, "No points", "Choose a points layer first.")
            return
        self.load_layer()
        if getattr(self, "quad_win", None) is None:
            self.quad_win = QuadLabelWindow(
                self.iface, getattr(self, "state", {}) or {}, self)
        self.quad_win.show()
        self.quad_win.raise_()
        self.quad_win.refresh()

    # ------------------------------------------------------------ level UI
    # ------------------------------------------------------------ scheme
    def _current_scheme(self):
        return ("lulc", "worldcover", "custom")[self.cb_scheme.currentIndex()]

    def _scheme_classes(self):
        return gu.scheme_classes(self._current_scheme())

    def _build_class_buttons(self):
        """(Re)build the class buttons for the current scheme."""
        # clear existing buttons
        while self.class_grid.count():
            item = self.class_grid.takeAt(0)
            w = item.widget()
            if w is not None:
                w.setParent(None)
        classes = self._scheme_classes()
        if not classes:
            lbl = QLabel("No custom classes yet - click "
                         "'Edit custom classes ...' to add some.")
            lbl.setWordWrap(True)
            self.class_grid.addWidget(lbl, 0, 0, 1, 2)
            return
        for i, (code, name, color) in enumerate(classes):
            btn = QPushButton("{}. {}".format(code, name))
            btn.setMinimumHeight(30)
            dark = _is_dark(color)
            btn.setStyleSheet(
                "background-color: {}; color: {}; font-weight: bold; "
                "padding: 4px;".format(color, "white" if dark else "black"))
            btn.clicked.connect(
                lambda _, c=code, n=name: self.assign(c, n))
            self.class_grid.addWidget(btn, i // 2, i % 2)

    def _scheme_changed(self, idx):
        scheme = self._current_scheme()
        # share the active scheme so Tool 4/6 render with the same classes
        st = getattr(self, "state", None)
        if isinstance(st, dict):
            st["scheme"] = scheme
        self.btn_edit_custom.setVisible(scheme == "custom")
        # LU2003 Level-2 detail only makes sense for the LULC scheme
        is_lulc = scheme == "lulc"
        self.cb_level.setEnabled(is_lulc)
        if not is_lulc:
            self.cb_level.setCurrentIndex(0)
        self._build_class_buttons()
        self._level_changed(self.cb_level.currentIndex())

    def _edit_custom(self):
        from .custom_classes_dialog import CustomClassesDialog
        dlg = CustomClassesDialog(gu.load_custom_classes(), self)
        if dlg.exec():
            gu.save_custom_classes(dlg.classes())
            self._build_class_buttons()

    def _level_changed(self, idx):
        on = idx == LEVEL12 and self._current_scheme() == "lulc"
        self.lbl_l2.setVisible(on)
        self.cb_l2.setVisible(on)

    def _fill_l2(self, l1_code):
        self.cb_l2.clear()
        self.cb_l2.addItem("(choose detail class ...)", None)
        for lu, abbr, name in gu.l2_classes_for(l1_code):
            self.cb_l2.addItem("{} {} - {}".format(lu, abbr, name), lu)

    # ------------------------------------------------------------ layer
    def reset_position(self):
        self.pos = 0
        self._refresh()

    def _prepare_canvas(self):
        """Give the main canvas a clean, FAST labeling view.

        Google Satellite serves pre-cached tiles and loads instantly, so it
        is the default basemap. GEE-computed layers (NDVI/NDWI/WorldCover)
        re-run on every map tile and are slow to pan/zoom, so they start
        hidden - tick them only when you need them, or use the 4-view
        window which loads them per panel on demand."""
        try:
            gu.add_google_satellite()
            gu.show_only_layers(
                show_names=("Google Satellite", "sample_points"),
                hide_prefixes=("Index: ", "Stack ", "Sentinel-2",
                               "Sentinel-1", "Landsat", "Composite ",
                               "True color", "ESA WorldCover"))
            gu.raise_points_above(("sample_points",))
        except (AttributeError, TypeError):  # Qt/QGIS API compatibility
            pass

    def load_layer(self, layer=None):
        layer = layer or self.cb_layer.currentLayer()
        if layer is None:
            return
        self._ensure_fields(layer)
        self._prepare_canvas()
        self.fids = sorted([f.id() for f in layer.getFeatures()])
        self.pos = 0
        self._refresh()

    @staticmethod
    def _ensure_fields(layer):
        names = [f.name() for f in layer.fields()]
        missing = []
        for fname, ftype in (("class_code", QVariant.Int),
                             ("class_name", QVariant.String),
                             ("lu_code", QVariant.Int),
                             ("lu_name", QVariant.String),
                             ("use", QVariant.String)):
            if fname not in names:
                missing.append(QgsField(fname, ftype))
        if missing:
            layer.dataProvider().addAttributes(missing)
            layer.updateFields()

    # ------------------------------------------------------------ navigation
    def current_layer(self):
        return self.cb_layer.currentLayer()

    def step(self, d):
        if not self.fids:
            self.load_layer()
        if not self.fids:
            return
        self.pos = (self.pos + d) % len(self.fids)
        self._refresh()

    def next_unlabeled(self):
        lyr = self.current_layer()
        if lyr is None or not self.fids:
            return
        n = len(self.fids)
        for k in range(1, n + 1):
            p = (self.pos + k) % n
            f = lyr.getFeature(self.fids[p])
            if int(f["class_code"] or 0) == 0:
                self.pos = p
                self._refresh()
                return
        QMessageBox.information(self, "Done",
                                "All points are labeled. Good job!")

    def _refresh(self):
        lyr = self.current_layer()
        if lyr is None or not self.fids:
            self.lbl_pos.setText("0 / 0")
            return
        fid = self.fids[self.pos]
        f = lyr.getFeature(fid)
        self.lbl_pos.setText("{} / {}".format(self.pos + 1, len(self.fids)))
        fields = f.fields().names()
        lu = ""
        if "lu_name" in fields and f["lu_name"]:
            lu = "   L2: {} {}".format(f["lu_code"], f["lu_name"])
        hint = ""
        if "wc_hint" in fields and f["wc_hint"]:
            hint = "   WorldCover hint: {}".format(f["wc_hint"])
        gcp = ""
        try:
            res = self._nearest_gcp(f)
            if res:
                code, label, dist = res
                gcp = "   GCP: {} ({:.0f} m)".format(label, dist)
        except (ValueError, TypeError):  # JSON parsing
            pass
        self.lbl_info.setText(
            "point_id: {}   L1: {}   use: {}{}{}{}".format(
                f["point_id"] if "point_id" in fields else fid,
                f["class_name"] or "-", f["use"] or "-", lu, hint, gcp))
        lyr.selectByIds([fid])
        self._zoom_to(lyr, f)
        if self.chk_sig.isChecked():
            self._show_signature(f)
        labeled = sum(1 for ft in lyr.getFeatures()
                      if int(ft["class_code"] or 0) > 0)
        self.lbl_progress.setText(
            "Labeled: {} / {}".format(labeled, lyr.featureCount()))
        # refresh the L2 list for the current class
        code = int(f["class_code"] or 0)
        if code > 0:
            self._fill_l2(code)

        # keep the 4-view window in sync when navigation happens from the
        # main panel (guard against recursion with _syncing_quad)
        qw = getattr(self, "quad_win", None)
        if qw is not None and qw.isVisible() and \
                not getattr(self, "_syncing_quad", False):
            try:
                self._syncing_quad = True
                qw.sync_to_dock()
            finally:
                self._syncing_quad = False

    def _show_signature(self, feat):
        """Read the local stack raster (a 'Stack N bands' layer) at the
        point - a lightweight version of SCP's spectral signature."""
        try:
            from qgis.core import QgsRasterLayer
            stack = None
            for rl in QgsProject.instance().mapLayers().values():
                if isinstance(rl, QgsRasterLayer) and \
                        rl.name().startswith("Stack "):
                    stack = rl
                    break
            if stack is None:
                self.lbl_sig.setText("No local stack raster loaded "
                                     "(tick 'local multiband' in Tool 1).")
                return
            from qgis.core import QgsCoordinateTransform
            ct = QgsCoordinateTransform(
                self.current_layer().crs(), stack.crs(),
                QgsProject.instance())
            pt = ct.transform(feat.geometry().asPoint())
            val = stack.dataProvider().identify(
                pt, 1).results()  # QgsRaster.IdentifyFormatValue = 1
            if not val:
                self.lbl_sig.setText("")
                return
            items = list(val.items())[:8]
            txt = "  ".join("B{}:{}".format(
                k, round(v, 3) if isinstance(v, float) else v)
                for k, v in items if v is not None)
            self.lbl_sig.setText("signature: " + txt)
        except Exception as e:
            self.lbl_sig.setText("signature n/a ({})".format(e))

    def _zoom_to(self, lyr, feat):
        canvas = self.iface.mapCanvas()
        ct = QgsCoordinateTransform(
            lyr.crs(), canvas.mapSettings().destinationCrs(),
            QgsProject.instance())
        pt = ct.transform(feat.geometry().asPoint())
        canvas.setCenter(pt)
        canvas.zoomScale(self.sp_scale.value())
        canvas.refresh()

    # ------------------------------------------------------------ labeling
    def _set_attrs(self, values):
        lyr = self.current_layer()
        if lyr is None or not self.fids:
            return None
        fid = self.fids[self.pos]
        idx = {n: lyr.fields().indexOf(n) for n in values}
        lyr.dataProvider().changeAttributeValues(
            {fid: {idx[n]: v for n, v in values.items()}})
        lyr.triggerRepaint()
        return fid

    def assign(self, code, name):
        """Level-1 button clicked."""
        # changing L1 resets any old L2 detail
        if self._set_attrs({"class_code": code, "class_name": name,
                            "lu_code": 0, "lu_name": ""}) is None:
            return
        if self.cb_level.currentIndex() == LEVEL12:
            self._fill_l2(code)
            self._refresh_no_zoom()
        elif self.chk_auto.isChecked():
            self.step(1)
        else:
            self._refresh_no_zoom()

    def _assign_l2(self, idx):
        lu = self.cb_l2.itemData(idx)
        if lu is None:
            return
        l1, abbr, name = gu.LU2003[lu]
        self._set_attrs({"class_code": l1, "class_name": gu.l1_name(l1),
                         "lu_code": lu, "lu_name": name})
        if self.chk_auto.isChecked():
            self.step(1)
        else:
            self._refresh_no_zoom()

    def clear_label(self):
        self._set_attrs({"class_code": 0, "class_name": "",
                         "lu_code": 0, "lu_name": ""})
        self._refresh_no_zoom()

    def _refresh_no_zoom(self):
        """Update labels/progress without moving the canvas."""
        lyr = self.current_layer()
        if lyr is None or not self.fids:
            return
        f = lyr.getFeature(self.fids[self.pos])
        fields = f.fields().names()
        lu = ""
        if "lu_name" in fields and f["lu_name"]:
            lu = "   L2: {} {}".format(f["lu_code"], f["lu_name"])
        self.lbl_info.setText(
            "point_id: {}   L1: {}   use: {}{}".format(
                f["point_id"] if "point_id" in fields else "-",
                f["class_name"] or "-", f["use"] or "-", lu))
        labeled = sum(1 for ft in lyr.getFeatures()
                      if int(ft["class_code"] or 0) > 0)
        self.lbl_progress.setText(
            "Labeled: {} / {}".format(labeled, lyr.featureCount()))
