# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Siose2Manning
                                 A QGIS plugin
 Extrae el Manning n para HEC-RAS desde SIOSE AR (ID_COBERTURA_MAX)
 Version 0.1.2 - incluye disolucion opcional por Manning + colores estables
 ***************************************************************************/
"""

import os
import csv
import hashlib
from pathlib import Path

from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, QVariant
from qgis.PyQt.QtGui import QIcon, QColor
from qgis.PyQt.QtWidgets import QAction

import processing

from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsField,
    QgsFeature,
    QgsGeometry,
    QgsWkbTypes,
    QgsSymbol,
    QgsCategorizedSymbolRenderer,
    QgsRendererCategory
)

from .Mod3_dialog import Mod3Dialog


class Siose2Manning:

    def __init__(self, iface):
        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)

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

        self.actions = []
        self.menu = self.tr("&SIOSE2Manning")

        self.dlg = None
        self._is_dialog_open = False
        self._action_main = None

    def tr(self, message):
        return QCoreApplication.translate("Siose2Manning", message)

    # ======================================================
    # GUI
    # ======================================================

    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        parent=None
    ):
        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

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

        self.actions.append(action)
        return action

    def initGui(self):
        icon_path = os.path.join(self.plugin_dir, "icon.png")

        self._action_main = self.add_action(
            icon_path,
            text=self.tr("SIOSE AR -> Manning"),
            callback=self.run,
            parent=self.iface.mainWindow()
        )

    def unload(self):
        for action in self.actions:
            self.iface.removePluginMenu(self.tr("&SIOSE2Manning"), action)
            self.iface.removeToolBarIcon(action)

        self.actions = []
        self._action_main = None
        self.dlg = None
        self._is_dialog_open = False

    # ======================================================
    # CSV
    # ======================================================

    def _csv_path(self) -> Path:
        return Path(self.plugin_dir) / "data" / "siose_ar_id_to_manning_es.csv"

    def _load_manning_map(self) -> dict:
        csv_path = self._csv_path()

        if not csv_path.exists():
            raise Exception(f"No se encontro el CSV interno en: {csv_path}")

        mapping = {}

        try:
            f = csv_path.open("r", encoding="utf-8-sig", newline="")
            reader = csv.DictReader(f)
            rows = list(reader)
            f.close()
        except UnicodeDecodeError:
            f = csv_path.open("r", encoding="latin-1", newline="")
            reader = csv.DictReader(f)
            rows = list(reader)
            f.close()

        fields = reader.fieldnames or []

        if "S20" not in fields or "MANNING" not in fields:
            raise Exception("El CSV debe tener columnas 'S20' y 'MANNING'.")

        for row in rows:
            code_raw = (row.get("S20") or "").strip()
            man_raw = (row.get("MANNING") or "").strip()

            if not code_raw or not man_raw:
                continue

            try:
                code = int(code_raw)
                man = float(man_raw.replace(",", "."))
            except Exception:
                continue

            mapping[code] = man

        if not mapping:
            raise Exception("El CSV no cargo ningun mapeo S20 -> MANNING.")

        return mapping

    # ======================================================
    # Buscar capa SIOSE
    # ======================================================

    def _find_siose_layer_in_project(self) -> QgsVectorLayer:
        # Preferir la capa elegida en el diálogo
        if self.dlg is not None:
            try:
                if self.dlg.siose_layer is not None and self.dlg.siose_layer.isValid():
                    return self.dlg.siose_layer
            except Exception:
                pass

        for lyr in QgsProject.instance().mapLayers().values():
            if not isinstance(lyr, QgsVectorLayer) or not lyr.isValid():
                continue
            if lyr.geometryType() != QgsWkbTypes.PolygonGeometry:
                continue
            if "ID_COBERTURA_MAX" in [f.name() for f in lyr.fields()]:
                return lyr

        raise Exception(
            "No se encontro una capa SIOSE AR (T_COMBINADA) con el campo ID_COBERTURA_MAX en el proyecto."
        )

    # ======================================================
    # Crear AOI temporal
    # ======================================================

    def _make_aoi_layer(self, geom: QgsGeometry, crs_authid: str = None) -> QgsVectorLayer:
        if not crs_authid:
            crs_authid = self.iface.mapCanvas().mapSettings().destinationCrs().authid()
        aoi_layer = QgsVectorLayer(f"Polygon?crs={crs_authid}", "AOI_TMP", "memory")

        pr = aoi_layer.dataProvider()
        pr.addAttributes([QgsField("id", QVariant.Int)])
        aoi_layer.updateFields()

        feat = QgsFeature(aoi_layer.fields())
        feat["id"] = 1
        feat.setGeometry(geom)
        pr.addFeature(feat)

        aoi_layer.updateExtents()
        return aoi_layer

    # ======================================================
    # Disolver por Manning
    # ======================================================

    def _dissolve_layer(self, layer: QgsVectorLayer) -> QgsVectorLayer:
        result = processing.run(
            "native:dissolve",
            {
                "INPUT": layer,
                "FIELD": ["MANNING_N"],
                "OUTPUT": "memory:"
            }
        )

        dissolved = result["OUTPUT"]
        dissolved.setName("SIOSE_Manning_AOI")

        field_names = [f.name() for f in dissolved.fields()]
        pr = dissolved.dataProvider()

        if "AREA_M2" not in field_names:
            pr.addAttributes([QgsField("AREA_M2", QVariant.Double)])
        if "AREA_HA" not in field_names:
            pr.addAttributes([QgsField("AREA_HA", QVariant.Double)])

        dissolved.updateFields()

        idx_m2 = dissolved.fields().indexOf("AREA_M2")
        idx_ha = dissolved.fields().indexOf("AREA_HA")

        dissolved.startEditing()

        for f in dissolved.getFeatures():
            area_m2 = f.geometry().area()
            dissolved.changeAttributeValue(f.id(), idx_m2, area_m2)
            dissolved.changeAttributeValue(f.id(), idx_ha, area_m2 / 10000.0)

        dissolved.commitChanges()

        return dissolved

    # ======================================================
    # Colores estables por valor de Manning
    # ======================================================

    def _stable_color_from_value(self, value) -> QColor:
        """
        Genera un color pseudoaleatorio pero estable a partir del valor.
        Mismo Manning -> mismo color siempre.
        """
        text = f"{float(value):.6f}"
        digest = hashlib.sha256(text.encode("utf-8")).hexdigest()

        r = int(digest[0:2], 16)
        g = int(digest[2:4], 16)
        b = int(digest[4:6], 16)

        # Evitar colores muy oscuros o muy apagados
        r = 70 + (r % 156)
        g = 70 + (g % 156)
        b = 70 + (b % 156)

        return QColor(r, g, b)

    def _apply_manning_categories(self, layer: QgsVectorLayer):
        """
        Aplica simbología categorizada por MANNING_N con color estable por valor.
        La leyenda aparece automáticamente en el panel de capas.
        """
        values = set()

        for f in layer.getFeatures():
            val = f["MANNING_N"]
            if val is not None:
                try:
                    values.add(float(val))
                except Exception:
                    pass

        values = sorted(values)

        categories = []

        for val in values:
            symbol = QgsSymbol.defaultSymbol(layer.geometryType())
            symbol.setColor(self._stable_color_from_value(val))

            label = f"{val:.6f}".rstrip("0").rstrip(".")
            category = QgsRendererCategory(val, symbol, label)
            categories.append(category)

        renderer = QgsCategorizedSymbolRenderer("MANNING_N", categories)
        layer.setRenderer(renderer)
        layer.triggerRepaint()

    # ======================================================
    # Proceso principal
    # ======================================================

    def _run_step3(self, aoi_geom: QgsGeometry):
        manning_map = self._load_manning_map()

        # Cargar SIOSE siempre desde el GPKG directamente (evita geometrias invalidas expuestas)
        siose_layer = None
        if self.dlg is not None:
            gpkg_path = getattr(self.dlg, "siose_gpkg_path", None)
            siose_lyr_ref = getattr(self.dlg, "siose_layer", None)
            layer_name = siose_lyr_ref.name() if siose_lyr_ref else None

            if gpkg_path and layer_name:
                uri = f"{gpkg_path}|layername={layer_name}"
                fresh_layer = QgsVectorLayer(uri, layer_name, "ogr")
                if fresh_layer.isValid():
                    siose_layer = fresh_layer

        if siose_layer is None:
            siose_layer = self._find_siose_layer_in_project()

        # guardar estilo original para cuando NO se disuelva
        src_renderer = None
        try:
            if siose_layer.renderer() is not None:
                src_renderer = siose_layer.renderer().clone()
        except Exception:
            src_renderer = None

        # Obtener CRS del AOI y del SIOSE
        aoi_crs = getattr(self.dlg, "aoi_crs", None)
        aoi_crs_authid = aoi_crs.authid() if aoi_crs and aoi_crs.isValid() else None
        siose_crs_authid = siose_layer.crs().authid()

        # Crear capa AOI con su CRS original
        aoi_layer = self._make_aoi_layer(aoi_geom, aoi_crs_authid)

        # Reproyectar AOI al CRS del SIOSE si son diferentes
        if aoi_crs_authid and aoi_crs_authid != siose_crs_authid:
            reproject_result = processing.run(
                "native:reprojectlayer",
                {
                    "INPUT": aoi_layer,
                    "TARGET_CRS": siose_crs_authid,
                    "OUTPUT": "memory:"
                }
            )
            aoi_layer = reproject_result["OUTPUT"]

        # Extraer subset por bbox usando QgsFeatureRequest (sin processing, sin fallos)
        from qgis.core import QgsFeatureRequest, QgsVectorLayer as QVL, QgsFeature
        from qgis.PyQt.QtCore import QVariant

        bbox = aoi_layer.extent()
        # Reproyectar bbox al CRS del SIOSE si es necesario
        if aoi_crs_authid and aoi_crs_authid != siose_crs_authid:
            from qgis.core import QgsCoordinateTransform, QgsCoordinateReferenceSystem
            xform = QgsCoordinateTransform(
                QgsCoordinateReferenceSystem(aoi_crs_authid),
                QgsCoordinateReferenceSystem(siose_crs_authid),
                QgsProject.instance()
            )
            bbox = xform.transformBoundingBox(bbox)

        # Crear capa memoria solo con features del bbox (rapido, sin processing)
        subset_layer = QVL(
            f"Polygon?crs={siose_crs_authid}", "SIOSE_SUBSET", "memory"
        )
        subset_layer.dataProvider().addAttributes(siose_layer.fields().toList())
        subset_layer.updateFields()

        request = QgsFeatureRequest().setFilterRect(bbox)
        request.setFlags(
            QgsFeatureRequest.Flag.NoGeometry.__class__(0)
            if hasattr(QgsFeatureRequest, 'Flag') else QgsFeatureRequest.NoFlags
        )
        feats = []
        for feat in siose_layer.getFeatures(request):
            geom = feat.geometry()
            if geom is None or geom.isEmpty():
                continue
            new_feat = QgsFeature(subset_layer.fields())
            new_feat.setGeometry(geom)
            new_feat.setAttributes(feat.attributes())
            feats.append(new_feat)
        subset_layer.dataProvider().addFeatures(feats)
        subset_layer.updateExtents()

        # Fix solo sobre el subset pequeno (rapido)
        fix_result = processing.run(
            "native:fixgeometries",
            {
                "INPUT": subset_layer,
                "METHOD": 1,
                "OUTPUT": "TEMPORARY_OUTPUT"
            }
        )
        fix_output = fix_result["OUTPUT"]
        if isinstance(fix_output, QgsVectorLayer):
            siose_fixed = fix_output
        else:
            siose_fixed = QgsVectorLayer(fix_output, "SIOSE_FIXED", "ogr")

        # Clip sobre el subset reparado
        result_clip = processing.run(
            "native:clip",
            {
                "INPUT": siose_fixed,
                "OVERLAY": aoi_layer,
                "OUTPUT": "memory:"
            }
        )
        tmp_layer = result_clip["OUTPUT"]

        # conservar campos útiles
        keep_candidates = ["id", "REFCAT", "ID_COBERTURA_MAX"]
        tmp_fields = [f.name() for f in tmp_layer.fields()]
        keep_fields = [f for f in keep_candidates if f in tmp_fields]

        if "ID_COBERTURA_MAX" not in keep_fields and "ID_COBERTURA_MAX" in tmp_fields:
            keep_fields.append("ID_COBERTURA_MAX")

        result_keep = processing.run(
            "native:retainfields",
            {
                "INPUT": tmp_layer,
                "FIELDS": keep_fields,
                "OUTPUT": "memory:"
            }
        )

        out_layer = result_keep["OUTPUT"]
        out_layer.setName("SIOSE_Manning_AOI")

        # añadir campos
        pr = out_layer.dataProvider()
        pr.addAttributes([
            QgsField("MANNING_N", QVariant.Double),
            QgsField("AREA_M2", QVariant.Double),
            QgsField("AREA_HA", QVariant.Double),
        ])
        out_layer.updateFields()

        idx_code = out_layer.fields().indexOf("ID_COBERTURA_MAX")
        idx_man = out_layer.fields().indexOf("MANNING_N")
        idx_m2 = out_layer.fields().indexOf("AREA_M2")
        idx_ha = out_layer.fields().indexOf("AREA_HA")

        if idx_code == -1:
            raise Exception("La capa resultante no tiene el campo ID_COBERTURA_MAX. Revise que esta usando T_COMBINADA.")

        missing = set()

        out_layer.startEditing()

        for f in out_layer.getFeatures():
            fid = f.id()

            try:
                code_int = int(f[idx_code])
            except Exception:
                code_int = None

            man = None
            if code_int is not None:
                man = manning_map.get(code_int)
                if man is None:
                    missing.add(code_int)

            area_m2 = f.geometry().area()

            out_layer.changeAttributeValue(fid, idx_man, man)
            out_layer.changeAttributeValue(fid, idx_m2, area_m2)
            out_layer.changeAttributeValue(fid, idx_ha, area_m2 / 10000.0)

        out_layer.commitChanges()

        do_dissolve = False
        if self.dlg is not None:
            try:
                do_dissolve = self.dlg.get_dissolve_checked()
            except Exception:
                do_dissolve = False

        if do_dissolve:
            out_layer = self._dissolve_layer(out_layer)
            self._apply_manning_categories(out_layer)
        else:
            if src_renderer is not None:
                try:
                    out_layer.setRenderer(src_renderer)
                    out_layer.triggerRepaint()
                except Exception:
                    pass

        # Añadir capa y moverla al tope del panel
        QgsProject.instance().addMapLayer(out_layer, False)
        root = QgsProject.instance().layerTreeRoot()
        root.insertLayer(0, out_layer)
        self.iface.setActiveLayer(out_layer)

        if missing:
            self.iface.messageBar().pushWarning(
                "SIOSE -> Manning",
                f"Capa creada, pero faltan codigos en el CSV para: {sorted(missing)}"
            )
        else:
            msg = "Capa resultado creada correctamente."
            if do_dissolve:
                msg += " Se aplico disolucion por Manning y colores por categoria."
            self.iface.messageBar().pushInfo(
                "SIOSE -> Manning",
                msg
            )
            # Limpiar dialogo para proxima ejecucion
            if self.dlg is not None:
                self.dlg.reset_after_execution()

    # ======================================================
    # Ejecutar
    # ======================================================

    def run(self):
        if self._is_dialog_open and self.dlg is not None:
            self.dlg.show()
            self.dlg.raise_()
            self.dlg.activateWindow()
            return

        if self.dlg is None:
            self.dlg = Mod3Dialog(self.iface)
            self.dlg.finished.connect(self._on_dialog_finished)

        self._is_dialog_open = True

        if self._action_main is not None:
            self._action_main.setEnabled(False)

        self.dlg.show()
        result = self.dlg.exec()

        if result:
            try:
                self._run_step3(self.dlg.aoi_geom)
            except Exception as e:
                self.iface.messageBar().pushCritical("SIOSE -> Manning", str(e))

    def _on_dialog_finished(self, _code):
        self._is_dialog_open = False
        if self._action_main is not None:
            self._action_main.setEnabled(True)