# -*- coding: utf-8 -*-
"""
Created on 15 jan. 2026

Gestionnaire de contraintes pour la vue tabulaire (attribute table).
Valide les contraintes lors de la sauvegarde des modifications.
"""
import re
from typing import Tuple, Optional, Dict, Any, List
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.PyQt.QtCore import QTimer
from qgis.core import QgsVectorLayer, QgsFeature, Qgis
from qgis.utils import iface
import qgis.core

class TableViewConstraints:
    """
    Gestionnaire de contraintes pour la vue tabulaire.
    Valide les contraintes des attributs avant la validation des modifications.
    """

    def __init__(self, layer: QgsVectorLayer, constraintsData: list) -> None:
        """
        Initialise le gestionnaire de contraintes.

        :param layer: La couche vectorielle QGIS
        :type layer: QgsVectorLayer
        :param constraintsData: Les données de contraintes provenant de l'espace collaboratif
        :type constraintsData: list
        """
        self.layer = layer
        self.constraintsData = constraintsData
        self.isConnected = False
        
        # Dictionnaire pour un accès rapide aux contraintes par nom de champ
        self.constraintsByField = {}
        self._buildConstraintsIndex()
        
        # Configuration des validateurs
        # La contrainte NOT NULL est validée uniquement pour les objets existants (fid >= 0).
        # Pour les nouveaux objets (fid < 0) et le champ identifiant (idNameForDatabase),
        # elle est ignorée côté client (voir validateFieldValue).
        self._validatorConfig = {
            'nullable': {
                'check': lambda c, v: c.get('nullable') is False and (v is None or v == qgis.core.NULL or v == '' or v == 'NULL'),
                'message': lambda f, c, v: f"Le champ '{f}' ne peut pas être vide (contrainte NOT NULL)"
            },
            'min_length': {
                'check': lambda c, v: c.get('min_length') is not None and v is not None and len(str(v)) < c['min_length'],
                'message': lambda f, c, v: f"Le champ '{f}' doit contenir au moins {c['min_length']} caractères"
            },
            'max_length': {
                'check': lambda c, v: c.get('max_length') is not None and v is not None and len(str(v)) > c['max_length'],
                'message': lambda f, c, v: f"Le champ '{f}' ne peut pas dépasser {c['max_length']} caractères"
            },
            'min_value': {
                'check': lambda c, v: c.get('min_value') is not None and v is not None and self._tryFloat(v, c['min_value'], lambda nv, mv: nv < mv),
                'message': lambda f, c, v: f"Le champ '{f}' doit être supérieur ou égal à {c['min_value']}"
            },
            'max_value': {
                'check': lambda c, v: c.get('max_value') is not None and v is not None and self._tryFloat(v, c['max_value'], lambda nv, mv: nv > mv),
                'message': lambda f, c, v: f"Le champ '{f}' doit être inférieur ou égal à {c['max_value']}"
            },
            'pattern': {
                'check': lambda c, v: c.get('pattern') is not None and v is not None and v != qgis.core.NULL and v != 'NULL' and not re.match(c['pattern'], str(v)),
                'message': lambda f, c, v: f"Le champ '{f}' ne correspond pas au format attendu"
            },
            'enum': {
                # Skip null values: qgis.core.NULL (QGIS auto-null) and 'NULL' (our ValueMap stored value)
                # are both valid representations of a null choice and must not be enum-checked.
                'check': lambda c, v: c.get('enum') is not None and v is not None and v != qgis.core.NULL and v != 'NULL' and not self._isInEnum(v, c['enum']),
                'message': lambda f, c, v: f"Le champ '{f}' doit avoir une des valeurs prédéfinies"
            }
        }

    def _buildConstraintsIndex(self) -> None:
        """
        Construit un index des contraintes par nom de champ pour un accès rapide.
        """
        for fieldData in self.constraintsData:
            if fieldData.get('name'):
                self.constraintsByField[fieldData['name']] = fieldData

    def connectSignals(self) -> None:
        """
        Connecte les signaux nécessaires pour la validation des contraintes.
        """
        if not self.isConnected:
            # Connecte au signal d'édition démarré
            self.layer.editingStarted.connect(self.onEditingStarted)
            
            # Connecte au signal de modification d'attribut (temps réel)
            self.layer.attributeValueChanged.connect(self.onAttributeValueChanged)
            # Connecte au signal avant validation des changements
            self.layer.beforeCommitChanges.connect(self.validateBeforeCommit)
            self.isConnected = True

    def disconnectSignals(self) -> None:
        """
        Déconnecte les signaux.
        """
        if self.isConnected:
            try:
                self.layer.editingStarted.disconnect(self.onEditingStarted)
                self.layer.attributeValueChanged.disconnect(self.onAttributeValueChanged)
                self.layer.beforeCommitChanges.disconnect(self.validateBeforeCommit)
            except TypeError:
                # Les signaux n'étaient pas connectés
                pass
            self.isConnected = False

    def onEditingStarted(self) -> None:
        """
        Appelé quand l'édition démarre sur la couche.
        """
        if iface:
            iface.messageBar().pushMessage(
                "Mode édition",
                f"Édition activée sur '{self.layer.name()}'. Les contraintes seront validées lors de la sauvegarde.",
                level=Qgis.MessageLevel.Info,
                duration=3
            )

    def onAttributeValueChanged(self, fid: int, fieldIndex: int, newValue: Any) -> None:
        """
        Appelé en temps réel quand une valeur d'attribut change dans la table.
        Permet de valider immédiatement la nouvelle valeur.
        
        :param fid: ID de l'entité modifiée
        :param fieldIndex: Index du champ modifié
        :param newValue: Nouvelle valeur saisie
        """
        # Les valeurs null-like (None, qgis.core.NULL, 'NULL') sont toujours valides à ce stade
        # et seront gérées par validateBeforeCommit si nécessaire.
        if newValue is None or newValue == qgis.core.NULL or newValue == 'NULL':
            return

        fieldName = self.layer.fields()[fieldIndex].name()

        # Pour un champ nullable, une chaîne vide est un état transitoire vers NULL
        # (l'utilisateur vide le champ avant de le passer à NULL explicitement).
        # La validation finale au moment du commit gérera ce cas si besoin.
        constraint = self.constraintsByField.get(fieldName)
        if newValue == '' and (not constraint or constraint.get('nullable') is not False):
            return

        feature = self.layer.getFeature(fid)
        
        # Valider la nouvelle valeur
        isValid, errorMsg = self.validateFieldValue(fieldName, newValue, fid, feature)
        
        if not isValid:
            # Afficher un avertissement immédiat
            if iface:
                iface.messageBar().pushMessage(
                    "Attention - Contrainte non respectée",
                    f"Entité #{fid}, champ '{fieldName}': {errorMsg}",
                    level=Qgis.MessageLevel.Warning,
                    duration=5
                )

    def getValidationErrors(self, editBuffer) -> list:
        """
        Valide le buffer sans effet de bord et retourne la liste des erreurs de validation.
        Peut être appelé depuis n'importe quel point du flux de sauvegarde.
        """
        if not editBuffer:
            return []
        validationErrors = []
        for fid, attributes in editBuffer.changedAttributeValues().items():
            feature = self.layer.getFeature(fid)
            if feature.isValid():
                validationErrors.extend(self._validateFeatureAttributes(fid, feature, attributes.items()))
        for fid, feature in editBuffer.addedFeatures().items():
            allFields = [(i, feature.attribute(i)) for i in range(len(self.layer.fields()))]
            validationErrors.extend(self._validateFeatureAttributes(fid, feature, allFields))
        return validationErrors

    def rollBackAndPreserveChanges(self) -> None:
        """
        Annule le commit en cours et restaure les modifications de l'utilisateur dans un nouveau
        buffer d'édition, afin qu'il puisse corriger les erreurs sans perdre son travail.

        Stratégie :
        - On appelle editBuffer.rollBack() (et NON layer.rollBack()) pour vider le buffer sans
          terminer la session d'édition en cours. QGIS va donc committer « rien » vers SpatiaLite
          et fermer proprement la session via son propre cycle commitChanges().
        - Un QTimer.singleShot(0, ...) planifie la restauration des modifications APRÈS que le
          cycle commitChanges() de QGIS ait entièrement terminé de s'exécuter. Cela évite de
          ré-entrer dans commitChanges() depuis l'intérieur d'un handler beforeCommitChanges,
          ce qui provoquerait un second commit (et donc un second envoi au serveur).
        - Les entités ajoutées (fid < 0) sont re-ajoutées depuis leur objet QgsFeature sauvegardé.
        - Les modifications d'attributs et de géométrie sont re-appliquées pour les entités
          existantes (fid >= 0).
        - Les suppressions (fid >= 0) sont re-appliquées.
        """
        from qgis.core import QgsFeature, QgsGeometry

        editBuffer = self.layer.editBuffer()

        # --- 1. Sauvegarder l'état du buffer avant de le vider ---
        added_features = []
        changed_attrs = {}  # {fid: {fieldIdx: value}} — entités existantes uniquement
        changed_geoms = {}  # {fid: QgsGeometry}       — entités existantes uniquement
        deleted_fids = []

        if editBuffer:
            for fid, feature in editBuffer.addedFeatures().items():
                added_features.append(QgsFeature(feature))
            for fid, attrMap in editBuffer.changedAttributeValues().items():
                if fid >= 0:
                    changed_attrs[fid] = dict(attrMap)
            for fid, geom in editBuffer.changedGeometries().items():
                if fid >= 0:
                    changed_geoms[fid] = QgsGeometry(geom)
            deleted_fids = list(editBuffer.deletedFeatureIds())

            # --- 2. Vider le buffer pour que QGIS committe « rien » vers SpatiaLite ---
            # editBuffer.rollBack() vide le contenu du buffer sans terminer la session ;
            # QGIS trouve un buffer vide et ne modifie pas SpatiaLite, puis ferme la session.
            editBuffer.rollBack()

        # --- 3. Planifier la restauration APRÈS la fin du cycle commitChanges() ---
        # QTimer.singleShot(0) s'exécute dès que la pile d'appels courante est terminée,
        # c'est-à-dire après que QGIS ait fermé la session d'édition. On évite ainsi tout
        # ré-entrée dans la mécanique de commit.
        layer = self.layer  # capture locale pour la lambda

        def _restore():
            if not layer.isEditable():
                layer.startEditing()
            for feature in added_features:
                layer.addFeature(feature)
            for fid, attrMap in changed_attrs.items():
                for fieldIdx, value in attrMap.items():
                    layer.changeAttributeValue(fid, fieldIdx, value)
            for fid, geom in changed_geoms.items():
                layer.changeGeometry(fid, geom)
            for fid in deleted_fids:
                layer.deleteFeature(fid)
            # Le flag peut être resté True si __connectBeforeCommitChanges a court-circuité
            # sa vérification (editBuffer vide) sans jamais atteindre le code de nettoyage.
            # On le remet à False ici pour que le prochain commit parte bien au serveur.
            layer._validation_already_handled = False

        QTimer.singleShot(0, _restore)

    def validateBeforeCommit(self) -> None:
        """
        Valide toutes les modifications avant de les enregistrer.
        Empêche la sauvegarde si des contraintes ne sont pas respectées.

        Si le gestionnaire de sauvegarde du plugin (PluginModule.__saveChangesForOneLayer)
        a déjà effectué la validation et le rollback-with-preservation pour cette couche,
        il positionne le flag '_validation_already_handled' pour éviter un double rollback.
        """
        # Éviter le double rollback si la pipeline de sauvegarde a déjà traité cette couche
        if getattr(self.layer, '_validation_already_handled', False):
            self.layer._validation_already_handled = False
            return

        editBuffer = self.layer.editBuffer()
        if not editBuffer:
            return

        validationErrors = self.getValidationErrors(editBuffer)

        if validationErrors:
            self.showValidationErrors(validationErrors)
            self.rollBackAndPreserveChanges()
            # Signaler aux autres handlers connectés à beforeCommitChanges que
            # le rollback a déjà été effectué pour ne pas retraiter
            self.layer._validation_already_handled = True
            if iface:
                iface.messageBar().pushMessage(
                    "Erreur de validation",
                    "Sauvegarde annulée : des contraintes ne sont pas respectées. Corrigez les erreurs et sauvegardez à nouveau.",
                    level=Qgis.MessageLevel.Critical,
                    duration=7
                )

    def _validateFeatureAttributes(self, fid: int, feature: QgsFeature, 
                                   fieldItems: List[Tuple[int, Any]]) -> List[Dict[str, Any]]:
        """
        Valide une liste d'attributs pour une entité donnée.
        Méthode helper pour éviter la duplication de code.

        :param fid: ID de l'entité
        :param feature: L'entité à valider
        :param fieldItems: Liste de tuples (fieldIndex, value) à valider
        :return: Liste des erreurs de validation
        """
        errors = []
        for fieldIndex, value in fieldItems:
            fieldName = self.layer.fields()[fieldIndex].name()
            isValid, errorMsg = self.validateFieldValue(
                fieldName, 
                value, 
                fid, 
                feature
            )
            
            if not isValid:
                errors.append({
                    'fid': fid,
                    'fieldName': fieldName,
                    'error': errorMsg,
                    'value': value
                })
        
        return errors

    def validateFieldValue(self, fieldName: str, value: Any, fid: int, feature: QgsFeature) -> Tuple[bool, str]:
        """
        Valide une valeur de champ selon les contraintes définies.
        Utilise une configuration data-driven pour valider dynamiquement.

        :param fieldName: Nom du champ
        :param value: Valeur à valider
        :param fid: ID de l'entité
        :param feature: L'entité complète (pour les contraintes contextuelles)
        :return: Tuple (isValid, errorMessage)
        """
        constraint = self.constraintsByField.get(fieldName)
        if not constraint:
            return True, ""

        # Validation de l'unicité (nécessite une logique spéciale avec fid)
        if constraint.get('unique') is True and value is not None:
            if self._checkUnique(fieldName, value, fid):
                return False, f"Le champ '{fieldName}' doit être unique. La valeur '{value}' existe déjà"

        # La contrainte NOT NULL est ignorée pour :
        #   - les nouvelles entités (fid < 0) : validée côté WfsPost avant l'envoi serveur
        #   - le champ identifiant (idNameForDatabase) : valeur assignée par le serveur
        isNewFeature = fid < 0
        idNameForDatabase = getattr(self.layer, 'idNameForDatabase', None)

        # Itérer sur tous les validateurs configurés
        for validatorKey, config in self._validatorConfig.items():
            if validatorKey == 'nullable' and (isNewFeature or fieldName == idNameForDatabase):
                continue
            try:
                if config['check'](constraint, value):
                    return False, config['message'](fieldName, constraint, value)
            except Exception:  # nosec B112
                # Si le validateur échoue (ex: conversion impossible), on continue
                continue

        return True, ""
    
    # === Méthodes helper pour les validations complexes ===
    
    def _tryFloat(self, value: Any, constraintValue: Any, comparator) -> bool:
        """Tente une comparaison numérique, retourne False si impossible"""
        try:
            return comparator(float(value), float(constraintValue))
        except (ValueError, TypeError):
            return False
    
    def _isInEnum(self, value: Any, enumValues: Any) -> bool:
        """Vérifie si une valeur est dans l'énumération"""
        validValues = enumValues if isinstance(enumValues, list) else list(enumValues.keys()) if isinstance(enumValues, dict) else []
        return value in validValues or str(value) in [str(v) for v in validValues]
    
    def _checkUnique(self, fieldName: str, value: Any, fid: int) -> bool:
        """Vérifie si la valeur est unique dans la couche"""
        for existingFeature in self.layer.getFeatures():
            if existingFeature.id() != fid:
                if existingFeature.attribute(fieldName) == value:
                    return True  # Duplicate trouvé
        return False  # Unique

    def showValidationErrors(self, errors: List[Dict[str, Any]]) -> None:
        """
        Affiche les erreurs de validation à l'utilisateur.

        :param errors: Liste des erreurs de validation
        """
        if not errors:
            return

        # Construire le message d'erreur
        errorMessage = "Les contraintes suivantes ne sont pas respectées :\n\n"
        
        # Grouper les erreurs par entité
        errorsByFeature = {}
        for error in errors:
            fid = error['fid']
            if fid not in errorsByFeature:
                errorsByFeature[fid] = []
            errorsByFeature[fid].append(error)

        # Limiter l'affichage aux 10 premières erreurs pour ne pas surcharger
        errorCount = 0
        maxErrors = 10
        
        for fid, featureErrors in errorsByFeature.items():
            if errorCount >= maxErrors:
                remaining = len(errors) - errorCount
                errorMessage += f"\n... et {remaining} autres erreur(s)"
                break
            
            errorMessage += f"Entité #{fid} :\n"
            for error in featureErrors:
                errorMessage += f"  • {error['error']}\n"
                errorCount += 1
                if errorCount >= maxErrors:
                    break
            errorMessage += "\n"

        errorMessage += "\nVeuillez corriger ces erreurs avant de sauvegarder."

        # Afficher le message d'erreur
        msgBox = QMessageBox()
        msgBox.setIcon(QMessageBox.Icon.Critical)
        msgBox.setWindowTitle("Erreurs de validation des contraintes")
        msgBox.setText("Impossible de sauvegarder les modifications")
        msgBox.setInformativeText(errorMessage)
        msgBox.setStandardButtons(QMessageBox.StandardButton.Ok)
        msgBox.exec()