# -*- coding: utf-8 -*-
"""
/***************************************************************************
 Gestion_forestiere
                                 A QGIS plugin
 Affichage coordonnées sur click souris
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2025-03-31
        git sha              : $Format:%H$
        copyright            : (C) 2025 - 2026 by Marc GROSJEAN
        email                : marc.grosjean@wanadoo.fr
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""
import sys
import os
import csv
import subprocess
import re
import datetime
import time

sys.path.append(os.path.dirname(__file__))

from gestion_forestiere import resources_rc

# ----------------- Imports Qt / PyQt -----------------
from qgis.PyQt.QtCore import (
    QSettings,
    QTranslator,
    QCoreApplication,
    QDate,
    QTimer,
    QUrl,
    QThread,
    QSize,
    Qt,
)
from qgis.PyQt.QtGui import (
    QIcon,
    QDesktopServices,
    QFont,
    QColor,
    QTextCursor,
)
from qgis.PyQt.QtWidgets import (
    QAction,
    QTableWidgetItem,
    QApplication,
    QMessageBox,
    QHeaderView,
    QDialog,
    QInputDialog,
    QTableWidget,
    QDateEdit,
    QToolButton,
)

# ----------------- Imports QGIS -----------------
from qgis.gui import QgsMapToolEmitPoint, QgsMapToolPan
from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsFeature,
    QgsGeometry,
    QgsPointXY,
    QgsFeatureRequest,
    edit,
    QgsMessageLog,
    Qgis,
    QgsPalLayerSettings,
    QgsVectorLayerSimpleLabeling,
    QgsRuleBasedLabeling,
    QgsTextFormat,
    QgsTextBufferSettings,
    QgsRectangle,
)

from qgis.PyQt import QtCore, QtGui, QtWidgets

# ----------------- Imports internes -----------------
from .coord_click_dialog import CoordClickDialog  # ta fenêtre principale (hérite de QDialog + setupUi)

# ----------------- Imports Python standard -----------------
from datetime import datetime


# Importations des données de la table des données depuis fichier utils
from .utils import (
    safe_set_text,
    extract_table_values,
    unique_values_per_column,
    log_debug,
    log_warning,
    log_error,
    show_success_bar,
    format_date,
    get_valid_active_layer,
    get_valid_active_layer_start,
    show_error_bar,
    safe_str,
    load_table_from_csv,
    save_table_to_csv,
    get_fill_color_from_layer,
    batched,
    tr,
    safe_set_date,
    get_csv_path,
    get_finances_csv_path,
    show_update_message_and_create_field,
    extract_features_data,
    apply_numeric_to_lineedit,
    setup_doublespinbox,
    get_locale_and_currency,
)

# importations fichier analyses.py
from .Analyse import (
    export_typeval_excel,
    export_standard_excel,

    )

# importations constantes

from .constantes import (
    TYPE_PARC_LIBELLES,
    SAISIE_COMBO_COUNTS,
    SAISIE_COMBO_SETTINGS,
    PLUGIN_VERSION,
)

from .analyse_worker import AnalyseWorker

from .finance_manager import FinanceManager

# importations fichier config.py
from .param import ConfigDialog


#Import outil fill ring
from .fill_ring_tool import FillRingTool

#Importation fenêtre gestion infos polygones
from .infos_polygon import InfosPolygonManager
from .create_polygon_dialog import Ui_createPolygonDialog

from .group_polygon import GroupPolygonTool
from .dialog_group import DialogGroup

# importation pour compatibilité QT5/QT6
from .compat_qt import (
    ItemIsEditable,
    MatchFixedString,
    HeaderStretch,
    QVariant,
    exec_dialog,
    disable_native_form
)

from .analyse_worker import AnalyseWorker



class FenetrePrincipale(CoordClickDialog):  # Fenêtre principale, héritée de CoordClickDialog en attente
    def __init__(self):
        super().__init__()
        print('fenetre principale')


class CoordClick:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        start = time.perf_counter()
        self.start_time = time.time()

        """Constructor.

        :param iface: An interface instance that will be passed to this class
        :type iface: QgsInterface
        """
        super().__init__()  #new
        # Save reference to the QGIS interface
        self.iface = iface
        self.canvas = iface.mapCanvas()  #new
        self.layer = None
        #self.is_analyzing = False

        # Initialisation contrôle regroupement actif ou non
        self.group_mode_active = False

        # Créer un objet pan_tool pour reprendre la main plus tard
        self.pan_tool = QgsMapToolPan(self.canvas)

        # Variable pour suivre si une analyse est en cours
        self.analyse_en_cours = False
        # Variable pour stocker une couche en attente d'analyse
        self.layer_pending = None

        QgsProject.instance().cleared.connect(self.on_project_closed)

        # Verrouillage de l'analyse au moment de l'init
        self.plugin_just_loaded = True

        # Affichage d’un message pour prévenir l’utilisateur
        QgsProject.instance().readProject.connect(self.on_project_loaded)


        # Initialisation du répertoire plugin
        self.plugin_dir = os.path.dirname(__file__)
        # Créer (si besoin) le dossier ressources dans le plugin
        self.ressources_dir = os.path.join(self.plugin_dir, "data")
        os.makedirs(self.ressources_dir, exist_ok=True)

        #Définir le chemin complet vers le CSV dans ressources
        self.csv_path = os.path.join(self.ressources_dir, "table_data.csv")

        self.first_start = True   #new

        # Création de self.dlg
        self.dlg = CoordClickDialog()

        # Initialisation N° version plugin
        self.dlg.lblVersion.setText(f"<a href='version'>v{PLUGIN_VERSION}</a>")
        self.dlg.lblVersion.linkActivated.connect(self.on_version_clicked)

        # Appel la fonction de date aujourd'hui des QDateEdit
        set_all_qdateedit_to_today(self.dlg)
        self.first_show = True
        self.last_position = None

        self.last_click_point = None

        # Création du dialog de configuration et passage de iface
        self.config_dialog = ConfigDialog(self.iface.mainWindow(), self.iface)

        self.translators = []

        # Création de la boîte de dialogue creation polygone
        self.create_polygon_dialog = QDialog()
        self.ui_create_polygon = Ui_createPolygonDialog()
        self.ui_create_polygon.setupUi(self.create_polygon_dialog)

        # Charger la Qtablewidget masquée en tampon
        self.dlg.tableWidgetFinance.setColumnCount(6)
        self.dlg.tableWidgetFinance.setHorizontalHeaderLabels(
            ["ID Parcelle", "Date","Libellé", "Type", "Montant", "Commentaire"])
        self.dlg.tableWidgetFinance.hide()  # en plus, si tu veux être sûr qu’elle soit cachée

        # Initialisation de la valeur parcelle_id
        # pour le contrôle du fichier finances.csv
        self.dlg.current_parcelle_id = None

        # Instanciation FinanceManager avec la dlg qui contient maintenant la table
        self.finance_manager = FinanceManager(self.dlg)

        self.dlg.tableWidgetData.cellChanged.connect(self.refresh_combo_finance)

        # Initialise le symbole monétaire dans le champ lineEditCout, recettes.dépense
        setup_doublespinbox(self.dlg.lineEditCout, value=0.0, decimals=2, with_currency=True)


        # Initialisation de l'export
        self._export_in_progress = False

        # Connexion des combos
        self.connect_saisie_combos()

        # Initialisation des attributs
        self.current_feature_id = None
        self.first_show = True
        self.last_position = None

        # Rend les champs de l'onglet propriétaire non modifiables
        self.dlg.nomProp.setReadOnly(True)
        self.dlg.lineAdress.setReadOnly(True)
        self.dlg.lineMail.setReadOnly(True)
        self.dlg.lineTel.setReadOnly(True)

        # initialise les variables pour analyse_worker
        self.analyse_results = None
        self.analyse_thread = None
        self.analyse_worker = None

        # Gestion couleur Bouton A propos de  quand on passe dessus
        self.dlg.toolButtonAbout.setAutoRaise(False)
        self.dlg.toolButtonAbout.setStyleSheet("""
        QToolButton#toolButtonAbout {
            background-color: #f0f0f0;
            border: 1px solid #888;
            border-radius: 6px;
            padding: 4px;
        }
        QToolButton#toolButtonAbout:hover {
            background-color: #0044FF;
        }
        """)

        # Mettre les titres de colonnes en gras
        header = self.dlg.tableWidgetData.horizontalHeader()
        header.setSectionResizeMode(HeaderStretch)
        font = header.font()
        font.setBold(True)
        header.setFont(font)

        # 🔗 Connexion de la case à cocher au changement d'état
        self.dlg.checkEditProp.stateChanged.connect(self.toggle_edit_mode)
        self.dlg.btnSaveProp.setEnabled(False)
        self.toggle_edit_mode()

        # Initialisation du répertoire plugin
        self.plugin_dir = os.path.dirname(__file__)

        # Récupération du canvas courant
        self.canvas = self.iface.mapCanvas()

        # Création d'un outil émettant un point au clic
        self.point_tool = QgsMapToolEmitPoint(self.canvas)

        locale = QSettings().value('locale/userLocale')[0:2]
        print(f"[DEBUG] Langue détectée : {locale}")


        # Liste des fichiers de traduction à charger, sans la partie locale ni extension
        translation_files = [
            'coord_click_dialog_base',
            'constantes',
            'infos_polygon',
            'gestion_forestiere',
            'param',
            'create_polygon_dialog',
            'utils',
            'ui_config_dialog',
            'create_fields',
            'cadastre_manager',
            'finance_manager',
        ]

        locale = QSettings().value('locale/userLocale')[0:2]

        for base_name in translation_files:
            locale_path = os.path.join(self.plugin_dir, 'i18n', f"{base_name}_{locale}.qm")
            if os.path.exists(locale_path):
                translator = QTranslator()
                translator.load(locale_path)
                QCoreApplication.installTranslator(translator)
                self.translators.append(translator)

        # Déclaration menu
        self.actions = []


        # Controle de la mise à jour de la table
        self.table_update_in_progress = False

        # Connecte ma fenêtre pour saisie des infos
        self.infos_polygon_manager = InfosPolygonManager(self.ui_create_polygon, self.create_polygon_dialog)


        # Connexion bouton Sauvegarde prix et date achat
        self.dlg.btnRecPrix.clicked.connect(self.save_prix_date)

        # Initialisation de la case à cocher et bouton enregistrer
        self.dlg.btnSaveProp.clicked.connect(self.save_proprietaire_info)

        # Initialisation de l’état propriétaire
        self.is_proprietaire = False

        # 🔁 Mise à jour initiale et dynamique de la visibilité des onglets
        self.update_tab_visibility()
        self.dlg.checkBoxConfig.toggled.connect(self.update_tab_visibility)


        # 🔄 Connexion au changement de couche
        self.iface.currentLayerChanged.connect(self.on_current_layer_changed)

        end = time.perf_counter()
        print(f"Temps __init_ : {end - start:.3f} s")

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('GestionForestiere', message)

    #    return QCoreApplication.translate('CoordClick', message)

    def add_action(
            self,
            icon_path,
            text,
            callback,
            enabled_flag=True,
            add_to_menu=True,
            add_to_toolbar=True,
            status_tip=None,
            whats_this=None,
            parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """
        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            # Adds plugin icon to Plugins toolbar
            self.iface.addToolBarIcon(action)

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

        self.actions.append(action)

        return action

    def initGui(self):
        print(f"[DEBUG] Connexion faite depuis instance {id(self)}")
        """Create the menu entries and toolbar icons inside the QGIS GUI."""
        # Action principale - Gestion parcelles forestières
        icon_path = os.path.join(self.plugin_dir,'icons', 'icon.png')
        self.action_main = QAction(QIcon(icon_path), tr('Gestion parcelles forestières'), self.iface.mainWindow())
        self.action_main.triggered.connect(self.run)
        self.iface.addPluginToMenu(tr('Gestion parcelles forestières'), self.action_main)
        self.iface.addToolBarIcon(self.action_main)

        # Action secondaire - Configuration Plugin
        icon2_path = os.path.join(self.plugin_dir,'icons', 'icon2.png')
        self.action_config = QAction(QIcon(icon2_path), tr('Configuration Plugin'), self.iface.mainWindow())
        self.action_config.triggered.connect(self.run_config_dialog)
        self.iface.addPluginToMenu(tr('Gestion parcelles forestières'), self.action_config)
        self.iface.addToolBarIcon(self.action_config)

        # Action trois - Page aide au démarrage
        icon3_path = os.path.join(self.plugin_dir,'icons', 'icon3.png')
        self.action_helpgen = QAction(QIcon(icon3_path), tr('Lire Avant'), self.iface.mainWindow())
        self.action_helpgen.triggered.connect(self.open_help_gen)
        self.iface.addPluginToMenu(tr('Gestion parcelles forestières'), self.action_helpgen)
        self.iface.addToolBarIcon(self.action_helpgen)

        # Liste des actions pour suppression dans unload()
        self.actions = [self.action_main, self.action_config, self.action_helpgen]

        layer = self.iface.activeLayer()
        if isinstance(layer, QgsVectorLayer) and layer.geometryType() == Qgis.GeometryType.Polygon:
            try:
                disable_native_form(layer)
                layer.featureAdded.connect(self.infos_polygon_manager.on_feature_added)
            except Exception as e:
                QgsMessageLog.logMessage(
                    f"Erreur lors de la connexion au signal featureAdded : {e}",
                    "coord_click",
                    level=Qgis.Warning
                )

            # Affichage du message au démarrage (une seule fois)
            show_update_message_and_create_field(layer)

        # Flag d'initialisation
        self.first_start = True

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(
                tr(u'Gestion parcelles forestières'),
                action)
            self.iface.removeToolBarIcon(action)

        # Déconnexion du clic sur la carte (point_tool)
        try:
            self.point_tool.canvasClicked.disconnect(self.display_point)
        except Exception:
            pass

        # Revenir à l'outil de navigation (curseur main)
        self.iface.mapCanvas().setMapTool(self.pan_tool)


    def run(self):
        """Run method that performs all the real work"""

        # Vérifie que la couche active est valide et contient les champs requis au démarrage

        start = time.perf_counter()

        layer = get_valid_active_layer_start(self.dlg)
        if not layer:
            return  # Stoppe le lancement du plugin si la couche est invalide


        self.layer = layer  # Enregistre la couche si valide

        # Connexion au changement de couche active
        print("[DEBUG] Connexion du signal currentLayerChanged")
        self.iface.layerTreeView().currentLayerChanged.connect(self.on_layer_selected_from_tree)

        # Met à jour le nom de la couche dans l'interface

        self.dlg.lineLayerAct.setText(self.layer.name())  #new

        # Affiche la boîte de dialogue *avant* de lancer les calculs
        # self.dlg.show()
        # self.dlg.raise_()
        # QCoreApplication.processEvents()  # Force l'affichage du message et du dialog

        # Charge les données au lancement de la fenêtre
        self.finance_manager.initialize()

        if self.first_start:
            self.first_start = False
        #    self.dlg = CoordClickDialog()

            self.print_tab_indexes()

            # Affichage de la couche active dans le champ
            self.dlg.lineLayerAct.setText(self.layer.name())

            # Lancement de l'aide au plugin
            self.dlg.btnHelp.clicked.connect(self.open_help)

            #Remplir le combo Modif légende A vérifier si utile
            self.remplir_combo_modif_legend()

            # Connexion du bouton About
            self.dlg.toolButtonAbout.clicked.connect(self.show_about)

            # Charger les données CSV dans la table au démarrage
            load_table_from_csv(self.dlg.tableWidgetData, self.csv_path)
            self.enable_table_editing()

            # Récupération icône bouton recherche
            # --- Configuration du bouton ---
            btn = self.dlg.btnSearch

            # Icônes normale et survol
            icone_normale = QIcon(":/plugins/gestion_forestiere/icons/search.png")
            icone_hover = QIcon(":/plugins/gestion_forestiere/icons/search_hover.png")

            # Configuration initiale
            btn.setIcon(icone_normale)
            btn.setIconSize(QSize(50, 50))

            # Feuille de style pour fond rond + couleur au survol
            btn.setStyleSheet("""
            QToolButton {
                border-radius: 25px;             /* moitié largeur/hauteur pour rond */
                background-color: #dcdcdc;       /* couleur normale */
                border: 2px solid #888;          /* contour */
            }
            QToolButton:hover {
                background-color: #0044FF;       /* couleur au survol */
            }
            QToolButton:pressed {
                background-color: #ffaa00;       /* couleur au clic */
            }
            """)

            # Gestion du changement d'icône au survol
            def on_enter(event):
                btn.setIcon(icone_hover)
                event.accept()

            def on_leave(event):
                btn.setIcon(icone_normale)
                event.accept()

            btn.enterEvent = on_enter
            btn.leaveEvent = on_leave

            # Connexion bouton Search
            self.dlg.btnSearch.clicked.connect(self.rechercher_parcelle)

            # Lancement de l'analyse en arrière-plan
            print("[DEBUG] appel start_analyse_worker")
            self.start_analyse_worker()


            #Bouton de sauvegarde des données
            self.dlg.btnSaveData.clicked.connect(self.save_table_data)

            # Appel sauvegarde fichier Excel finances
            self.finance_manager.load_finance_from_csv()

            # Ensuite remplir tous les combos de saisie d'un coup
            for prefix, combo_base_name, column_index in SAISIE_COMBO_SETTINGS:
                self.populate_saisie_combos(prefix, combo_base_name, column_index)


            # Connecte la checkbox à la méthode de gestion des champs
            self.dlg.checkEditProp.stateChanged.connect(self.toggle_edit_mode)

            # Connecte le bouton de sauvegarde
        #    self.dlg.btnSaveProp.clicked.connect(self.save_proprietaire_info)
            # Applique l'état initial des champs (cochée ou pas) pour l'onglet Propriétaire
            self.toggle_edit_mode()

            # Remplir le combo au lancement
            self.refresh_combo_finance()

            # Connexion du bouton d'enregistrement de l'onglet Saisie
            self.connect_enregistrement_boutons()

            # Connexion bouton Enregistrement Financier
            self.finance_manager = FinanceManager(self.dlg)
            self.dlg.btnSaveFinance.clicked.connect(self.handle_finance_entry)

            # Connexion bouton ouverture fichier finances.csv
            self.dlg.btnOpenCsv.clicked.connect(self.open_finances_csv)

            # Connecte le signal pour cliquer sur la carte
            self.point_tool.canvasClicked.connect(self.display_point)


        # Définit gestion_forestiere comme outil actif
        self.canvas.setMapTool(self.point_tool)

        # Active les étiquettes dès le lancement
        self.afficher_labels_dynamiques_sur_couche()

        # Connexion des cases à cocher pour l'export
        self.init_export_connections()

        end = time.perf_counter()
        print(f"Temps __init__ CoordClick : {end - start:.3f} s")

    def run_config_dialog(self):

        # Activer le plugin
        self.plugin_active = True

        # Récupérer la couche active
        layer = self.iface.activeLayer()

        # Vérifier si la couche est une couche vecteur au format .gpkg
        if not isinstance(layer, QgsVectorLayer):
            QMessageBox.warning(
                self.iface.mainWindow(),
                tr("Format non valide"),
                tr(
                    "La couche sélectionnée n'est pas une couche vecteur.\n"
                    "Merci de sélectionner une couche compatible."
                )
            )
            return


        # Créer une instance de ConfigDialog et afficher la boîte de dialogue
        self.config_dialog = ConfigDialog(self.iface.mainWindow(), self.iface)
        exec_dialog(self.config_dialog)

    def start_analyse_worker(self):
        """Prépare les données et lance le thread d’analyse."""
        if not self.layer or not self.layer.isValid():
            print("[INFO] Pas de couche valide, analyse non lancée")
            return

        if self.analyse_en_cours:
            print("[INFO] Analyse déjà en cours, lancement ignoré")
            return

        self.analyse_en_cours = True
        self.analyse_results = None

        # --- 🔹 Étape 1 : copie locale des données ---
        print("[DEBUG] Copie locale des attributs avant analyse...")
        features_data = []
        for f in self.layer.getFeatures():
            features_data.append({
                "Possession": f["Possession"],
                "typeParc": f["typeParc"],
                "SURFACE": f["SURFACE"],
                "indice_parc": f["indice_parc"]
            })
        print(f"[DEBUG] Copie terminée ({len(features_data)} entités).")

        # --- 🔹 Étape 2 : lancement du worker dans un thread ---
    #    from .analyse_worker import AnalyseWorker  # import local pour éviter cycles

        # Extraire les données une seule fois
    #    features_data = extract_features_data(self.layer)
        features_data = extract_features_data(self.layer, only_possessed=True)

        self.analyse_thread = QThread()
        self.analyse_worker = AnalyseWorker(features_data)
        self.analyse_worker.moveToThread(self.analyse_thread)

        self.analyse_thread.started.connect(self.analyse_worker.run)
        self.analyse_worker.finished.connect(self.on_analyse_finished)
        self.analyse_worker.finished.connect(self.analyse_thread.quit)
        self.analyse_worker.finished.connect(self.analyse_worker.deleteLater)
        self.analyse_thread.finished.connect(self.analyse_thread.deleteLater)

        self.analyse_thread.start()

    def on_analyse_finished(self, results):
        print("[DEBUG] on_analyse_finished reçu avec results :", results)
        self.analyse_results = results
        self.analyse_en_cours = False  # 🔓 Fin d’analyse
        self.data_analyse()  # mettre à jour l'interface avec les résultats

        if self.layer:
            self.iface.messageBar().pushMessage(
                tr("Analyse terminée"),
                tr(f"Fin des calculs pour la couche : {self.layer.name()}"),
                level=Qgis.Info,
                duration=5  # secondes
            )

    def init_supparc_button(self):
        button = self.dlg.btnSupParcelle

        if button is None:
            return

        # Appliquer un style rouge clair avec du texte visible
        button.setStyleSheet("""
            QPushButton {
                background-color: #f28b82;  /* rouge clair */
                color: black;
                font-weight: bold;
                border-radius: 4px;
                padding: 4px 8px;
            }
            QPushButton:hover {
                background-color: #FF0000;  /* rouge un peu plus foncé au survol */
            }
        """)

    # Activation de la fenêtre de regroupement de parcelles
    def activate_group_polygon_tool(self):
        if hasattr(self, 'group_dialog') and self.group_dialog is not None:
            # Dialog déjà ouverte
            self.group_dialog.raise_()
            self.group_dialog.activateWindow()
            return

        from .dialog_group import DialogGroup
        self.group_dialog = DialogGroup(
            iface=self.iface,
            layer=self.iface.activeLayer(),
            parent=None
        )

        self.dlg.hide()
        self.group_dialog.finished.connect(self.on_group_dialog_closed)
        self.group_dialog.show()
        self.group_dialog.raise_()
        self.group_dialog.activateWindow()

    def on_group_dialog_closed(self):
        """Restaure la fenêtre principale et réactive l’outil de clic"""
        self.dlg.show()
        self.group_dialog = None

        # 🔥 Réactiver ton outil de clic normal
        if hasattr(self, "point_tool") and self.point_tool is not None:
            self.canvas.setMapTool(self.point_tool)

    def init_group_polygon_button(self):
        button = self.dlg.btnGroupPolygon

        if button is None:
            return

        # Déconnexion propre
        try:
            button.clicked.disconnect(self.activate_group_polygon_tool)
        except TypeError:
            pass

        layer = self.iface.activeLayer()
        if not layer:
            return

        # Instanciation de l'outil de regroupement
        from .group_polygon import GroupPolygonTool
        self.group_polygon_tool = GroupPolygonTool(
            self.canvas,
            layer
        )

        # Connexion bouton → activation
        button.clicked.connect(self.activate_group_polygon_tool)

        # Style bouton
        button.setStyleSheet("""
            QPushButton {
                background-color: #99ccff;
                font-weight: bold;
                color: black;
                border-radius: 4px;
                padding: 4px 8px;
            }
            QPushButton:hover {
                background-color: #3399ff;
            }
        """)


    def init_ring_fill_button(self):
        button = self.dlg.btnRingFill

        if button is None:
            return

        # Déconnecter proprement sans masquer toutes les exceptions
        try:
            button.clicked.disconnect(self.activate_fill_ring_tool)
        except TypeError:
            pass  # Aucun slot connecté, pas de souci

        # Récupère la couche active et instancie l'outil Fill Ring
        layer = self.iface.activeLayer()
        self.fill_ring_tool = FillRingTool(self.canvas, layer, self.on_fill_ring_completed)

        # Connecter le bouton à l'activation de l'outil
        button.clicked.connect(self.activate_fill_ring_tool)

        # Appliquer la couleur au bouton
        button.setStyleSheet("""
            QPushButton {
                background-color: #ffff66;  /* jaune clair (équivalent à "yellow") */
                font-weight: bold;
                color: black;
                border-radius: 4px;
                padding: 4px 8px;
            }
            QPushButton:hover {
                background-color: #ffd700;  /* jaune foncé/or (style bouton hover) */
            }
        """)

    def activate_fill_ring_tool(self):
        """Active l’outil Fill Ring pour dessiner un anneau/polygone."""
        self.canvas.setMapTool(self.fill_ring_tool)

    def on_fill_ring_completed(self, geometry):
        print("Géométrie créée :", geometry.asWkt())

        # Appel de la boîte de dialogue
        self.infos_polygon_manager.dlg.show()

    def print_tab_indexes(self):
        tab_widget = self.dlg.tabWidget
        for i in range(tab_widget.count()):
            print(f"Onglet {i} : '{tab_widget.tabText(i)}'")

    def show_about(self):
        html = tr(
            "🌲 <b>Gestion forestière</b> v%1<br>"
            "© 2025-2026 Marc GROSJEAN<br><br>"
            "Plugin développé pour QGIS3/4.xx Qt5/Qt6.<br>"
            "Tous droits réservés.<br>"
            "<a href='mailto:marc.grosjean@wanadoo.fr'>marc.grosjean@wanadoo.fr</a>"
        ).replace("%1", PLUGIN_VERSION)

        QMessageBox.about(
            self.dlg,
            tr("À propos du plugin"),
            html
        )

    def openChangelog(self):
        """
        Ouvre le fichier changelog selon la langue de QGIS.
        - Francophone (fr) → changelog_fr.md
        - Toutes les autres → changelog_en.md
        """
        # Langue de l'interface QGIS
        lang = QSettings().value("locale/userLocale", "fr")[:2].lower()

        # Choix du fichier
        if lang == "fr":
            filename = "changelog_fr.md"
        else:
            filename = "changelog_en.md"

        path = os.path.join(self.plugin_dir, filename)

        # Vérification existence du fichier
        if not os.path.exists(path):
            QMessageBox.warning(self.dlg, "Erreur", f"Fichier changelog introuvable : {path}")
            return

        # Ouvrir le fichier dans l'application par défaut
        QDesktopServices.openUrl(QUrl.fromLocalFile(path))

    # Version plugin cliquable pour ouverture changelog
    def on_version_clicked(self, link):
        if link == "version":
            self.openChangelog()

    # Gestion des cases à cocher de l'export dans l'onglet Analyses
    def init_export_connections(self):
        self.dlg.checkToutExport.toggled.connect(self.on_check_tout_export_toggled)
        self.dlg.checkTravExport.toggled.connect(self.on_check_specific_export_toggled)
        self.dlg.checkTraitExport.toggled.connect(self.on_check_specific_export_toggled)
        self.dlg.checkPrevExport.toggled.connect(self.on_check_specific_export_toggled)
        self.dlg.checkTypeVal.toggled.connect(self.on_check_typeval_toggled)
        self.dlg.checkToutExport.toggled.connect(self.on_check_other_exports_toggled)
        self.dlg.checkTravExport.toggled.connect(self.on_check_other_exports_toggled)
        self.dlg.checkTraitExport.toggled.connect(self.on_check_other_exports_toggled)
        self.dlg.checkPrevExport.toggled.connect(self.on_check_other_exports_toggled)
        # Connexion du bouton export ici
        self.dlg.btnExport.clicked.connect(self.export_to_excel)
        # (optionnel) connexion pour la case ouvrir export
        self.dlg.checkOpenExport.toggled.connect(self.on_check_open_export_toggled)


    def on_current_layer_changed(self, layer):
        # 🔹 Vérification / création du champ 'prixAct' pour cette couche
        show_update_message_and_create_field(layer)

        if isinstance(layer, QgsVectorLayer) and layer.geometryType() == Qgis.GeometryType.Polygon:
            self.infos_polygon_manager.connect_to_layer(layer)


    def on_check_open_export_toggled(self, checked):
        pass

    ### Gestion des cases à cocher des exports Excel

    def on_check_tout_export_toggled(self, checked):
        if self._export_in_progress:
            return

        self.dlg.checkTravExport.setEnabled(not checked)
        self.dlg.checkTraitExport.setEnabled(not checked)
        self.dlg.checkPrevExport.setEnabled(not checked)
        if checked:
            self.dlg.checkTravExport.setChecked(False)
            self.dlg.checkTraitExport.setChecked(False)
            self.dlg.checkPrevExport.setChecked(False)


    def on_check_specific_export_toggled(self, checked):
        if self._export_in_progress:
            return

        if checked:
            self.dlg.checkToutExport.setChecked(False)

    # Si checkTypeVal est cochée
    def on_check_typeval_toggled(self, checked):
        if self._export_in_progress:
            return

        self.dlg.checkToutExport.setEnabled(not checked)
        self.dlg.checkTravExport.setEnabled(not checked)
        self.dlg.checkTraitExport.setEnabled(not checked)
        self.dlg.checkPrevExport.setEnabled(not checked)
        if checked:
            self.dlg.checkToutExport.setChecked(False)
            self.dlg.checkTravExport.setChecked(False)
            self.dlg.checkTraitExport.setChecked(False)
            self.dlg.checkPrevExport.setChecked(False)

    # Autres cases à cocher que checkTypeVal
    def on_check_other_exports_toggled(self, checked):
        if checked:
            self.dlg.checkTypeVal.setEnabled(False)
            self.dlg.checkTypeVal.setChecked(False)
        else:
            # Si aucune autre n’est cochée, réactiver checkTypeVal
            if not (self.dlg.checkToutExport.isChecked() or
                    self.dlg.checkTravExport.isChecked() or
                    self.dlg.checkTraitExport.isChecked() or
                    self.dlg.checkPrevExport.isChecked()):
                self.dlg.checkTypeVal.setEnabled(True)

    #Réinitialisation des cases à cocher de l'export
    def export_to_excel(self):
        self._export_in_progress = True  # Blocage des signaux liés à l'export

        if self.dlg.checkTypeVal.isChecked():
            export_typeval_excel(self)
        else:
            export_standard_excel(self)

        self.reset_export_checkboxes()

        self._export_in_progress = False  # Réactivation normale des signaux

    # Reset des cases à cocher
    def reset_export_checkboxes(self):
        checkboxes = [
            self.dlg.checkTypeVal,
            self.dlg.checkToutExport,
            self.dlg.checkTravExport,
            self.dlg.checkTraitExport,
            self.dlg.checkPrevExport,
            self.dlg.checkOpenExport,
        ]

        for checkbox in checkboxes:
            checkbox.blockSignals(True)
            checkbox.setChecked(False)
            checkbox.setEnabled(True)
            checkbox.blockSignals(False)

    def open_help_gen(self):
        self.open_localized_pdf(
            "Lire_avant.pdf",
            tr("Aide manquante"),
            tr("Le fichier d'aide de démarrage est introuvable.")
        )

    def open_help(self):
        self.open_localized_pdf(
            "help.pdf",
            tr("Aide manquante"),
            tr("Le fichier d'aide général est introuvable.")
        )

    def open_localized_pdf(self, base_filename, title, missing_message):
        """
        Ouvre un fichier PDF localisé en fonction de la langue système.
        Tous les fichiers doivent être nommés sous la forme base_langue.pdf, ex : help_fr.pdf, help_en.pdf
        """
        base_dir = os.path.dirname(__file__)
        docs_dir = os.path.join(base_dir, "docs")

        # Langue de l'interface QGIS, ex: 'fr', 'en'
        lang = QSettings().value("locale/userLocale", "fr")[:2].lower()

        # Nom du fichier avec suffixe langue obligatoire
        name, ext = os.path.splitext(base_filename)
        localized_filename = f"{name}_{lang}{ext}"
        localized_path = os.path.join(docs_dir, localized_filename)

        # Si le PDF localisé n’existe pas, fallback sur français
        if not os.path.exists(localized_path):
            fallback_filename = f"{name}_fr{ext}"
            localized_path = os.path.join(docs_dir, fallback_filename)

        # Ouverture si le fichier existe
        if os.path.exists(localized_path):
            QDesktopServices.openUrl(QUrl.fromLocalFile(localized_path))
        else:
            QMessageBox.warning(
                self.dlg,
                tr(title),
                tr(f"{missing_message}\n({localized_filename})")
            )

    # Méthode pour activer et désactiver les champs

    def toggle_edit_mode(self):
        editable = self.dlg.checkEditProp.isChecked()
        # Champs de l'onglet propriétaire
        self.dlg.nomProp.setReadOnly(not editable)
        self.dlg.lineAdress.setReadOnly(not editable)
        self.dlg.lineMail.setReadOnly(not editable)
        self.dlg.lineTel.setReadOnly(not editable)

        # Active ou désactive le bouton Enregistrer

        self.dlg.btnSaveProp.setVisible(editable)
        self.dlg.btnSaveProp.setEnabled(editable)

    def update_tab_visibility_checkbox(self, proprietaire):
        self.is_proprietaire = not proprietaire  # False si possession est True

        # Récupère l'état de checkBoxConfig
        config_mode = self.dlg.checkBoxConfig.isChecked()

        if not proprietaire:
            # Si ce n'est pas le propriétaire, afficher seulement "Propriétaire"
            for i in range(self.dlg.tabWidget.count()):
                nom_onglet = self.dlg.tabWidget.tabText(i)
                self.dlg.tabWidget.setTabVisible(i, nom_onglet == 'Propriétaire')
            self.dlg.checkBoxSaisie.setVisible(False)
        else:
            # Si c’est le propriétaire, afficher les onglets 1 à 4 par défaut
            onglets_base = ['Informations', 'Travaux', 'Traitements', 'Prévisions', 'Financier']
            for i in range(self.dlg.tabWidget.count()):
                nom_onglet = self.dlg.tabWidget.tabText(i)
                self.dlg.tabWidget.setTabVisible(i, nom_onglet in onglets_base)
            self.dlg.checkBoxSaisie.setVisible(True)
            self.dlg.checkBoxSaisie.setChecked(False)  # Pour forcer état initial

    def save_proprietaire_info(self):
        if not self.dlg.checkEditProp.isChecked():
            QMessageBox.warning(
                self.dlg,
                tr("Édition désactivée"),
                tr("Veuillez cocher 'Modifier les informations' pour enregistrer les changements.")
            )
            return

        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # La couche est invalide ou incomplète

        if not layer:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Pas de couche sélectionnée.")
            )
            return

        if self.current_feature_id is None:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Aucune entité sélectionnée.")
            )
            return

        if not layer.isEditable():
            if not layer.startEditing():
                QMessageBox.critical(
                    self.dlg,
                    tr("Erreur"),
                    tr("Impossible de démarrer l'édition sur la couche.")
                )
                return

        feature_id = self.current_feature_id
        feature = layer.getFeature(feature_id)

        # Champs à modifier
        champ_valeurs = {
            "nom_Voisin": self.dlg.nomProp.text(),
            "adresse_Voisin": self.dlg.lineAdress.toPlainText(),
            "mail_Voisin": self.dlg.lineMail.text(),
            "tel_Voisin": self.dlg.lineTel.text(),
        }

        for nom_champ, valeur in champ_valeurs.items():
            if nom_champ in layer.fields().names():
                feature.setAttribute(nom_champ, valeur)
            else:
                print(f"❌ Champ {nom_champ} introuvable dans la couche")

        # Enregistrement
        if layer.updateFeature(feature):
            if layer.commitChanges():
                QMessageBox.information(
                    self.dlg,
                    tr("Succès"),
                    tr("Les informations ont bien été enregistrées.")
                )
                self.iface.messageBar().pushSuccess(
                    tr("Gestion forestière"),
                    tr("✅ Modifications enregistrées avec succès !")
                )

            else:
                layer.rollBack()
                QMessageBox.critical(
                    self.dlg,
                    tr("Erreur"),
                    tr("Erreur lors de l'enregistrement des modifications.")
                )
        else:
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Impossible de mettre à jour l'entité.")
            )

            # 💡 Gérer l'affichage des onglets en fonction de 'possession' et de la case à cocher saisie

    def toggle_saisie_tabs(self, checked):
        if not self.is_proprietaire:
            return

        index_base = [0, 1, 2, 3, 4]  # Informations, Travaux, Traitements, Prévisions, Financier
        index_saisie = [6, 7, 8, 9, 10]  # Saisie Infos, Saisie Tvx, Saisie Trait., Saisie Prév., Saisie Fiancière

        # Cacher tous les onglets
        for i in range(self.dlg.tabWidget.count()):
            self.dlg.tabWidget.setTabVisible(i, False)

        # Afficher les bons onglets
        onglets_visibles = index_saisie if checked else index_base
        for idx in onglets_visibles:
            self.dlg.tabWidget.setTabVisible(idx, True)

        QApplication.processEvents()

     #Autorise l'édition des cellules de TableWidget qui alimmente les combos
    # def enable_table_editing(self):
    #     table = self.dlg.tableWidgetData
    #     print(f"Nombre de lignes dans tableWidgetData : {table.rowCount()}")
    #     print(f"Nombre de colonnes dans tableWidgetData : {table.columnCount()}")
    #     for row in range(table.rowCount()):
    #         for col in range(table.columnCount()):
    #             item = table.item(row, col)
    #             if not item:
    #                 item = QTableWidgetItem("")
    #                 table.setItem(row, col, item)
    #             item.setFlags(item.flags() | Qt.ItemIsEditable)

    def enable_table_editing(self):
        table = self.dlg.tableWidgetData

        table.blockSignals(True)
        table.setUpdatesEnabled(False)

        rows = table.rowCount()
        cols = table.columnCount()

        # print(f"Activation édition : {rows} lignes, {cols} colonnes")  # désactiver en prod

        for row in range(rows):
            for col in range(cols):
                item = table.item(row, col)
                if item is None:
                    # print(f"Création d'un item vide en [{row}, {col}]")  # désactiver en prod
                    item = QTableWidgetItem("")
                    table.setItem(row, col, item)
                current_flags = item.flags()
                if not (current_flags & ItemIsEditable):
                    new_flags = current_flags | ItemIsEditable
                    item.setFlags(new_flags)
                    # print(f"Item [{row}, {col}] flags modifiés")  # désactiver en prod

        table.setUpdatesEnabled(True)
        table.blockSignals(False)

    def save_table_data(self):
        csv_path = get_csv_path()
        if not csv_path:
            return
        save_table_to_csv(self.dlg.tableWidgetData, csv_path)
        QMessageBox.information(
            self.dlg,
            tr("Données sauvegardées"),
            tr("Le contenu de la table a été sauvegardé avec succès.")
        )

        # Débloquer et rafraîchir les combos
        self.table_update_in_progress = False

        start = time.perf_counter()
        self.refresh_combos_from_table()
        elapsed = time.perf_counter() - start
        print(f"refresh_combos_from_table() pris {elapsed:.3f} secondes")

        # Rafraîchir les combos pour la feature active affichée (valeurs + sélection)
        self.refresh_all_saisie_fields()

    def update_tab_visibility(self):

        config_mode = self.dlg.checkBoxConfig.isChecked()
        saisie_mode = self.dlg.checkBoxSaisie.isChecked()

        # Cacher ou afficher checkBoxConfig selon la possession
        self.dlg.checkBoxConfig.setVisible(self.is_proprietaire)

        # Index des onglets
        index_donnees = 11
        index_analyses = 12
        index_proprietaire = 5

        index_base = [0, 1, 2, 3, 4]  # Informations, Travaux, Traitements, Prévisions, Financier
        index_saisie = [6, 7, 8, 9, 10]  # Saisie Infos, Saisie Tvx, Saisie Trait., Saisie Prév., Saisie Financière
        print(f"Nombre onglets: {self.dlg.tabWidget.count()}")
        # Cacher tous les onglets au départ
        for i in range(self.dlg.tabWidget.count()):
            self.dlg.tabWidget.setTabVisible(i, False)

        # 🟩 Mode configuration → affichage des onglets Données et Analyses
        if config_mode:
            self.dlg.tabWidget.setTabVisible(index_donnees, True)
            self.dlg.tabWidget.setTabVisible(index_analyses, True)
        #    self.enable_table_editing()
            self.dlg.checkBoxSaisie.setVisible(False)
            self.data_analyse()
            return

        # 🟦 Mode normal
        self.dlg.tabWidget.setTabVisible(index_donnees, False)

        if self.is_proprietaire:
            self.dlg.checkBoxSaisie.setVisible(True)
            onglets_a_afficher = index_saisie if saisie_mode else index_base
        else:
            self.dlg.checkBoxSaisie.setVisible(False)
            onglets_a_afficher = [index_proprietaire]

        for idx in onglets_a_afficher:
            self.dlg.tabWidget.setTabVisible(idx, True)

        QApplication.processEvents()

    def handle_checkbox_toggle(self, checked):
        self.toggle_saisie_tabs(checked)

        if not checked:
            QTimer.singleShot(100, self.refresh_all_saisie_fields)

    @staticmethod
    def qdate_from_string(value):
        """
        Convertit une chaîne de caractères (ou objet date/datetime) en QDate.
        """
        if isinstance(value, QDate):
            return value
        if isinstance(value, (datetime.date, datetime.datetime)):
            return QDate(value.year, value.month, value.day)
        if isinstance(value, QVariant.String):
            try:
                year, month, day = map(int, value.split('-'))
                return QDate(year, month, day)
            except (ValueError, TypeError):
                return None
        return None

    def refresh_all_saisie_fields(self):
        """
        Recharge tous les champs (plantations, taux, combos, champs texte, etc.) depuis la couche.
        """
        # Récupère la couche et la feature sélectionnée pour les mettre à jour en visu

        print("refresh_all_saisie_fields lancé")
        QgsMessageLog.logMessage("refresh_all_saisie_fields lancé", "gestion_forestiere", level=Qgis.Info)

        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # La couche est invalide ou incomplète

        if layer and self.current_feature_id is not None:
            feature = layer.getFeature(self.current_feature_id)

            # Mise à jour plantations / taux
            plantations, taux = self.build_liste_arbres(feature)
            self.dlg.plantation.setText(plantations)
            self.dlg.taux.setText(taux)

            #Mise à jour année, prix, date achat et total plants
            self.dlg.annee.setText(str(feature["annee"]) if feature["annee"] else "")
            self.dlg.totalPlants.setText(str(feature["totalplants"]) if feature["totalplants"] else "")
            apply_numeric_to_lineedit(self.dlg.prix, feature["prix"], decimals=0, with_currency=True)
            safe_set_text(self.dlg.dateAchat, feature["dateAchat"])
            apply_numeric_to_lineedit(self.dlg.prix_Act, feature["prixAct"], decimals=0, with_currency=True)

            # Mise à jour dynamique des edit Terrain et Acces
            self.dlg.editTerrain.setText(str(feature["Terrain"]) if feature["Terrain"]else "")
            self.dlg.editAcces.setText(str(feature["Acces"]) if feature["Acces"] else "")
            self.dlg.editRemTerrain.setText(str(feature["RemTerrain"]) if feature["RemTerrain"] else "")

            # Mise à jour du libellé + couleur du type de parcelle
            type_parc_val = feature["typeParc"]
            if type_parc_val:
                libelle = TYPE_PARC_LIBELLES.get(type_parc_val, "Inconnu")
                self.dlg.libelleTypeParc.setText(libelle)

                layer = get_valid_active_layer(self.dlg)
                if not layer:
                    return

                couleur = get_fill_color_from_layer(layer, type_parc_val)
                if couleur:
                    self.dlg.colorFrame.setStyleSheet(f"background-color: {couleur}; border: 1px solid black;")
                else:
                    self.dlg.colorFrame.setStyleSheet("background-color: none; border: 1px solid black;")
            else:
                self.dlg.libelleTypeParc.setText("")
                self.dlg.colorFrame.setStyleSheet("background-color: none; border: 1px solid black;")

            # Mise à jour dynamique des champs des onglets Tvx, Trait et Prev en visu
            if hasattr(self.dlg, "travauxListe"):
                texte = self.build_travaux_dates(feature)
                self.dlg.travauxListe.setPlainText(texte)

            if hasattr(self.dlg, "travauxRq"):
                texte = self.build_travaux_remarques(feature)
                self.dlg.travauxRq.setPlainText(texte)

            if hasattr(self.dlg, "traitListe"):
                texte = self.build_trait_dates(feature)
                self.dlg.traitListe.setPlainText(texte)

            if hasattr(self.dlg, "traitRq"):
                texte = self.build_trait_remarques(feature)
                self.dlg.traitRq.setPlainText(texte)

            if hasattr(self.dlg, "prevListe"):
                texte = self.build_prev_dates(feature)
                self.dlg.prevListe.setPlainText(texte)

            if hasattr(self.dlg, "prevRq"):
                texte = self.build_prev_remarques(feature)
                self.dlg.prevRq.setPlainText(texte)

            # Mise à jour des combos
            for prefix, combo_base in [("plant", "comboPlant"), ("Tvx", "comboTvx"),
                                       ("Trait", "comboTrait"), ("Prev", "comboPrev")]:
                self.init_saisie_combos(prefix, combo_base, feature)

    def handle_possession_toggle(self, state):
        self.is_proprietaire = bool(state)
        self.enregistrer_possession(state)
        self.update_tab_visibility()

    def enregistrer_possession(self, state):
        """Met à jour le champ Possession selon la case à cocher"""

        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # Couche invalide ou non compatible

        if not layer.isEditable():
            layer.startEditing()

        feature = layer.getFeature(self.current_feature_id)
        if feature:
            feature['Possession'] = state  # True ou False
            layer.updateFeature(feature)

    # Raffraichissement des combos par rapport à tablewidgetdata
    def refresh_combos_from_table(self):
        if self.table_update_in_progress:
            return

        self.populate_saisie_combos("Tvx", "comboTvx", 1)
        self.populate_saisie_combos("Trait", "comboTrait", 2)
        self.populate_saisie_combos("Prev", "comboPrev", 1)
        self.populate_saisie_combos("Plant", "comboPlant", 0)
    #    self.populate_terrain_acces_combos()

    def populate_saisie_combos(self, prefix, combo_base_name, column_index):
        """
        Remplit les comboBox d’un onglet Saisie (Tvx, Trait, Prev).

        Args:
            prefix (str): préfixe du champ attributaire, ex: 'Tvx', 'Trait', 'Prev'
            combo_base_name (str): nom de base des combo ex: 'comboTvx', 'comtrait', 'comboPrev'
            column_index (int): index de colonne dans tableWidgetData (0-based)
        """
        # Récupérer les valeurs uniques de la colonne

        raw_data = extract_table_values(self.dlg.tableWidgetData, [column_index])
        valeurs = unique_values_per_column(raw_data)[0]

        nb_combos = SAISIE_COMBO_COUNTS.get(prefix, 6)  # défaut = 6 si inconnu

        for i in range(1, nb_combos + 1):
            combo = getattr(self.dlg, f"{combo_base_name}{i}", None)
            if combo is not None:
                combo.blockSignals(True)
                combo.clear()
                combo.addItem("")
                combo.addItems(valeurs)
                combo.blockSignals(False)

    def remplir_combobox_terrain_acces(self, valeur_terrain_actuelle='', valeur_acces_actuelle=''):
        """Remplit les comboModifTerrain et comboModifAcces avec les valeurs uniques de tableWidgetData.
        Sélectionne la valeur correspondante si elle existe.
        """

        table = self.dlg.tableWidgetData

        # Utiliser des ensembles pour éviter les doublons
        valeurs_terrain = set()
        valeurs_acces = set()


        for row in range(table.rowCount()):
            item_terrain = table.item(row, 4)  # Colonne Terrain
            if item_terrain:
                valeur = item_terrain.text().strip()
                if valeur:
                    valeurs_terrain.add(valeur)

            item_acces = table.item(row, 3)  # Colonne Acces
            if item_acces:
                valeur = item_acces.text().strip()
                if valeur:
                    valeurs_acces.add(valeur)


        # Remplit les combobox avec données tableWidgetData
        self.dlg.comboModifTerrain.clear()
        self.dlg.comboModifTerrain.addItem("")
        self.dlg.comboModifTerrain.addItems(sorted(valeurs_terrain))

        self.dlg.comboModifAcces.clear()
        self.dlg.comboModifAcces.addItem("")
        self.dlg.comboModifAcces.addItems(sorted(valeurs_acces))

        # Sélectionne les valeurs existantes si fournies
        if valeur_terrain_actuelle:
            index_terrain = self.dlg.comboModifTerrain.findText(valeur_terrain_actuelle)
            if index_terrain != -1:
                self.dlg.comboModifTerrain.setCurrentIndex(index_terrain)

        if valeur_acces_actuelle:
            index_acces = self.dlg.comboModifAcces.findText(valeur_acces_actuelle)
            if index_acces != -1:
                self.dlg.comboModifAcces.setCurrentIndex(index_acces)


    def init_combo_modif_legend(self, feature):
        # Récupère l'entier depuis la table attributaire
        type_parc_val = feature['typeParc']

        # Convertit en libellé via le dictionnaire
        libelle = TYPE_PARC_LIBELLES.get(type_parc_val, "")


        # Recherche le libellé dans la combo
        index = self.dlg.comboModifLegend.findText(libelle, MatchFixedString)

        if index >= 0:
            self.dlg.comboModifLegend.setCurrentIndex(index)

        else:
            self.dlg.comboModifLegend.setCurrentIndex(0)


    def init_saisie_combos(self, prefix, combo_base_name, feature):
        """
        Initialise les combos avec les valeurs de l'entité (feature).

        Args:
            prefix (str): préfixe des champs ('Tvx', 'Trait', 'Previ')
            combo_base_name (str): nom de base des combo ex: 'comboTvx', 'comtrait', 'comboPrev'
        """
        nb_combos = SAISIE_COMBO_COUNTS.get(prefix, 6)  # 🔥 Ici on récupère combien de combos utiliser

        for i in range(1, nb_combos + 1):
            field_name = f"{prefix}{i}"
            combo_name = f"{combo_base_name}{i}"
            combo = getattr(self.dlg, combo_name, None)
            if combo is not None:
                combo.blockSignals(True)  # 🛑 Bloque les signaux pendant l'initialisation

                # Valeur du champ si elle existe
                val = ""
                if field_name in feature.fields().names():
                    val = feature[field_name] or ""

                index = combo.findText(val)
                combo.setCurrentIndex(index if index >= 0 else 0)

                combo.blockSignals(False)  # ✅ Réactive après mise à jour

    def on_saisie_combo_changed(self, prefix, combo_base_name, i):
        """
        Gère la mise à jour du champ {prefix}{i} lorsque le combo change.
        Commit, relance l'édition, et rafraîchit l'affichage des combos.
        """
        combo = getattr(self.dlg, f"{combo_base_name}{i}")
        new_val = combo.currentText()

        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # La couche est invalide ou incomplète

        fid = self.current_feature_id

        if layer is None or fid is None:
            return

        # 1. Démarrer l'édition si besoin
        if not layer.isEditable():
            if not layer.startEditing():
                QMessageBox.critical(
                    self.dlg,
                    tr("Erreur"),
                    tr("Impossible de démarrer l'édition sur la couche.")
                )
                return

        # 2. Modifier l'attribut
        field_idx = layer.fields().indexOf(f"{prefix}{i}")
        if field_idx < 0:
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Le champ {}{} est introuvable.").format(prefix, i)
            )
            return

        if not layer.changeAttributeValue(fid, field_idx, new_val):
            layer.rollBack()
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Échec de la modification de l'attribut.")
            )
            return

        # 3. Commit et relancer l'édition
        if not layer.commitChanges():
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Échec de l'enregistrement des modifications combo changed.")
            )
            layer.rollBack()
            return
        else:
            layer.startEditing()
            # self.iface.messageBar().pushMessage(
            #     tr("Succès"),
            #     tr("{}{} mis à jour → {}").format(prefix, i, new_val),
            #     level=0
            # )

        # 4. Rafraîchir l'affichage des combos - A vérifier, semble inutile
        feature = layer.getFeature(fid)
        self.init_saisie_combos(prefix, combo_base_name, feature)


    def enregistrer_modifs_saisie(self, prefix, combo_prefix, date_prefix=None, rem_prefix=None):
        """
        Enregistre les modifications pour un onglet de saisie donné (Tvx, Trait, Prev, Infos...).

        :param prefix: Préfixe des champs dans la couche (ex: 'Tvx', 'Trait', 'Prev', 'plant')
        :param combo_prefix: Préfixe des combobox dans l’UI (ex: 'comboTvx', 'comboTrait')
        :param date_prefix: (Optionnel) Préfixe des widgets de date dans l’UI (ex: 'dateTvx')
        :param rem_prefix: (Optionnel) Préfixe des widgets de remarque dans l’UI (ex: 'remTvx')
        """
        # 🔍 Récupération de la couche
        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # La couche est invalide ou incomplète

        if not layer:
            show_error_bar(
                self.iface,
                tr("Erreur"),
                tr("Aucune couche sélectionnée.")
            )
            return

        # 🔒 Vérifie l'entité sélectionnée
        if self.current_feature_id is None:
            log_warning(tr("Aucune entité sélectionnée."))
            return

        # 🔧 Passe la couche en mode édition si nécessaire
        if not layer.isEditable() and not layer.startEditing():
            show_error_bar(
                self.iface,
                tr(")Erreur"),
                tr("Impossible de passer la couche en mode édition.")
            )
            return

        # 🔄 Récupération de l'entité courante
        feature = layer.getFeature(self.current_feature_id)

        # ✅ Sauvegarde des valeurs des combobox (obligatoire)
        self.save_saisie_values(layer, feature, prefix, combo_prefix)

        # 🗓️💬 Si des champs date/remarque sont définis, on les enregistre aussi
        if date_prefix and rem_prefix:
            self.save_saisie_fields(prefix, date_prefix, rem_prefix)

        # 💾 Validation des modifications
        if layer.commitChanges():
            current_tab_name = self.dlg.tabWidget.tabText(self.dlg.tabWidget.currentIndex())
            show_success_bar(
                self.iface,
                tr("✅ Gestion_forestière"),
                tr("Modifications enregistrées pour : {}").format(current_tab_name)
            )
            layer.startEditing()  # Repassage en mode édition pour d'autres modifs
        else:
            layer.rollBack()
            show_error_bar(
                self.iface,
                tr("❌ Erreur"),
                tr("Échec de l'enregistrement des modifications Saisie.")
            )

    def save_saisie_values(self, layer, feature, prefix, combo_base):
        """
        Sauvegarde les valeurs des combobox de saisie dans les champs correspondants.
        """
        updates = {}

        nb_combos = SAISIE_COMBO_COUNTS.get(prefix, 6)  # 🔥 Ajouté ici aussi !

        for i in range(1, nb_combos + 1):
            combo_name = f"{combo_base}{i}"
            combo = getattr(self.dlg, combo_name, None)
            field_name = f"{prefix}{i}"

            if combo is None or not field_name:
                continue

            if field_name not in feature.fields().names():
                log_warning(f"Champ {field_name} absent de la couche.")
                continue

            new_value = combo.currentText()
            if feature[field_name] != new_value:
                updates[layer.fields().indexFromName(field_name)] = new_value

        if updates:
            res = layer.changeAttributeValues(feature.id(), updates)
            if res:
                log_debug(tr("✅ Valeurs mises à jour pour {}: {}").format(prefix, updates))
            else:
                log_error(
                    tr("❌ Échec lors de l'application des modifications.")
                )
        else:
            log_debug(
                tr("ℹ️ Aucune modification détectée.")
            )

    def connect_enregistrement_boutons(self):
        """
        Connecte automatiquement tous les boutons d'enregistrement liés aux combos de saisie.
        """
        saisie_combo_buttons = {
            "btnEnregTvx": ("Tvx", "comboTvx", "dateTvx", "remTvx"),
            "btnEnregTrait": ("Trait", "comboTrait", "dateTrait", "remTrait"),
            "btnEnregPrev": ("Prev", "comboPrev", "datePrev", "remPrev"),
            "btnEnregInfos": (
                "plant",
                "comboPlant",
            ),  # 🆕 Saisie Informations
        }

        # Connexion des boutons standards
        for bouton_name, params in saisie_combo_buttons.items():
            bouton = getattr(self.dlg, bouton_name, None)
            if bouton:
                bouton.clicked.connect(lambda _, p=params: self.enregistrer_modifs_saisie(*p))

        # Connexion spécifique pour Saisie Infos
        bouton_infos = getattr(self.dlg, "btnEnregInfos", None)
        if bouton_infos:
            bouton_infos.clicked.connect(self.save_infos_saisie)  # 👈 Appel direct à la méthode spécifique

    def connect_saisie_combos(self):
        erreurs = []  # Liste pour stocker les erreurs

        for prefix, base_name in [("plant", "comboPlant"),("Tvx", "comboTvx"), ("Trait", "comboTrait"), ("Prev", "comboPrev")]:
            nb_combos = SAISIE_COMBO_COUNTS.get(prefix, 6)
            for i in range(1, nb_combos + 1):
                combo = getattr(self.dlg, f"{base_name}{i}", None)
                if combo is not None:
                    combo.currentIndexChanged.connect(
                        lambda _, i=i, p=prefix, b=base_name: self.on_saisie_combo_changed(p, b, i)
                    )
                else:
                    erreurs.append(f"{base_name}{i}")

        if erreurs:
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Les combobox suivantes sont introuvables : {}").format(", ".join(erreurs))
            )



    def update_saisie_fields(self, feature, prefix, combo_prefix):
        """
        Met à jour dynamiquement les combobox de saisie (Tvx, Traitement, Prévision)
        en fonction des données du feature sélectionné.

        :param feature: l'entité sélectionnée
        :param prefix: préfixe du champ (ex: 'Tvx', 'Trait', 'Prev')
        :param combo_prefix: préfixe des objets combobox (ex: 'comboTvx', 'comboTrait', 'comboPrev')
        """
        nb_combos = SAISIE_COMBO_COUNTS.get(prefix, 6)
        for i in range(1, nb_combos + 1):
            field_name = f"{prefix}{i}"
            combo_name = f"{combo_prefix}{i}"

            value = feature[field_name] if field_name in feature.fields().names() else None
            combo = getattr(self.dlg, combo_name, None)

            if combo:
                if value:
                    combo.setCurrentText(str(value))
                else:
                    combo.setCurrentIndex(-1)  # Aucun élément sélectionné


    def on_feature_selected(self, feature):
        for name in feature.fields().names():
            # Onglet 6
            self.update_saisie_combo_fields(feature, "Tvx", "comboTvx")
            self.update_saisie_extra_fields(feature, "Tvx", "dateTvx", "remTvx", date_type="QDateEdit")
            # Onglet 7
            self.update_saisie_combo_fields(feature, "Trait", "comboTrait")
            self.update_saisie_extra_fields(feature, "Trait", "dateTrait", "remTrait", date_type="QDateEdit")
            #Onglet 8
            self.update_saisie_combo_fields(feature, "Prev", "comboPrev")
            self.update_saisie_extra_fields(feature, "Prev", "datePrev", "remPrev", date_type="QLineEdit")


    def on_feature_selection_changed(self):
        layer = self.iface.activeLayer()
        selected = layer.selectedFeatures()
        if selected:
            feature = selected[0]
            self.on_feature_selected(feature)

    def update_saisie_extra_fields(self, feature, prefix, date_prefix, rem_prefix, date_type="QDateEdit",
                                   rem_type="QTextEdit"):
        """
        Met à jour les champs de date et de remarque associés aux combobox,
        en fonction du prefix (Tvx, Trait, Prev), en utilisant SAISIE_COMBO_COUNTS.
        """
        nb_champs = SAISIE_COMBO_COUNTS.get(prefix, 6)

        for i in range(1, nb_champs + 1):
            # --- DATE ---
            date_field_name = f"date{prefix}{i}"
            date_widget = getattr(self.dlg, f"{date_prefix}{i}", None)
            if date_widget and date_field_name in feature.fields().names():
                value = feature[date_field_name]
                if value:
                    if date_type == "QDateEdit":
                        date = QDate.fromString(value, "yyyy-MM-dd")
                        date_widget.setDate(date)
                    else:
                        date_widget.setText(str(value))
                else:
                    date_widget.clear()

            # --- REMARQUE ---
            rem_field_name = f"rem{prefix}{i}"
            rem_widget = getattr(self.dlg, f"{rem_prefix}{i}", None)
            if rem_widget and rem_field_name in feature.fields().names():
                value = feature[rem_field_name]
                if value:
                    rem_widget.setText(str(value))
                else:
                    rem_widget.clear()

    def update_saisie_combo_fields(self, feature, prefix, combo_base):
        """
        Met à jour les combobox pour un groupe donné (ex : Tvx, Trait, Prev) directement depuis l'entité.
        """
        nb_combos = SAISIE_COMBO_COUNTS.get(prefix, 6)
        for i in range(1, nb_combos + 1):
            field_name = f"{prefix}{i}"
            combo_name = f"{combo_base}{i}"
            combo = getattr(self.dlg, combo_name, None)
            if combo:
                value = feature[field_name]


                # 👉 Remettre les options standards (depuis ta table ou liste prédéfinie)
                self.populate_saisie_combo(combo, prefix)

                if value:
                    # Cherche la valeur dans les options
                    index = combo.findText(value)
                    if index != -1:
                        combo.setCurrentIndex(index)
                    else:
                        # Pas trouvée ? Ajoute-la et sélectionne
                        combo.addItem(value)
                        combo.setCurrentIndex(combo.count() - 1)
                else:
                    combo.setCurrentIndex(0)

    def populate_saisie_combo(self, combo, prefix):
        """
        Remplit une comboBox spécifique avec les valeurs possibles pour le prefix donné.
        """
        # Mapping des préfixes à leur colonne
        column_mapping = {
            "Tvx": 1,
            "Trait": 2,
            "Prev": 2,  # Prev utilise la même colonne que Trait
        }

        column_index = column_mapping.get(prefix)

        if column_index is None:
            log_warning(f"Préfixe inconnu pour populate_saisie_combo : {prefix}")
            return

        raw_data = extract_table_values(self.dlg.tableWidgetData, [column_index])
        valeurs = unique_values_per_column(raw_data)[0]

        combo.clear()
        combo.addItem("")  # Valeur vide en premier
        combo.addItems(valeurs)


    @staticmethod
    def build_liste_arbres(feature):
        """
        Récupère les champs plant1..plant4 et Tx1..Tx4 de feature
        et renvoie deux chaînes à afficher dans plantation et taux.
        """
        fields_plants = ['plant1', 'plant2', 'plant3', 'plant4']
        fields_tx = ['Tx1', 'Tx2', 'Tx3', 'Tx4']

        valeurs_plants = [str(feature[f]) for f in fields_plants if feature[f]]
        valeurs_tx = [str(feature[f]) for f in fields_tx if feature[f]]

        to_str_plants = "\n".join(valeurs_plants) if valeurs_plants else tr("Aucune essence définie ici…")
        to_str_tx = "\n".join(valeurs_tx) if valeurs_tx else tr("Aucun")

        return to_str_plants, to_str_tx

    @staticmethod
    def build_travaux_dates(feature):
        mon_tab = []

        for i in range(1, SAISIE_COMBO_COUNTS["Tvx"] + 1):  # création liste des dates et remarques
            dat = f"dateTvx{i}"
            rq = f"Tvx{i}"
            mon_tab.append([dat, rq])

        fields = sum(mon_tab, [])
        str2 = '\n'.join(
            f'{format_date(feature[a])} : {feature[b]}'
            for a, b in batched(fields, 2) if feature[b]
        )
        if not str2:
            return 'Aucun travaux effectués ici...'
        return str2

    @staticmethod
    def build_travaux_remarques(feature):

        mon_tab = []

        for i in range(1, SAISIE_COMBO_COUNTS["Tvx"] + 1):           #création liste des dates et remarques du display_point
            dat = "dateTvx" + str(i)
            rq = "remTvx" + str(i)
            mon_tab.append([dat, rq])

        fields = sum(mon_tab,[])
        str3 = '\n'.join(
            f'{format_date(feature[a])} : {feature[b]}'
            for a, b in batched(fields, 2) if feature[b]
        )
        return str3 if str3 else "..."

    @staticmethod
    def build_trait_dates(feature):

        mon_tab = []

        for i in range(1, SAISIE_COMBO_COUNTS["Trait"] + 1):  # création liste des dates et remarques du display_point
            dat = "dateTrait" + str(i)
            rq = "Trait" + str(i)
            mon_tab.append([dat, rq])

        fields = sum(mon_tab, [])
        str4 =  '\n'.join(
            f'{format_date(feature[a])} : {feature[b]}'
            for a, b in batched(fields, 2) if feature[b]
        )  # contrôle présence champ remarque si date existe et concatenation champ date et travaux
        if not str4:
            return 'Aucun traitement effectués ici...'
        return str4

    @staticmethod
    def build_trait_remarques(feature):

        mon_tab = []

        for i in range(1, SAISIE_COMBO_COUNTS["Trait"] + 1):  # création liste des dates et remarques du display_point
            dat = "dateTrait" + str(i)
            rq = "remTrait" + str(i)
            mon_tab.append([dat, rq])

        fields = sum(mon_tab, [])
        str5 = '\n'.join(
            f'{format_date(feature[a])} : {feature[b]}'
            for a, b in batched(fields, 2) if feature[b]
        )  # contrôle présence champ remarque si date existe
        return str5 if str5 else "..."  # si aucune remarque des traitements dans la parcelle

    @staticmethod
    def build_prev_dates(feature):

        mon_tab = []

        for i in range(1, SAISIE_COMBO_COUNTS["Prev"] + 1):
            dat = "datePrev" + str(i)
            rq = "Prev" + str(i)
            mon_tab.append([dat, rq])

        fields = sum(mon_tab, [])
        str6 =  '\n'.join(
            f'{feature[a]} : {feature[b]}'
            for a, b in batched(fields, 2) if feature[b]
        )  # contrôle présence champ remarque si date existe et concatenation champ date et prévisions
        if not str6:
            return 'Aucun travaux prévus ici...'
        return str6

    # Affichage de la liste des remarques et controle des champs nuls

    @staticmethod
    def build_prev_remarques(feature):

        mon_tab = []

        for i in range(1, SAISIE_COMBO_COUNTS["Prev"] + 1):  # création liste des dates et remarques du display_point
            dat = "datePrev" + str(i)
            rq = "remPrev" + str(i)
            mon_tab.append([dat, rq])

        fields = sum(mon_tab, [])
        str7 = '\n'.join(
            f'{feature[a]} : {feature[b]}'
            for a, b in batched(fields, 2) if feature[b]
        )  # contrôle présence champ remarque si date existe
        return str7 if str7 else "..."  # si aucune remarque sur les prévisions dans la parcelle

    #Gestion du combobox ModifLegend pour le remplir avec la liste
    def remplir_combo_modif_legend(self):
        self.dlg.comboModifLegend.clear()

        # On remplit la combo avec les libellés en gardant les clés associées
        for key in sorted(TYPE_PARC_LIBELLES.keys()):
            label = TYPE_PARC_LIBELLES[key]
            self.dlg.comboModifLegend.addItem(label, key)

    def save_type_parc_value(self, layer, feature):
        """
        Sauvegarde la valeur de typeParc (entier) en fonction du libellé sélectionné dans comboModifLegend.
        """
        libelle_selectionne = self.dlg.comboModifLegend.currentText()

        # Recherche l'entier correspondant dans le dictionnaire inverse
        inverse_dict = {v: k for k, v in TYPE_PARC_LIBELLES.items()}
        type_parc_val = inverse_dict.get(libelle_selectionne, None)

        if type_parc_val is None:
            log_warning(f"⚠️ Libellé '{libelle_selectionne}' non trouvé dans le dictionnaire.")
            return

        if 'typeParc' not in feature.fields().names():
            log_error(
                tr("❌ Champ 'typeParc' non trouvé dans la couche.")
            )
            return

        if feature['typeParc'] != type_parc_val:
            index = layer.fields().indexFromName('typeParc')
            res = layer.changeAttributeValues(feature.id(), {index: type_parc_val})
            if res:
                log_debug(f"✅ Champ 'typeParc' mis à jour avec la valeur {type_parc_val}")
            else:
                log_error(
                    tr("❌ Échec de mise à jour pour 'typeParc'")
                )
        else:
            log_debug(
                tr("ℹ️ Aucun changement détecté pour 'typeParc'")
            )

    def remplir_champs_modifiables(self, feature):
        """Remplit tous les champs modifiables à partir d'une entité, de façon générique."""
        self.dlg.anneeModif.setText(safe_str(feature['annee']))
        self.dlg.totalPlantsModif.setText(safe_str(feature['totalplants']))
        self.dlg.remModifTerrain.setPlainText(safe_str(feature['RemTerrain']))
    #    self.dlg.prixModif.setText(safe_str(feature['prix']))
        setup_doublespinbox(self.dlg.prixModif, value=feature['prix'], decimals=0, with_currency=True)
        #self.dlg.prixActModif.setText(safe_str(feature['prixAct']))
        setup_doublespinbox(self.dlg.prixActModif, value=feature['prixAct'], decimals=0, with_currency=True)
        safe_set_date(self.dlg.dateEditAchat, feature['dateAchat'])



        # Dictionnaire des préfixes : nom champ QGIS -> préfixe widget Qt
        champs = {
            'Tx': 'txModif',
            'remTvx': 'remTvx',
            'remTrait': 'remTrait',
            'remPrev': 'remPrev',
            'datePrev': 'datePrev'
        }

        for prefix, widget_prefix in champs.items():
            count = SAISIE_COMBO_COUNTS.get(prefix.replace("rem", ""), 6)  # remTvx -> Tvx
            for i in range(1, count + 1):
                champ_nom = f"{prefix}{i}"
                widget_nom = f"{widget_prefix}{i}"
                widget = getattr(self.dlg, widget_nom, None)
                if widget and champ_nom in feature.fields().names():
                    widget.setText(safe_str(feature[champ_nom]))
            # Gestion des dates (dateTvx, dateTrait, datePrev)
            date_champs = {
                'dateTvx': 'dateTvx',
                'dateTrait': 'dateTrait',
            }

            for prefix, widget_prefix in date_champs.items():
                count = SAISIE_COMBO_COUNTS.get(prefix.replace("date", ""), 6)  # dateTvx -> Tvx
                for i in range(1, count + 1):
                    champ_nom = f"{prefix}{i}"
                    widget_nom = f"{widget_prefix}{i}"
                    widget = getattr(self.dlg, widget_nom, None)
                    if widget and champ_nom in feature.fields().names():
                        value = feature[champ_nom]
                        if value:
                            if isinstance(value, str):
                                # Pour les chaînes de caractères, directement les mettre
                                widget.setText(safe_str(value))
                            elif isinstance(value, QDate):
                                # Pour les dates, mettre sous format QDate
                                widget.setDate(value)

    def save_saisie_fields(self, prefix, date_prefix, rem_prefix):
        """
        Sauvegarde les champs de l'onglet générique (Tvx, Trait, Prev).
        :param prefix: Le préfixe des champs de l'entité (ex : 'Tvx', 'Trait', 'Prev')
        :param date_prefix: Le préfixe des widgets de date (ex : 'dateTvx', 'dateTrait', 'datePrev')
        :param rem_prefix: Le préfixe des widgets de remarque (ex : 'remTvx', 'remTrait', 'remPrev')
        """

        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # La couche est invalide ou incomplète

        if not layer:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Aucune couche sélectionnée.")
            )
            return

        if self.current_feature_id is None:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Aucune entité sélectionnée.")
            )
            return

        if not layer.isEditable():
            if not layer.startEditing():
                QMessageBox.critical(
                    self.dlg,
                    tr("Erreur"),
                    tr("Impossible de démarrer l'édition sur la couche.")
                )
                return

        feature = layer.getFeature(self.current_feature_id)
        nb_champs = SAISIE_COMBO_COUNTS.get(prefix, 6)

        for i in range(1, nb_champs + 1):
            date_field = f"date{prefix}{i}"
            rem_field = f"rem{prefix}{i}"

            # --- DATE ---
            date_widget = getattr(self.dlg, f"{date_prefix}{i}", None)
            if date_widget and date_field in feature.fields().names():
                if hasattr(date_widget, 'date'):  # QDateEdit
                    value = date_widget.date().toString("yyyy-MM-dd")
                else:  # QLineEdit
                    value = date_widget.text()
                feature.setAttribute(date_field, value)
            else:
                print(f"⚠️ Champ ou widget date manquant : {date_field}")

            # --- REMARQUE ---
            rem_widget = getattr(self.dlg, f"{rem_prefix}{i}", None)
            if rem_widget and rem_field in feature.fields().names():
                if hasattr(rem_widget, 'toPlainText'):  # QTextEdit
                    value = rem_widget.toPlainText()
                else:  # QLineEdit
                    value = rem_widget.text()
                feature.setAttribute(rem_field, value)
            else:
                print(f"⚠️ Champ ou widget remarque manquant : {rem_field}")

        # Mise à jour de l'entité dans la couche (pas de commit ici)
        if not layer.updateFeature(feature):
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Échec de la mise à jour de l'entité.")
            )
            return


    def connect_save_buttons(self):
        """Connecte les boutons de sauvegarde aux méthodes de sauvegarde pour chaque onglet."""
        self.dlg.btnEnregTvx.clicked.connect(lambda: self.save_saisie_fields('Tvx', 'dateTx', 'remTx'))
        self.dlg.btnEnregTrait.clicked.connect(lambda: self.save_saisie_fields('Trait', 'dateTrait', 'remTrait'))
        self.dlg.btnEnregPrev.clicked.connect(lambda: self.save_saisie_fields('Prev', 'datePrev', 'remPrev'))

        # Connexion pour le bouton de l'onglet Saisie Infos, en ajoutant Tx1 à 4, annee, totalplants

    def save_infos_saisie(self):
        """Sauvegarde les informations modifiées dans l'onglet Saisie Infos."""

        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return  # La couche est invalide ou incomplète

        if not layer:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Aucune couche sélectionnée.")
            )
            return

        if self.current_feature_id is None:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Aucune entité sélectionnée.")
            )
            return

        if not layer.isEditable():
            if not layer.startEditing():
                QMessageBox.critical(
                    self.dlg,
                    tr("Erreur"),
                    tr("Impossible de démarrer l'édition sur la couche.")
                )
                return

        feature_id = self.current_feature_id
        feature = layer.getFeature(feature_id)

        # ✅ Vérifie que la somme des taux ne dépasse pas 100
        tx_fields = [self.dlg.txModif1, self.dlg.txModif2, self.dlg.txModif3, self.dlg.txModif4]
        tx_values = []

        for tx in tx_fields:
            val = tx.text().strip()
            if val and val.upper() != "NULL":
                try:
                    tx_values.append(int(val))
                except ValueError:
                    QMessageBox.warning(
                        self.dlg,
                        tr("Erreur"),
                        tr("Valeur invalide dans un des taux : '{}' (entier attendu)").format(val)
                    )
                    return

        total_tx = sum(tx_values)

        if total_tx > 100:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("La somme des taux ne peut pas dépasser 100 % (actuellement : {} %)").format(total_tx)
            )
            return

        # Champs à modifier dans l'onglet Saisie Infos
        champ_valeurs = {
            "annee": self.dlg.anneeModif.text(),
            "totalplants": self.dlg.totalPlantsModif.text(),
            "Tx1": self.dlg.txModif1.text(),
            "Tx2": self.dlg.txModif2.text(),
            "Tx3": self.dlg.txModif3.text(),
            "Tx4": self.dlg.txModif4.text(),
            "Terrain": self.dlg.comboModifTerrain.currentText(),
            "Acces": self.dlg.comboModifAcces.currentText(),
            "RemTerrain": self.dlg.remModifTerrain.toPlainText(),
            # Ajouter ici d'autres champs si nécessaire
            # "autre_champ": self.dlg.nomQtWidget.text(),
        }

        for nom_champ, valeur in champ_valeurs.items():
            if nom_champ in layer.fields().names():
                champ_type = layer.fields().field(nom_champ).type()
                # Vérification propre des "valeurs vides"
                if valeur.strip().upper() in ("", "NULL", "NONE"):
                    feature.setAttribute(nom_champ, None)
                elif champ_type in (int,):
                    feature.setAttribute(nom_champ, int(valeur))
                elif champ_type == float:
                    feature.setAttribute(nom_champ, float(valeur))
                else:
                    feature.setAttribute(nom_champ, valeur)
            else:
                print(f"❌ Champ {nom_champ} introuvable dans la couche")

        # ➕ Sauvegarde du champ typeParc (depuis comboModifLegend)
        libelle = self.dlg.comboModifLegend.currentText()
        inverse_dict = {v: k for k, v in TYPE_PARC_LIBELLES.items()}
        type_parc_val = inverse_dict.get(libelle, None)

        if type_parc_val is None:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Le libellé '{}' n'est pas reconnu.").format(libelle)
            )
            return

        if 'typeParc' not in layer.fields().names():
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Le champ 'typeParc' est introuvable dans la couche.")
            )
            return

        feature.setAttribute('typeParc', type_parc_val)


        # Appliquer les modifications
        if not layer.updateFeature(feature):
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Échec de la mise à jour de l'entité.")
            )
            return

        if not layer.commitChanges():
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Échec lors de la sauvegarde des modifications.")
            )
            return
        # 🔥 Repasser en mode édition
        if not layer.startEditing():
            QMessageBox.critical(
                self.dlg,
                tr("Erreur"),
                tr("Impossible de repasser en mode édition après sauvegarde.")
            )

        QMessageBox.information(
            self.dlg,
            tr("Succès"),
            tr("Les informations ont été enregistrées avec succès.")
        )

    def afficher_labels_dynamiques_sur_couche(self):
        """Affiche dynamiquement les étiquettes selon 'Possession' et 'typeParc'."""
        layer = self.iface.activeLayer()
        if not isinstance(layer, QgsVectorLayer):
            print("Pas de couche vecteur active.")
            return

        root_rule = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings())

        label_expression = (
            '"section" || "numero" || '
            'CASE WHEN "indice_parc" IS NOT NULL AND "indice_parc" != \'\' '
            'THEN \'\' || "indice_parc" ELSE \'\' END || '
            '\'\\n\' || "SURFACE" || \' ares\''
        )

        # Règle 1 : Possession = TRUE (et on applique une couleur selon typeParc)
        rule1 = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings())
        rule1.setFilterExpression('"Possession" = TRUE AND "typePArc" NOT IN (6,7)')

        settings1 = QgsPalLayerSettings()
        settings1.isExpression = True
        settings1.fieldName = label_expression

        format1 = QgsTextFormat()
        format1.setFont(QFont("Arial", 9))
        format1.setSize(10)
        format1.setColor(QColor("black"))  # Valeur par défaut, on peut en ajouter d'autres

        settings1.setFormat(format1)
        rule1.setSettings(settings1)
        rule1.setDescription("Parcelles possession = True")
        root_rule.appendChild(rule1)

        # Règle 2 : Possession = FALSE
        rule2 = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings())
        rule2.setFilterExpression('"Possession" = FALSE OR "Possession" IS NULL')

        settings2 = QgsPalLayerSettings()
        settings2.isExpression = True
        settings2.fieldName = '"section" || "numero"'

        format2 = QgsTextFormat()
        format2.setFont(QFont("Arial", 9))
        format2.setSize(9)
        format2.setColor(QColor("black"))  # Par exemple rouge

     #   format2.setBuffer(buffer_settings)
        settings2.setFormat(format2)
        rule2.setSettings(settings2)
        rule2.setDescription("Liste générale parrcelles")
        root_rule.appendChild(rule2)


    #     # Règle 3 : toutes les parcelles dont le propriétaire est connus sont en violet
        rule3 = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings())
        rule3.setFilterExpression('("Possession" = FALSE OR "Possession" IS NULL) AND "nom_Voisin" IS NOT NULL')

        settings3 = QgsPalLayerSettings()
        settings3.isExpression = True
        settings3.fieldName = '"section" || "numero"'

        format3 = QgsTextFormat()
        format3.setFont(QFont("Arial", 9))
        format3.setSize(9)
        format3.setColor(QColor("#7f00ff"))  # Valeur par défaut, on peut en ajouter d'autres

        settings3.setFormat(format3)
        rule3.setSettings(settings3)
        rule3.setDescription("Parcelles propriétaires connus")
        root_rule.appendChild(rule3)

        # # Règle 4 : Possession = TRUE (et on applique une couleur selon typeParc)
        rule4 = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings())
        rule4.setFilterExpression('"Possession" = TRUE AND "typePArc" IN(6,7)')

        settings4 = QgsPalLayerSettings()
        settings4.isExpression = True
        settings4.fieldName = '"section" || "numero" || \'\\n\' || "SURFACE" || \' ares\''

        # Création de la police avec style gras
        font = QFont("Arial", 10)
        font.setBold(True)  # Active le gras

        format4 = QgsTextFormat()
        format4.setFont(font)
        format4.setSize(10)
        format4.setColor(QColor("white"))  # Valeur par défaut, on peut en ajouter d'autres

        settings4.setFormat(format4)
        rule4.setSettings(settings4)
        rule4.setDescription("Parcelles possession = True et couleur foncé")
        root_rule.appendChild(rule4)

        # Appliquer le système d’étiquettes
        labeling = QgsRuleBasedLabeling(root_rule)
        layer.setLabeling(labeling)
        layer.setLabelsEnabled(True)
        layer.triggerRepaint()


    def display_point(self, point, button):

        # Désactivation displaypoint si le mode regroupement est en cours d'utilisation
        if self.group_mode_active:
            return

        self.last_position = self.dlg.pos()
        self.dlg.hide()

        # Sauvegarde le point cliqué dans un attribut pour pouvoir l’utiliser ensuite
        self.last_click_point = point

        # Vérifie que la couche active est valide
        layer = get_valid_active_layer(self.dlg)
        if not layer:
            return

        # Recherche de la géométrie cliquée
        req = QgsFeatureRequest().setFilterRect(QgsGeometry.fromPointXY(QgsPointXY(point.x(), point.y())).boundingBox())
        #entites = layer.getFeatures(req)

        entites = list(layer.getFeatures(req))

        if not entites:
            QMessageBox.information(
                self.dlg,
                tr("Aucune entité"),
                tr("Aucune entité sélectionnée à cet endroit.")
            )
            return

        for e in entites:
            # Stocke l'ID de l'entité cliquée
            self.current_feature_id = e.id()
            fid = e.id()
            # Stock id pour finance_manager
            # Stocke le champ attributaire "id" (utilisé pour la finance, etc.)
            if "id" in e.fields().names():
                self.dlg.current_parcelle_id = e["id"]
                print("ID attributaire récupéré :", e["id"])

            # Affiche les coordonnées
            self.dlg.coordClick.setText(f"{point.x()}, {point.y()}")

            # Affiche la section, numéro, indice
            indice = e['indice_parc'] if 'indice_parc' in e.fields().names() and e['indice_parc'] else ''
            self.dlg.coord2.setText(e['section'] + e['numero'] + indice)

            # Affiche la surface
            self.dlg.surface.setText(str(e['SURFACE']) + ' ares')


            # Remplit les champs texte simples
            safe_set_text(self.dlg.annee, e['annee'])
            #safe_set_text(self.dlg.prix, e['prix'])
            #apply_numeric_to_lineedit(self.dlg.prix, e["prix"], decimals=2, with_currency=True)
            apply_numeric_to_lineedit(self.dlg.prix, e["prix"], decimals=0, with_currency=True)

            safe_set_text(self.dlg.dateAchat, e['dateAchat'])
            apply_numeric_to_lineedit(self.dlg.prix_Act, e["prixAct"], decimals=0, with_currency=True)
            #safe_set_text(self.dlg.prix_Act, e['prixAct'])
            safe_set_text(self.dlg.totalPlants, e['totalplants'])
            safe_set_text(self.dlg.editTerrain, e['Terrain'])
            safe_set_text(self.dlg.editAcces, e['Acces'])
            safe_set_text(self.dlg.editRemTerrain, e['RemTerrain'])

            # Mémorisation des valeurs Terrain et Accès
            terrain = e['Terrain']
            acces = e['Acces']

            start = time.time()
            self.remplir_combobox_terrain_acces(terrain, acces)
            self.remplir_combobox_terrain_acces()
            end = time.time()
            print(f"remplir_combobox_terrain_acces took {end - start:.3f} seconds")

            # Affiche la possession
            self.dlg.checkBoxPossession.setChecked(bool(e['Possession']))

            # Affiche le libellé du type de parcellaire
            type_parc_val = e['typeParc']
            libelle = TYPE_PARC_LIBELLES.get(type_parc_val, "Inconnu")
            self.dlg.libelleTypeParc.setText(libelle)

            # Couleur de fond en fonction du type
            layer = get_valid_active_layer(self.dlg)
            if not layer:
                return
            couleur = get_fill_color_from_layer(layer, type_parc_val)
            QgsMessageLog.logMessage(
                f"ColorFrame dp mise à jour avec couleur {couleur} pour typeParc {type_parc_val}",
                "gestion_forestiere",
                level=Qgis.Info
            )
            self.dlg.colorFrame.setStyleSheet(f"background-color: {couleur}; border: 1px solid black;")

            # Coordonnées du voisin
            safe_set_text(self.dlg.nomProp, e['nom_Voisin'])
            safe_set_text(self.dlg.lineAdress, e['adresse_Voisin'])
            safe_set_text(self.dlg.lineMail, e['mail_Voisin'])
            safe_set_text(self.dlg.lineTel, e['tel_Voisin'])

            # Champs modifiables
            self.remplir_champs_modifiables(e)

            # Possession : influe sur les onglets visibles
            self.is_proprietaire = bool(e['possession'])
            self.dlg.checkBoxSaisie.setChecked(False)
            self.update_tab_visibility()

            # Infos plantations
            txt_plants, txt_taux = self.build_liste_arbres(e)
            self.dlg.plantation.setText(txt_plants)
            self.dlg.taux.setText(txt_taux)

            # --- Alignement à droite pour taux ---
            cursor = self.dlg.taux.textCursor()
            cursor.select(QTextCursor.SelectionType.Document)
            self.dlg.taux.setTextCursor(cursor)
            self.dlg.taux.setAlignment(Qt.AlignmentFlag.AlignRight)

            # Connexions pour la checkbox de possession
            try:
                self.dlg.checkBoxPossession.toggled.disconnect()
            except TypeError:
                pass
            self.dlg.checkBoxPossession.toggled.connect(self.handle_possession_toggle)

            # Connexion pour la case à cocher de saisie
            try:
                self.dlg.checkBoxSaisie.toggled.disconnect(self.handle_checkbox_toggle)
            except TypeError:
                pass
            self.dlg.checkBoxSaisie.toggled.connect(self.handle_checkbox_toggle)

            # Initialisation des combos de saisie
            fid = self.current_feature_id
            if not layer or fid is None:
                return
            feature = layer.getFeature(fid)

            for prefix, combo_base in [("plant", "comboPlant"), ("Tvx", "comboTvx"), ("Trait", "comboTrait"),
                                       ("Prev", "comboPrev")]:
                self.init_saisie_combos(prefix, combo_base, feature)


            # Initialisation des combos de légendes modifiables
            self.init_combo_modif_legend(feature)

            # Infos complémentaires
            self.dlg.travauxListe.setText(self.build_travaux_dates(e))
            self.dlg.travauxRq.setText(self.build_travaux_remarques(e))
            self.dlg.traitListe.setText(self.build_trait_dates(e))
            self.dlg.traitRq.setText(self.build_trait_remarques(e))
            self.dlg.prevListe.setText(self.build_prev_dates(e))
            self.dlg.prevRq.setText(self.build_prev_remarques(e))

        # Positionnement de la fenêtre
        if self.first_show:
            qr = self.dlg.frameGeometry()
            cp = self.dlg.screen().availableGeometry().center()
            qr.moveCenter(cp)
            self.dlg.move(qr.topLeft())
            self.first_show = False
        elif self.last_position is not None:
            self.dlg.move(self.last_position)

        # Mode lecture seule ou édition
        self.dlg.checkEditProp.setChecked(False)
        self.toggle_edit_mode()

        # Connexion à la checkbox de saisie
        try:
            self.dlg.checkBoxSaisie.toggled.disconnect()
        except TypeError:
            pass
        self.dlg.checkBoxSaisie.toggled.connect(self.handle_checkbox_toggle)

        # Connexion à la checkbox de configuration
        try:
            self.dlg.checkBoxConfig.toggled.disconnect()
        except Exception:
            pass
        self.dlg.checkBoxConfig.toggled.connect(self.update_tab_visibility)

        # Initialisation du bouton "fill ring"
        self.init_ring_fill_button()

        # Initialisation bouton regroupement
        self.init_group_polygon_button()

        # Appel affichage données financières
        id_parcelle = str(self.dlg.current_parcelle_id)
        self.afficher_recettes_depenses(id_parcelle)

        # Initialisation bouton suppression parcelle
        try:
            self.dlg.btnSupParcelle.clicked.disconnect()

        except TypeError:
            pass

        # Connexion du bouton en passant fid et last_click_point
        self.dlg.btnSupParcelle.clicked.connect(
            lambda: self.infos_polygon_manager.supprimer_entite_fille(self.current_feature_id)
        )

        # Appel style bouton suppression parcelle
        self.init_supparc_button()

        # Mise à jour finale des onglets
        self.dlg.checkBoxConfig.blockSignals(True)
        self.dlg.checkBoxConfig.setChecked(False)
        self.dlg.checkBoxConfig.blockSignals(False)
        self.update_tab_visibility()

        # Affichage de la fenêtre
        self.dlg.show()

    # Affichage des résultats de l'onglet Analyses dans les champs
    def data_analyse(self):

        if not hasattr(self, 'analyse_results') or not self.analyse_results:
            # Pas encore de résultats : afficher rien ou message
            return

        results = self.analyse_results

        self.dlg.lineTotalSurface.setText(str(results.get("surface_forestiere", "")))
        self.dlg.lineTotalSurfaceF.setText(str(results.get("surface_friche", "")))
        self.dlg.lineTotalRepiq.setText(str(results.get("total_plants", "")))
        self.dlg.textEditAnalyse.setPlainText(results.get("types_parcelles", ""))
        self.dlg.textEditEssences.setPlainText(results.get("types_essences", ""))
        self.dlg.textEditRegroup.setPlainText(results.get("regroupement", ""))
        self.dlg.lineTotalParcelles.setText(str(results.get("nb_parcelles", "")))

        # --- Nouveaux résultats (type 12 = Achat, type 13 = Échange)
        self.dlg.surface_Achat.setText(str(results.get("surface_achat", "")))
        self.dlg.liste_Achat.setPlainText(str(results.get("liste_achat", "")))
        self.dlg.surface_Echange.setText(str(results.get("surface_echange", "")))
        self.dlg.liste_Echange.setPlainText(str(results.get("liste_echange", "")))

    def rechercher_parcelle(self):
        # Demande à l'utilisateur la recherche (ex : D566)
        texte, ok = QInputDialog.getText(
            self.dlg,
            tr("Recherche"),
            tr("Numéro de parcelle (ex : D566, AR1241) :")
        )
        if not ok or not texte:
            return

        texte = texte.strip().upper()

        # Extraire la partie lettre (section) et la partie nombre (numero)
        import re
        m = re.match(r"([A-Z]+)(\d+)", texte)
        if not m:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Format invalide. Exemple attendu : D566")
            )
            return

        section_input, numero_input = m.groups()
        numero_input = int(numero_input)  # conversion en entier

        layer = self.iface.activeLayer()
        if not layer:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr("Aucune couche active.")
            )
            return

        # Construire une expression SQL qui filtre la section, le numero, et indice_parc est vide ou NULL
        expr = f"\"section\" = '{section_input}' AND \"numero\" = {numero_input} AND (\"indice_parc\" IS NULL OR \"indice_parc\" = '')"

        request = QgsFeatureRequest().setFilterExpression(expr)

        for feature in layer.getFeatures(request):
            geom = feature.geometry()
            if geom:
                # Centrage sur la parcelle sans modifier le zoom
                canvas = self.iface.mapCanvas()
                current_extent = canvas.extent()
                center = geom.boundingBox().center()
                new_extent = QgsRectangle(
                    center.x() - current_extent.width() / 2,
                    center.y() - current_extent.height() / 2,
                    center.x() + current_extent.width() / 2,
                    center.y() + current_extent.height() / 2,
                )
                canvas.setExtent(new_extent)
                canvas.refresh()
                return

        QMessageBox.information(
            self.dlg,
            tr("Aucune correspondance"),
            tr("Aucune parcelle trouvée pour : ") + texte
        )

    def refresh_combo_finance(self):
        table = self.dlg.tableWidgetData
        combo = self.dlg.comboBoxFinance

        # Récupérer les valeurs uniques de la colonne 6 (index 5)
        valeurs = set()
        for row in range(table.rowCount()):
            item = table.item(row, 5)  # colonne 6 = index 5
            if item:
                text = item.text().strip()
                if text:
                    valeurs.add(text)

            # Vider le combo
            combo.clear()

            # Ajouter un élément vide en premier
            combo.addItem("")

            # Ajouter ensuite les valeurs triées
            for val in sorted(valeurs):
                combo.addItem(val)

            # Sélectionner par défaut le champ vide
            combo.setCurrentIndex(0)

    # def on_save_finance_clicked(self):
    #     self.finance_manager.add_finance_entry()


    def handle_finance_entry(self):
        self.finance_manager.add_finance_entry()
        self.refresh_combo_finance()

        layer = self.iface.activeLayer()
        id_parcelle = getattr(self.dlg, "current_parcelle_id", "")

        request = QgsFeatureRequest().setFilterExpression(f'"id" = \'{id_parcelle}\'')
        features = list(layer.getFeatures(request))

        if features:
            self.remplir_champs_financiers(features[0])
        else:
            QMessageBox.warning(
                self.dlg,
                tr("Erreur"),
                tr(f"Aucune entité trouvée avec l'ID {id_parcelle}")
            )

    def afficher_recettes_depenses(self, id_parcelle):
        """
        Affiche les recettes et dépenses d'une parcelle
        et calcule les totaux avec devise et locale.
        """
        chemin_csv = get_finances_csv_path()
        if not chemin_csv or not os.path.exists(chemin_csv):
            self.dlg.textEditRecettes.setPlainText("Aucune donnée.")
            self.dlg.textEditDepenses.setPlainText("Aucune donnée.")
            self.dlg.lineEditTotRecettes.setText("0.00")
            self.dlg.lineEditTotDepenses.setText("0.00")
            self.dlg.lineEditEcart.setText("0.00")
            return

        lignes_recettes = []
        lignes_depenses = []
        total_recettes = 0.0
        total_depenses = 0.0

        locale, currency = get_locale_and_currency()

        def parse_montant(montant_str):
            """Convertit une chaîne '123,45 €' en float 123.45"""
            if not montant_str:
                return 0.0

            montant_clean = montant_str

            # Enlever devise si présente
            if currency:
                montant_clean = montant_clean.replace(currency, "")

            # Nettoyage général
            montant_clean = (
                montant_clean.replace(" ", "")  # espaces
                .replace("\u202f", "")  # espaces insécables
                .replace(",", ".")  # virgule → point
                .strip()
            )

            try:
                return float(montant_clean)
            except ValueError:
                return 0.0

        with open(chemin_csv, newline='', encoding='utf-8') as f:
            reader = csv.reader(f)
            next(reader, None)  # en-tête

            for row in reader:
                if len(row) < 6:
                    continue

                id_csv, date_csv, libelle, type_finance, montant_str, commentaire = row

                if id_csv != id_parcelle:
                    continue

                date_str = format_date(date_csv)
                montant_val = parse_montant(montant_str)

                # Formatage affichage avec locale + devise
                formatted = f"{montant_val:.2f}"
                decimal_point = locale.decimalPoint()
                if decimal_point != '.':
                    formatted = formatted.replace('.', decimal_point)
                if currency:
                    formatted += f" {currency}"

                ligne = f"{date_str} | {libelle} | {formatted} | {commentaire}"

                if type_finance == "Recette":
                    lignes_recettes.append(ligne)
                    total_recettes += montant_val
                elif type_finance == "Dépense":
                    lignes_depenses.append(ligne)
                    total_depenses += montant_val

        ecart = total_recettes - total_depenses

        # --- Affichage texte ---
        self.dlg.textEditRecettes.setPlainText(
            "\n".join(lignes_recettes) if lignes_recettes else tr("Aucune recette.")
        )
        self.dlg.textEditDepenses.setPlainText(
            "\n".join(lignes_depenses) if lignes_depenses else tr("Aucune dépense.")
        )

        # --- Affichage des totaux ---
        apply_numeric_to_lineedit(self.dlg.lineEditTotRecettes, total_recettes, decimals=2, with_currency=True)
        apply_numeric_to_lineedit(self.dlg.lineEditTotDepenses, total_depenses, decimals=2, with_currency=True)
        apply_numeric_to_lineedit(self.dlg.lineEditEcart, ecart, decimals=2, with_currency=True)

        # Mise en rouge de lineEditEcart si négatif avec conservation du style
        base_style = "font: bold 14pt 'Garamond';"

        if ecart < 0:
            self.dlg.lineEditEcart.setStyleSheet(base_style + " color: red;")
        else:
            self.dlg.lineEditEcart.setStyleSheet(base_style)

    def save_prix_date(self):
        layer = self.iface.activeLayer()
        if not layer:
            QMessageBox.critical(self.dlg, tr("Erreur"), tr("Aucune couche active."))
            return

        if not layer.isEditable():
            if not layer.startEditing():
                QMessageBox.critical(self.dlg, tr("Erreur"), tr("Impossible d'activer le mode édition."))
                return

        id_parcelle = self.dlg.current_parcelle_id

        request = QgsFeatureRequest().setFilterExpression(f'"id" = \'{id_parcelle}\'')
        features = list(layer.getFeatures(request))
        if not features:
            QMessageBox.warning(self.dlg, tr("Erreur"), tr("Aucune entité trouvée pour l'identifiant spécifié."))
            return

        feature = features[0]

        # 🔥 LECTURE DIRECTE AU LIEU DU TEXTE
        prix_val = int(self.dlg.prixModif.value())
        prix_act_val = int(self.dlg.prixActModif.value())

        date_val = self.dlg.dateEditAchat.date()

        # Indices des champs
        idx_prix = layer.fields().indexFromName('prix')
        idx_date = layer.fields().indexFromName('dateAchat')
        idx_prixAct = layer.fields().indexFromName('prixAct')

        # Mise à jour
        success1 = layer.changeAttributeValue(feature.id(), idx_prix, prix_val)
        success2 = layer.changeAttributeValue(feature.id(), idx_date, date_val)
        success3 = True
        if idx_prixAct != -1:
            success3 = layer.changeAttributeValue(feature.id(), idx_prixAct, prix_act_val)

        if success1 and success2 and success3:
            QMessageBox.information(self.dlg, tr("Succès"), tr("Prix et date d'achat sauvegardés."))
        else:
            QMessageBox.critical(self.dlg, tr("Erreur"), tr("Échec lors de la mise à jour des attributs."))

    def remplir_champs_financiers(self, feature):
        """Remplit uniquement les champs financiers à partir de l'entité."""

        # Pour le prix
        setup_doublespinbox(self.dlg.prixModif, value=feature['prix'], decimals=0, with_currency=True)
        # Pour la date d'achat
        date_achat = feature['dateAchat']

        if not date_achat:  # champ vide ou None
            self.dlg.dateEditAchat.setDate(QDate.currentDate())
        else:
            # Si le champ est un QDateTime ou QDate (ce qui est le cas si le champ est "Date")
            if hasattr(date_achat, 'date'):  # QDateTime
                self.dlg.dateEditAchat.setDate(date_achat.date())
            elif isinstance(date_achat, QDate):  # QDate directement
                self.dlg.dateEditAchat.setDate(date_achat)
            else:
                # fallback si quelque chose ne va pas
                self.dlg.dateEditAchat.setDate(QDate.currentDate())

        # Pour le prix Actuel
        setup_doublespinbox(self.dlg.prixActModif, value=feature['prixAct'], decimals=0, with_currency=True)

    def open_finances_csv(self):

        # 1. Récupération du nom de la couche active
        layer = self.iface.activeLayer()
        if not layer:
            QMessageBox.warning(self.dlg, tr("Erreur"), tr("Aucune couche active."))
            return

        layer_name = re.sub(r'\W+', '_', layer.name())

        # 2. Récupération du nom du projet
        project_path = QgsProject.instance().fileName()
        prefix = os.path.basename(project_path)[:3].lower() if project_path else "def"

        # 3. Construction du nom et du chemin du fichier CSV
        filename = f"fin_{layer_name}_{prefix}.csv"
        plugin_dir = os.path.dirname(__file__)
        csv_path = os.path.join(plugin_dir, "data", filename)

        # 4. Vérification et ouverture
        if not os.path.exists(csv_path):
            QMessageBox.warning(self.dlg, tr("Fichier introuvable"),
                                tr("Le fichier {} est introuvable.").format(filename))
            return

        try:
            if os.name == 'nt':  # Windows
                os.startfile(csv_path)
            elif os.name == 'posix':  # Linux / macOS
                subprocess.call(["xdg-open", csv_path])
            else:
                QMessageBox.warning(self.dlg, tr("Erreur"), tr("Système non supporté."))
        except Exception as e:
            QMessageBox.critical(self.dlg, tr("Erreur"), tr("Impossible d'ouvrir le fichier :\n{}").format(str(e)))


    # Gestion dynamique de la couche utilisée
    def on_layer_selected_from_tree(self, new_layer):
        print("[DEBUG] Signal reçu dans on_layer_selected_from_tree")
        if not new_layer:
            print("[DEBUG] Pas de couche active")
            return
        print(f"[DEBUG] Couche sélectionnée : {new_layer.name()}")

        # Filtrer les cas où new_layer est None ou non valide
        if not isinstance(new_layer, QgsVectorLayer) or not new_layer.isValid():
            print("[DEBUG] Couche ignorée (vide ou non vectorielle)")
            return

        # Vérifie si le plugin est actif
        plugin_active = getattr(self, "plugin_active", False)

        # Vérification couche active via l'utilitaire
        # -> Si plugin actif, afficher les MessageBox
        # -> Sinon, mode silencieux, renvoie None si couche non valide
        valid_layer = get_valid_active_layer(self.dlg, silent=not plugin_active)
        if not valid_layer:
            print("[DEBUG] Couche non valide pour l’analyse")
            return

        # Si une analyse est déjà en cours, bloquer le changement si on change de couche
        if getattr(self, "analyse_en_cours", False):
            if self.layer and new_layer.id() != self.layer.id():
                if plugin_active:
                    QMessageBox.warning(
                        self.dlg,
                        tr("Analyse en cours"),
                        tr("Une analyse est en cours. Veuillez revenir à la couche précédente.")
                    )
                print("[DEBUG] Analyse en cours, changement de couche bloqué")
                return
            else:
                print("[DEBUG] Changement de couche ignoré (même couche)")
                return

        # Mise à jour de la couche
        self.layer = valid_layer
        if plugin_active:
            self.dlg.lineLayerAct.setText(valid_layer.name())
            print(f"[INFO] Nouvelle couche sélectionnée : {valid_layer.name()}")

        # Lancer l’analyse
        print("[DEBUG] appel start_analyse_worker")
        self.start_analyse_worker()

    def on_project_loaded(self):
        if not self.iface.activeLayer():
            QMessageBox.information(
                self.dlg,
                tr("Conseil de rechargement"),
                tr("Sélectionnez une couche avant le chargement du plugin.")
            )

    def on_project_closed(self):
        print("[INFO] Projet en cours de fermeture")

        self.analyse_en_cours = False
        self.layer = None
        self.dlg.lineLayerAct.clear()


# Assigne la date du jour aux QDateEdit
def set_all_qdateedit_to_today(parent_widget):
    today = QDate.currentDate()
    for child in parent_widget.findChildren(QDateEdit):
        child.setDate(today)