# utils.py


from qgis.core import (
    Qgis,
    QgsMessageLog,
    QgsProject,
    QgsVectorLayer,
    QgsSettings,
)

from qgis.core import QgsFeature, QgsField

from qgis.PyQt.QtCore import QSettings, QDate, QCoreApplication, QLocale

from qgis.PyQt import QtCore, QtGui, QtWidgets

from qgis.PyQt.QtWidgets import (
    QTableWidgetItem,
    QTableWidget,
    QMessageBox,
    QDoubleSpinBox,
)
from qgis.utils import iface

from .constantes import CADASTRE_FORMATS

from compat_qt import QVariant

import csv
import os
import itertools
import re
import datetime
import time

#Compatibilité Python < 3.12 : définition manuelle de itertools.batched

try:
    from itertools import batched
except ImportError:
    # Fallback pour Python < 3.12 (QGIS 3.34 LTS)
    from itertools import islice
    def batched(iterable, n):
        it = iter(iterable)
        while True:
            batch = tuple(islice(it, n))
            if not batch:
                return
            yield batch


# ----------------- Gestion traduction -----------------
def tr(message):
    return QCoreApplication.translate("GestionForestiere", message)


#-------------Fonctions utilitaires


def get_csv_path(filename="table_data.csv"):
    """
    Retourne le chemin complet d'un fichier CSV dans le dossier /data du plugin.
    Crée le dossier s'il n'existe pas.

    :param filename: Nom du fichier CSV à créer ou charger.
    :return: Chemin absolu vers le fichier CSV.
    """
    plugin_dir = os.path.dirname(__file__)
    ressources_dir = os.path.join(plugin_dir, "data")
    os.makedirs(ressources_dir, exist_ok=True)  # Crée le dossier /data s’il n’existe pas
    return os.path.join(ressources_dir, filename)


def get_finances_csv_path():
    """
    Retourne le chemin CSV pour les données financières,
    construit dynamiquement selon le nom de la couche active et le projet.

    :return: Chemin vers un fichier CSV du type fin_nomcouche_prefix.csv ou None si couche manquante.
    """
    layer = iface.activeLayer()
    if not layer:
        return None  # Tu peux aussi lever une exception ou afficher un QMessageBox

    layer_name = re.sub(r'\W+', '_', layer.name())  # Nettoyage nom couche : remplace les caractères spéciaux
    project_path = QgsProject.instance().fileName()

    if not project_path:
        project_prefix = "def"
    else:
        project_name = os.path.basename(project_path)
        project_prefix = project_name[:3].lower()

    filename = f"fin_{layer_name}_{project_prefix}.csv"
    return get_csv_path(filename)

# Fonction gèrant la sélection de la coucha active ainsi que le contrôle d'existence des champs
import traceback

def get_valid_active_layer(dlg=None, silent=False):
    """
    Retourne la couche vectorielle active si elle est valide et contient les champs requis.
    Affiche un message d'erreur seulement si silent=False.
    """
    layer = iface.activeLayer()

    if not layer or not isinstance(layer, QgsVectorLayer):
        if not silent:
            QMessageBox.warning(
                dlg,
                tr("Erreur"),
                tr("Aucune couche vectorielle active.")
            )
        return None

    expected_fields = {"id", "section", "numero", "SURFACE", "typeParc", "totalplants"}
    layer_fields = set(field.name() for field in layer.fields())

    if not expected_fields.issubset(layer_fields):
        if not silent:
            QMessageBox.critical(
                dlg,
                tr("Erreur"),
                tr("La couche active ne contient pas tous les champs \n nécessaires à l'utilisation du plugin")
            )
        return None

    return layer

def get_valid_active_layer_start(dlg):
    """
    Retourne la couche vectorielle active si elle est valide et contient les champs requis.
    Affiche un message d'erreur sinon.
    """
    layer = iface.activeLayer()
    if not layer or not isinstance(layer, QgsVectorLayer):
        QMessageBox.warning(
            dlg,
            tr("Erreur"),
            tr("Aucune couche vectorielle active.")
        )
        return None

    expected_fields = {"id", "section","numero", "SURFACE", "typeParc", "totalplants"}
    layer_fields = set(field.name() for field in layer.fields())

    if not expected_fields.issubset(layer_fields):
        QMessageBox.critical(
            dlg,
            tr("Erreur"),
            tr("La couche sélectionnée n'est pas compatible avec le plugin. \n "
            "Sélectionnez une couche valide \n"
            "et relancez le plugin")
        )
        return None
    return layer


# Gestion du symbole monétaire
def get_locale_and_currency():
    settings = QgsSettings()
    locale_name = settings.value("locale/userLocale", QLocale().name())
    locale = QLocale(locale_name)

    # Compatibilité Qt5 et Qt6
    try:
        # Qt6 : currencySymbol() n'accepte plus d'argument
        currency = locale.currencySymbol()
    except TypeError:
        # Qt5 : nécessite QLocale.CurrencySymbol
        currency = locale.currencySymbol(QLocale.CurrencySymbol)

    return locale, currency


# gestion des QlineEdit avec symbole monétaire
def apply_numeric_to_lineedit(lineedit, value, decimals=2, with_currency=True):
    """
    Affiche une valeur formatée dans un QLineEdit, avec ou sans symbole monétaire.
    Compatible Qt5 / Qt6.
    """
    locale, currency = get_locale_and_currency()

    if value is None or value == "":
        lineedit.setText("")
        lineedit.setReadOnly(True)
        return

    # --- Formatage Python (fiable Qt5/Qt6) ---
    try:
        num = float(value)
    except Exception:
        lineedit.setText("")
        lineedit.setReadOnly(True)
        return

    formatted = f"{num:.{decimals}f}"

    # --- Application du séparateur de la locale Qt ---
    decimal_point = locale.decimalPoint()
    if decimal_point != '.':
        formatted = formatted.replace('.', decimal_point)

    # --- Ajout devise ---
    if with_currency and currency:
        lineedit.setText(f"{formatted} {currency}")
    else:
        lineedit.setText(formatted)

    lineedit.setReadOnly(True)


# gestion des doubles spin box avec symbole monetaire
def setup_doublespinbox(
        spinbox: QDoubleSpinBox,
        value=None,
        decimals: int = 2,
        with_currency: bool = True,
        minimum: float = 0.0,
        maximum: float = 100000.0,
    ):
    """
    Configure un QDoubleSpinBox de façon robuste :
    - gère QVariant / None / str / int / float
    - bloque signaux pendant la mise à jour
    - force repaint / update
    - setSuffix selon locale
    """
    # Sécurité : widget correct ?
    if spinbox is None:
        return

    # Locale QGIS (sécurité)
    settings = QgsSettings()
    try:
        locale_name = settings.value("locale/userLocale", QLocale().name())
        locale = QLocale(locale_name)
        currency = locale.currencySymbol(QLocale.CurrencySymbol)
    except Exception:
        locale = QLocale()
        currency = ""

    spinbox.setDecimals(decimals)
    spinbox.setMinimum(minimum)
    spinbox.setMaximum(maximum)

    # conversion robuste
    num = 0.0
    try:
        # QGIS peut renvoyer NULL (ou QVariant) ou des chaînes avec espaces
        if value is None or value == "" or str(value).upper() == "NULL":
            num = 0.0
        else:
            # élimine suffixes éventuels (sécurité)
            s = str(value).strip()
            # retirer espaces et caractères monnaie éventuels (ex: "1 234,00 €")
            # on remplace les espaces insécables qui existent parfois en base
            s = s.replace("\xa0", "").replace(" ", "")
            # remplacer virgule par point pour conversion
            s = s.replace(",", ".")
            num = float(s)
    except Exception:
        num = 0.0

    # Bloquer signaux pour éviter boucles ou handlers perturbants
    try:
        spinbox.blockSignals(True)
        spinbox.setValue(num)
        spinbox.blockSignals(False)
    except Exception:
        # fallback : forcer conversion en float avant setValue
        try:
            spinbox.setValue(float(num))
        except Exception:
            pass

    # Suffixe monnaie
    if with_currency and currency:
        try:
            spinbox.setSuffix(f" {currency}")
        except Exception:
            spinbox.setSuffix("")

    # Forcer refresh visuel
    try:
        spinbox.update()
        spinbox.repaint()
        spinbox.show()
    except Exception:
        pass
def safe_set_text(line_edit, value):
    if line_edit is None:
        return

    if value is None or str(value).upper() in ("NULL", "NONE", ""):
        line_edit.setText("")
    else:
        # Formatage des dates
        if isinstance(value, QDate):
            text = value.toString("dd/MM/yyyy")
        elif isinstance(value, (datetime.date, datetime.datetime)):
            text = value.strftime("%d/%m/%Y")
        else:
            text = str(value)

        line_edit.setText(text)

def safe_set_date(date_edit, value):
    if date_edit is None:
        return
    if value in (None, "", "NULL"):
        # Mettre un texte vide à l'affichage
        date_edit.clear()
    else:
        if isinstance(value, QDate):
            date_edit.setDate(value)
        else:
            try:
                y, m, d = map(int, str(value).split("-"))
                date_edit.setDate(QDate(y, m, d))
            except Exception as e:
                print(f"❌ Impossible de parser la date: {value} ({e})")
                date_edit.clear()

# Extraction des champs section et numero fonction du pays
def extraire_section_numero(id_val: str, code_pays: str) -> tuple[str, str] | tuple[None, None]:
    """Extrait la section et le numéro selon le code pays (hors France)."""
    config = CADASTRE_FORMATS.get(code_pays)
    if not config:
        return None, None

    match = re.match(config["regex"], id_val)
    if match:
        section = match.group(config["section_index"])
        numero = match.group(config["numero_index"])
        return section, numero

    return None, None

# Sauvegarde de la table des données

def save_table_to_csv(table_widget: QTableWidget, csv_path: str):
    try:
        table_widget.setUpdatesEnabled(False)
        table_widget.blockSignals(True)

        start_time = time.perf_counter()
        data = []
        for row in range(table_widget.rowCount()):
            row_data = [
                table_widget.item(row, col).text() if table_widget.item(row, col) else ""
                for col in range(table_widget.columnCount())
            ]
            data.append(row_data)

        with open(csv_path, "w", newline='', encoding='utf-8') as file:
            writer = csv.writer(file)
            writer.writerows(data)

        elapsed_time = time.perf_counter() - start_time
        print(f"✅ Données sauvegardées dans : {csv_path} en {elapsed_time:.4f} s")

    except Exception as e:
        print(f"❌ Erreur lors de la sauvegarde : {e}")

    finally:
        table_widget.blockSignals(False)
        table_widget.setUpdatesEnabled(True)

# Chargement de la table des données

def load_table_from_csv(table_widget, file_path):
    """Charge efficacement un fichier CSV dans un QTableWidget."""
    if not os.path.exists(file_path):
        return

    # Désactive les signaux et mises à jour visuelles pendant le chargement
    table_widget.setUpdatesEnabled(False)
    table_widget.blockSignals(True)

    try:
        with open(file_path, mode='r', newline='', encoding='utf-8') as file:
            reader = csv.reader(file)
            for row_index, row_data in enumerate(reader):
                if row_index >= table_widget.rowCount():
                    table_widget.insertRow(table_widget.rowCount())
                for col_index, value in enumerate(row_data):
                    if col_index < table_widget.columnCount():
                        item = QTableWidgetItem(value)
                        table_widget.setItem(row_index, col_index, item)
    finally:
        # Réactive les signaux et mises à jour
        table_widget.blockSignals(False)
        table_widget.setUpdatesEnabled(True)

def extract_table_values(table_widget, col_indices):
    """
    Extrait pour chaque ligne de table_widget les valeurs des colonnes
    listées dans col_indices, et renvoie une liste de listes.
    """
    data = []
    for row in range(table_widget.rowCount()):
        row_vals = []
        for col in col_indices:
            item = table_widget.item(row, col)
            row_vals.append(item.text() if item else "")
        data.append(row_vals)
    return data

def unique_values_per_column(data):
    """
    À partir d'une liste de lignes (listes de colonnes),
    renvoie pour chaque colonne une liste triée de valeurs uniques.
    """
    if not data:
        return []
    num_cols = len(data[0])
    uniques = [set() for _ in range(num_cols)]
    for row_vals in data:
        for idx, val in enumerate(row_vals):
            uniques[idx].add(val)
    return [sorted(col_set) for col_set in uniques]

def format_date(value):
    """
    Formate une date (QDate, datetime.date ou str) au format 'dd/MM/yyyy'.
    Retourne '???' si la valeur est vide ou invalide.
    """
    if hasattr(value, 'toString'):
        return value.toString("dd/MM/yyyy")  # QDate
    elif hasattr(value, 'strftime'):
        return value.strftime("%d/%m/%Y")    # datetime.date
    elif isinstance(value, str):
        try:
            dt = datetime.datetime.strptime(value, "%Y-%m-%d")  # format standard CSV
            return dt.strftime("%d/%m/%Y")
        except ValueError:
            return value  # retourne la chaîne brute si parsing échoue
    return "???"

def safe_str(value):
    """Convertit une valeur pour affichage dans Qt : vide si None/NULL."""
    if value is None or str(value).upper() in ("NULL", "NONE"):
        return ""
    return str(value)

def get_fill_color_from_layer(layer, field_value):
    if not layer or not isinstance(layer, QgsVectorLayer):
        return None

    renderer = layer.renderer()
    if renderer and renderer.type() == "categorizedSymbol":
        for cat in renderer.categories():
            try:
                if float(cat.value()) == float(field_value):
                    symbol = cat.symbol()
                    for i in range(symbol.symbolLayerCount()):
                        sym_layer = symbol.symbolLayer(i)
                        if hasattr(sym_layer, "fillColor"):
                            return sym_layer.fillColor().name()
                        elif hasattr(sym_layer, "color"):
                            return sym_layer.color().name()
                    return symbol.color().name()
            except (TypeError, ValueError):
                continue
    return None


def extract_features_data(layer, only_possessed=True):
    """
    Extrait les entités sous forme de dictionnaires avec géométrie.
    Si `only_possessed` est True, ne conserve que celles dont 'Possession' est True.
    """
    start = time.time()
    features_data = []

    for f in layer.getFeatures():
        if only_possessed and not f["Possession"]:
            continue

        data = {field.name(): f[field.name()] for field in layer.fields()}
        data["_geom"] = f.geometry()  # on ajoute la géométrie ici
        data["fid"] = f.id()
        features_data.append(data)

    elapsed = time.time() - start
    print(f"[DEBUG] Extraction features_data terminée en {elapsed:.3f}s, {len(features_data)} entités extraites")
    return features_data


# Différentes méthodes de Messages d'erreurs et d'avertissement

def log_debug(message, plugin_name="gestion_forestiere"):
    """Log un message d'information (message traduit automatiquement)."""
    QgsMessageLog.logMessage(tr(message), plugin_name, Qgis.Info)

def log_warning(message, plugin_name="gestion_forestiere"):
    """Log un message d'avertissement (message traduit automatiquement)."""
    QgsMessageLog.logMessage(tr(message), plugin_name, Qgis.Warning)

def log_error(message, plugin_name="gestion_forestiere"):
    """Log un message d'erreur (message traduit automatiquement)."""
    QgsMessageLog.logMessage(tr(message), plugin_name, Qgis.Critical)

def show_success_bar(iface, title, message):
    """Affiche un message de succès dans la barre d'info (titres et messages traduits automatiquement)."""
    iface.messageBar().pushSuccess(tr(title), tr(message))

def show_warning_bar(iface, title, message):
    """Affiche un message d'avertissement dans la barre d'info (titres et messages traduits automatiquement)."""
    iface.messageBar().pushWarning(tr(title), tr(message))

def show_error_bar(iface, title, message):
    """Affiche un message d'erreur dans la barre d'info (titres et messages traduits automatiquement)."""
    iface.messageBar().pushCritical(tr(title), tr(message))

def couche_a_contenu(layer):
    """
    Vérifie si au moins un champ est rempli dans au moins une entité.
    Retourne True si du contenu est présent, sinon False.
    """
    for feat in layer.getFeatures():
        for val in feat.attributes():
            if val not in (None, ''):
                return True
    return False



# Message au premier lancement
def show_update_message_and_create_field(layer):
    """
    Affiche le message de mise à jour et crée le champ 'prixAct' si absent,
    une seule fois par couche. Ne fonctionne que pour les couches qui
    contiennent les champs 'id', 'section' et 'numero'.
    """
    if layer is None or not isinstance(layer, QgsVectorLayer):
        return  # couche invalide

    # Vérifier que la couche contient les champs requis
    field_names = [f.name() for f in layer.fields()]
    required_fields = {"id", "section", "numero"}
    if not required_fields.issubset(field_names):
        return  # couche non compatible

    # Utiliser l'ID unique de la couche pour stocker la version
    layer_id = layer.id()
    settings = QSettings()
    version_key = f"gestion_forestière/last_notified_version_{layer_id}"
    current_version = "1.5.7"
    last_notified = settings.value(version_key, "")

    # Message et création du champ uniquement si pas encore fait pour cette couche
    if last_notified != current_version:

        QMessageBox.information(
            None,
            tr("⚠️ Nouvelle configuration requise depuis la version V1.5.3"),
            tr(
                "Le champ 'prixAct' sera ajouté automatiquement pour la couche {layer_name}.\n"
                "Si ce champ existe déjà, il ne sera pas modifié."
            ).format(layer_name=layer.name())
        )

        # Créer le champ si absent
        if "prixAct" not in field_names:
            if not layer.isEditable():
                layer.startEditing()
            new_field = QgsField("prixAct", QVariant.Int)
            layer.dataProvider().addAttributes([new_field])
            layer.updateFields()
            layer.commitChanges()
            QgsMessageLog.logMessage(
                tr("Champ 'prixAct' créé pour la couche {layer_name}").format(layer_name=layer.name()),
                "gestion_forestiere",
                level=Qgis.Info
            )

        # Sauvegarder la version pour cette couche
        settings.setValue(version_key, current_version)






