#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Synchronisateur de compteurs mémoire
 Détecte et corrige les désynchronisations entre l'index et les fichiers
═══════════════════════════════════════════════════════════════════════════════

Usage:
  python sync_counters.py              # Mode détection (affiche les écarts)
  python sync_counters.py --fix        # Mode correction (applique les fixes)
  python sync_counters.py --quiet      # Mode silencieux (exit code uniquement)
  python sync_counters.py --json       # Sortie JSON (pour intégration CI)

Exit codes:
  0 = Index synchronisé (aucun écart)
  1 = Écarts détectés (corrections nécessaires)
  2 = Erreur d'exécution du script

Implémente la recommandation F002 de l'analyse ANL-20260215-001.
"""

import yaml
import os
import sys
import argparse
import json
import re
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional
from dataclasses import dataclass, field
from datetime import datetime


@dataclass
class SyncDelta:
    """Représente un écart de synchronisation."""
    location: str           # Chemin dans _index.yaml (ex: "semantic.defect_patterns.count")
    expected: Any           # Valeur attendue (calculée)
    actual: Any             # Valeur actuelle (dans _index.yaml)
    source: str             # Source de la valeur attendue
    severity: str = "low"   # low, medium, high
    
    @property
    def is_mismatch(self) -> bool:
        return self.expected != self.actual
    
    def to_dict(self) -> dict:
        return {
            "location": self.location,
            "expected": self.expected,
            "actual": self.actual,
            "source": self.source,
            "severity": self.severity,
            "mismatch": self.is_mismatch
        }


@dataclass
class SyncReport:
    """Rapport complet de synchronisation."""
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    deltas: List[SyncDelta] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)
    files_checked: int = 0
    
    @property
    def has_mismatches(self) -> bool:
        return any(d.is_mismatch for d in self.deltas)
    
    @property
    def mismatch_count(self) -> int:
        return sum(1 for d in self.deltas if d.is_mismatch)
    
    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp,
            "summary": {
                "total_checks": len(self.deltas),
                "mismatches": self.mismatch_count,
                "errors": len(self.errors),
                "files_checked": self.files_checked,
                "status": "DESYNC" if self.has_mismatches else "SYNC"
            },
            "deltas": [d.to_dict() for d in self.deltas if d.is_mismatch],
            "errors": self.errors
        }


class MemorySynchronizer:
    """Synchronise les compteurs de l'index avec l'état réel des fichiers."""
    
    def __init__(self, base_dir: str, verbose: bool = True):
        self.base_dir = Path(base_dir)
        self.memory_dir = self.base_dir / "memory"
        self.index_path = self.memory_dir / "_index.yaml"
        self.verbose = verbose
        self.report = SyncReport()
        self.index_data: Dict = {}
        
    def log(self, message: str, force: bool = False):
        """Affiche un message si le mode verbose est activé."""
        if self.verbose or force:
            print(message)
    
    def load_yaml(self, filepath: Path) -> Optional[Dict]:
        """Charge un fichier YAML de manière sécurisée."""
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                return yaml.safe_load(f)
        except Exception as e:
            self.report.errors.append(f"Error loading {filepath}: {e}")
            return None
    
    def save_yaml(self, filepath: Path, data: Dict) -> bool:
        """Sauvegarde un fichier YAML en préservant les commentaires autant que possible."""
        try:
            # Pour préserver la structure, on fait une substitution ciblée
            with open(filepath, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Sérialiser les nouvelles données
            with open(filepath, 'w', encoding='utf-8') as f:
                yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
            
            return True
        except Exception as e:
            self.report.errors.append(f"Error saving {filepath}: {e}")
            return False
    
    def count_yaml_items(self, filepath: Path, key: str) -> int:
        """Compte les éléments dans une liste YAML."""
        data = self.load_yaml(filepath)
        if data and key in data and isinstance(data[key], list):
            return len(data[key])
        return 0
    
    def count_heuristics(self, filepath: Path) -> int:
        """
        Compte les heuristics dans le fichier testing_heuristics.yaml.
        
        Parcourt dynamiquement toutes les sections qui sont des listes
        (selection, coverage, api_testing, etc.) en excluant les clés
        méta (version, metadata). Cela évite de hardcoder les noms de
        sections, qui évoluent au fil des enrichissements.
        """
        data = self.load_yaml(filepath)
        if not data:
            return 0
        
        HEURISTIC_META_KEYS = {'version', 'last_updated', 'metadata'}
        
        count = sum(
            len(v) for k, v in data.items()
            if k not in HEURISTIC_META_KEYS and isinstance(v, list)
        )
        
        return count
    
    def _count_heuristic_ids(self, data: Any, count: int = 0) -> int:
        """Compte récursivement les éléments avec id commençant par HEU-."""
        if isinstance(data, dict):
            if "id" in data and isinstance(data["id"], str) and data["id"].startswith("HEU-"):
                count += 1
            for value in data.values():
                count = self._count_heuristic_ids(value, count)
        elif isinstance(data, list):
            for item in data:
                count = self._count_heuristic_ids(item, count)
        return count
    
    def get_internal_metadata(self, filepath: Path, key: str) -> Any:
        """Récupère une métadonnée interne d'un fichier (dans la section metadata)."""
        data = self.load_yaml(filepath)
        if data and "metadata" in data:
            return data["metadata"].get(key)
        return None
    
    def count_files_in_dir(self, directory: Path, exclude_templates: bool = True) -> int:
        """Compte les fichiers YAML dans un répertoire (hors templates)."""
        if not directory.exists():
            return 0
        
        count = 0
        for f in directory.glob("*.yaml"):
            if exclude_templates and f.name.startswith("_template"):
                continue
            count += 1
        return count
    
    def count_episodic_files(self, category: str) -> Tuple[int, Optional[str]]:
        """
        Compte les fichiers épisodiques et trouve le plus récent.
        
        Convention de nommage Argus : YYYY-MM-DD_HH-MM_{type}_{ID}.yaml
        Le tri lexicographique sur le nom suffit pour identifier le dernier.
        Les templates (_template_*) sont exclus du comptage.
        
        Returns:
            Tuple (count, latest_id)
        """
        directory = self.memory_dir / "episodic" / category
        if not directory.exists():
            return 0, None
        
        files = []
        for f in directory.glob("*.yaml"):
            if f.name.startswith("_template"):
                continue
            files.append(f)
        
        if not files:
            return 0, None
        
        # Trier par date dans le nom du fichier (format: YYYY-MM-DD_HH-MM_...)
        files.sort(key=lambda x: x.name, reverse=True)
        
        # Extraire l'ID du fichier le plus récent
        latest_file = files[0]
        latest_id = self._extract_id_from_filename(latest_file.name, category)
        
        return len(files), latest_id
    
    def _extract_id_from_filename(self, filename: str, category: str) -> Optional[str]:
        """Extrait l'ID d'un nom de fichier épisodique."""
        # Format: YYYY-MM-DD_HH-MM_{type}_{id}.yaml
        prefixes = {
            "analyses": "ANL",
            "decisions": "DEC",
            "incidents": "INC",
            "executions": "EXE"
        }
        prefix = prefixes.get(category, "")
        
        # Cherche le pattern {PREFIX}-{YYYYMMDD}-{seq}
        pattern = rf"({prefix}-\d{{8}}-\d{{3}})"
        match = re.search(pattern, filename)
        if match:
            return match.group(1)
        return None
    
    def count_projects(self) -> Tuple[int, List[str]]:
        """
        Compte les projets et retourne leurs IDs.
        
        Returns:
            Tuple (count, list of project ids)
        """
        projects_dir = self.memory_dir / "projects"
        if not projects_dir.exists():
            return 0, []
        
        project_ids = []
        for f in projects_dir.glob("*.yaml"):
            if f.name.startswith("_template"):
                continue
            # L'ID du projet est le nom du fichier sans extension
            project_id = f.stem
            project_ids.append(project_id)
        
        return len(project_ids), sorted(project_ids)
    
    def analyze(self) -> SyncReport:
        """
        Analyse l'état de synchronisation de l'index.
        
        Returns:
            SyncReport avec tous les écarts détectés.
        """
        self.log("\n" + "═" * 60)
        self.log("🔄 ARGUS COUNTER SYNCHRONIZER")
        self.log("═" * 60)
        self.log(f"  Memory root: {self.memory_dir}")
        
        # Charger l'index
        self.index_data = self.load_yaml(self.index_path)
        if not self.index_data:
            self.report.errors.append("Could not load _index.yaml")
            return self.report
        
        self.report.files_checked += 1
        
        # ─────────────────────────────────────────────────────────────────────
        # 1. Vérifier les compteurs sémantiques
        # ─────────────────────────────────────────────────────────────────────
        self.log("\n📊 Checking semantic counters...")
        
        # Defect patterns
        patterns_file = self.memory_dir / "semantic" / "defect_patterns.yaml"
        if patterns_file.exists():
            patterns_data = self.load_yaml(patterns_file)
            if patterns_data and "patterns" in patterns_data:
                actual_count = len(patterns_data["patterns"])
                self.report.files_checked += 1
                
                # Vérifier vs index
                index_count = self._get_nested(self.index_data, "semantic.defect_patterns.count", 0)
                self.report.deltas.append(SyncDelta(
                    location="semantic.defect_patterns.count",
                    expected=actual_count,
                    actual=index_count,
                    source="memory/semantic/defect_patterns.yaml → patterns[]",
                    severity="medium"
                ))
                
                # Vérifier vs metadata interne (dans metadata.total_patterns)
                internal_count = self.get_internal_metadata(patterns_file, "total_patterns") or 0
                self.report.deltas.append(SyncDelta(
                    location="defect_patterns.yaml → metadata.total_patterns",
                    expected=actual_count,
                    actual=internal_count,
                    source="Count of patterns[] in file",
                    severity="low"
                ))
                
                self._log_check("defect_patterns", index_count, actual_count)
        
        # Testing heuristics
        heuristics_file = self.memory_dir / "semantic" / "testing_heuristics.yaml"
        if heuristics_file.exists():
            # Compter les heuristics (réparties dans plusieurs sections)
            actual_count = self.count_heuristics(heuristics_file)
            self.report.files_checked += 1
            
            # Vérifier vs index
            index_count = self._get_nested(self.index_data, "semantic.testing_heuristics.count", 0)
            self.report.deltas.append(SyncDelta(
                location="semantic.testing_heuristics.count",
                expected=actual_count,
                actual=index_count,
                source="memory/semantic/testing_heuristics.yaml → all sections",
                severity="medium"
            ))
            
            # Vérifier vs metadata interne (dans metadata.total_heuristics)
            internal_count = self.get_internal_metadata(heuristics_file, "total_heuristics") or 0
            self.report.deltas.append(SyncDelta(
                location="testing_heuristics.yaml → metadata.total_heuristics",
                expected=actual_count,
                actual=internal_count,
                source="Count of all heuristics in file",
                severity="low"
            ))
            
            self._log_check("testing_heuristics", index_count, actual_count)
        
        # ─────────────────────────────────────────────────────────────────────
        # 2. Vérifier les compteurs épisodiques
        # ─────────────────────────────────────────────────────────────────────
        self.log("\n📝 Checking episodic counters...")
        
        for category in ["analyses", "decisions", "incidents", "executions"]:
            count, latest = self.count_episodic_files(category)
            
            index_count = self._get_nested(self.index_data, f"episodic.{category}.count", 0)
            index_latest = self._get_nested(self.index_data, f"episodic.{category}.latest", None)
            
            self.report.deltas.append(SyncDelta(
                location=f"episodic.{category}.count",
                expected=count,
                actual=index_count,
                source=f"File count in memory/episodic/{category}/",
                severity="medium"
            ))
            
            self.report.deltas.append(SyncDelta(
                location=f"episodic.{category}.latest",
                expected=latest,
                actual=index_latest,
                source=f"Most recent file in memory/episodic/{category}/",
                severity="low"
            ))
            
            self._log_check(category, index_count, count)
        
        # ─────────────────────────────────────────────────────────────────────
        # 3. Vérifier les compteurs de projets
        # ─────────────────────────────────────────────────────────────────────
        self.log("\n📁 Checking project counters...")
        
        project_count, project_ids = self.count_projects()
        
        index_project_count = self._get_nested(self.index_data, "projects.count", 0)
        self.report.deltas.append(SyncDelta(
            location="projects.count",
            expected=project_count,
            actual=index_project_count,
            source="File count in memory/projects/",
            severity="medium"
        ))
        
        self._log_check("projects", index_project_count, project_count)
        
        # ─────────────────────────────────────────────────────────────────────
        # 4. Vérifier statistics.totals
        # ─────────────────────────────────────────────────────────────────────
        self.log("\n📈 Checking statistics totals...")
        
        # Total episodes
        total_episodes = sum(self.count_episodic_files(cat)[0] for cat in ["analyses", "decisions", "incidents", "executions"])
        index_total_episodes = self._get_nested(self.index_data, "statistics.totals.episodes", 0)
        self.report.deltas.append(SyncDelta(
            location="statistics.totals.episodes",
            expected=total_episodes,
            actual=index_total_episodes,
            source="Sum of all episodic files",
            severity="low"
        ))
        
        # Total patterns
        patterns_count = self.count_yaml_items(patterns_file, "patterns") if patterns_file.exists() else 0
        index_total_patterns = self._get_nested(self.index_data, "statistics.totals.patterns", 0)
        self.report.deltas.append(SyncDelta(
            location="statistics.totals.patterns",
            expected=patterns_count,
            actual=index_total_patterns,
            source="Count from defect_patterns.yaml",
            severity="low"
        ))
        
        # Total heuristics
        heuristics_count = self.count_heuristics(heuristics_file) if heuristics_file.exists() else 0
        index_total_heuristics = self._get_nested(self.index_data, "statistics.totals.heuristics", 0)
        self.report.deltas.append(SyncDelta(
            location="statistics.totals.heuristics",
            expected=heuristics_count,
            actual=index_total_heuristics,
            source="Count from testing_heuristics.yaml",
            severity="low"
        ))
        
        # Total projects
        index_total_projects = self._get_nested(self.index_data, "statistics.totals.projects", 0)
        self.report.deltas.append(SyncDelta(
            location="statistics.totals.projects",
            expected=project_count,
            actual=index_total_projects,
            source="Count from projects/",
            severity="low"
        ))
        
        # ─────────────────────────────────────────────────────────────────────
        # 5. Vérifier les sequences
        # ─────────────────────────────────────────────────────────────────────
        self.log("\n🔢 Checking sequence counters...")
        
        sequence_checks = [
            ("sequences.pattern", patterns_count, "defect_patterns count"),
            ("sequences.heuristic", heuristics_count, "testing_heuristics count"),
            ("sequences.analysis", self.count_episodic_files("analyses")[0], "analyses count"),
            ("sequences.decision", self.count_episodic_files("decisions")[0], "decisions count"),
            ("sequences.incident", self.count_episodic_files("incidents")[0], "incidents count"),
            ("sequences.execution", self.count_episodic_files("executions")[0], "executions count"),
        ]
        
        for location, expected, source in sequence_checks:
            actual = self._get_nested(self.index_data, location, 0)
            self.report.deltas.append(SyncDelta(
                location=location,
                expected=expected,
                actual=actual,
                source=source,
                severity="low"
            ))
        
        return self.report
    
    def _get_nested(self, data: Dict, path: str, default: Any = None) -> Any:
        """Récupère une valeur imbriquée dans un dict avec un chemin 'a.b.c'."""
        keys = path.split(".")
        current = data
        for key in keys:
            if isinstance(current, dict) and key in current:
                current = current[key]
            else:
                return default
        return current
    
    def _set_nested(self, data: Dict, path: str, value: Any):
        """Définit une valeur imbriquée dans un dict avec un chemin 'a.b.c'."""
        keys = path.split(".")
        current = data
        for key in keys[:-1]:
            if key not in current:
                current[key] = {}
            current = current[key]
        current[keys[-1]] = value
    
    def _log_check(self, name: str, index_val: Any, actual_val: Any):
        """Log une vérification avec symbole de statut."""
        if index_val == actual_val:
            self.log(f"  ✓ {name}: {actual_val}")
        else:
            self.log(f"  ✗ {name}: index={index_val}, actual={actual_val}")
    
    def apply_fixes(self) -> bool:
        """
        Applique les corrections pour tous les écarts détectés.
        
        Returns:
            True si toutes les corrections ont été appliquées avec succès.
        """
        if not self.report.has_mismatches:
            self.log("\n✅ No fixes needed - index is synchronized.")
            return True
        
        self.log(f"\n🔧 Applying {self.report.mismatch_count} fix(es)...")
        
        # 1. Corriger _index.yaml
        index_modified = False
        for delta in self.report.deltas:
            if delta.is_mismatch and delta.location.startswith(("semantic.", "episodic.", "projects.", "statistics.", "sequences.")):
                self._set_nested(self.index_data, delta.location, delta.expected)
                index_modified = True
                self.log(f"  ✓ Fixed {delta.location}: {delta.actual} → {delta.expected}")
        
        if index_modified:
            # Mettre à jour last_updated
            self.index_data["last_updated"] = datetime.now().strftime("%Y-%m-%d")
            self.save_index_preserving_format()
        
        # 2. Corriger les métadonnées internes des fichiers sémantiques
        for delta in self.report.deltas:
            if delta.is_mismatch:
                if "defect_patterns.yaml → total_patterns" in delta.location:
                    self._fix_internal_metadata(
                        self.memory_dir / "semantic" / "defect_patterns.yaml",
                        "total_patterns",
                        delta.expected
                    )
                elif "testing_heuristics.yaml → total_heuristics" in delta.location:
                    self._fix_internal_metadata(
                        self.memory_dir / "semantic" / "testing_heuristics.yaml",
                        "total_heuristics",
                        delta.expected
                    )
        
        self.log("\n✅ All fixes applied successfully.")
        return True
    
    def _fix_internal_metadata(self, filepath: Path, key: str, value: Any):
        """Corrige une métadonnée interne dans un fichier YAML."""
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Utiliser une regex pour remplacer la valeur
            pattern = rf"({key}:\s*)\d+"
            replacement = rf"\g<1>{value}"
            new_content = re.sub(pattern, replacement, content)
            
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(new_content)
            
            self.log(f"  ✓ Fixed {filepath.name} → {key}: {value}")
        except Exception as e:
            self.report.errors.append(f"Error fixing {filepath}: {e}")
    
    def save_index_preserving_format(self):
        """
        Sauvegarde l'index en préservant autant que possible le formatage.
        Utilise une approche de remplacement ciblé pour les valeurs numériques.
        """
        try:
            with open(self.index_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Pour chaque delta, remplacer la valeur dans le fichier
            for delta in self.report.deltas:
                if delta.is_mismatch and not "→" in delta.location:
                    # Construire le pattern de remplacement
                    # Le format est "key: value" où key est le dernier élément du path
                    key = delta.location.split(".")[-1]
                    
                    # Pattern pour remplacer la valeur
                    if delta.actual is None:
                        pattern = rf"({key}:\s*)null"
                    elif isinstance(delta.actual, int):
                        pattern = rf"({key}:\s*){delta.actual}(?!\d)"
                    elif isinstance(delta.actual, str):
                        pattern = rf'({key}:\s*)["\']?{re.escape(delta.actual)}["\']?'
                    else:
                        continue
                    
                    if delta.expected is None:
                        replacement = rf"\g<1>null"
                    elif isinstance(delta.expected, str):
                        replacement = rf'\g<1>"{delta.expected}"'
                    else:
                        replacement = rf"\g<1>{delta.expected}"
                    
                    content = re.sub(pattern, replacement, content, count=1)
            
            # Mettre à jour last_updated
            today = datetime.now().strftime("%Y-%m-%d")
            content = re.sub(r'(last_updated:\s*)["\']?\d{4}-\d{2}-\d{2}["\']?', rf'\g<1>"{today}"', content)
            
            with open(self.index_path, 'w', encoding='utf-8') as f:
                f.write(content)
                
        except Exception as e:
            self.report.errors.append(f"Error saving index: {e}")
    
    def print_report(self):
        """Affiche le rapport de synchronisation."""
        self.log("\n" + "═" * 60)
        self.log("📊 SYNCHRONIZATION REPORT")
        self.log("═" * 60)
        
        mismatches = [d for d in self.report.deltas if d.is_mismatch]
        
        self.log(f"  Files checked: {self.report.files_checked}")
        self.log(f"  Counters verified: {len(self.report.deltas)}")
        self.log(f"  Mismatches found: {len(mismatches)}")
        
        if mismatches:
            self.log(f"\n{'─' * 60}")
            self.log("❌ DESYNCHRONIZED COUNTERS:\n")
            
            for delta in mismatches:
                self.log(f"  📍 {delta.location}")
                self.log(f"     Index value:  {delta.actual}")
                self.log(f"     Actual value: {delta.expected}")
                self.log(f"     Source: {delta.source}")
                self.log("")
            
            self.log(f"{'─' * 60}")
            self.log("💡 Run with --fix to apply corrections.", force=True)
        else:
            self.log("\n✅ Index is fully synchronized!", force=True)
        
        if self.report.errors:
            self.log(f"\n⚠ Errors encountered:")
            for error in self.report.errors:
                self.log(f"  - {error}")
        
        self.log("═" * 60 + "\n")


def find_project_root() -> Path:
    """Trouve la racine du projet (le dossier contenant argus.persona.yaml et memory/)."""
    current = Path(__file__).resolve().parent
    
    while current != current.parent:
        if (current / 'argus.persona.yaml').exists() and (current / 'memory').is_dir():
            return current
        current = current.parent
    
    # Fallback : parent du dossier tests/
    return Path(__file__).resolve().parent.parent


def main():
    parser = argparse.ArgumentParser(
        description='Synchronize Argus memory index counters',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__
    )
    
    parser.add_argument(
        '--fix', '-f',
        action='store_true',
        help='Apply fixes to synchronize counters'
    )
    
    parser.add_argument(
        '--quiet', '-q',
        action='store_true',
        help='Quiet mode - only return exit code'
    )
    
    parser.add_argument(
        '--json', '-j',
        action='store_true',
        help='Output report as JSON'
    )
    
    args = parser.parse_args()
    
    try:
        project_root = find_project_root()
        sync = MemorySynchronizer(str(project_root), verbose=not args.quiet and not args.json)
        
        # Analyser l'état actuel
        report = sync.analyze()
        
        # Sortie JSON si demandé
        if args.json:
            print(json.dumps(report.to_dict(), indent=2))
            return 1 if report.has_mismatches else 0
        
        # Afficher le rapport
        sync.print_report()
        
        # Appliquer les corrections si demandé
        if args.fix and report.has_mismatches:
            sync.apply_fixes()
            # Re-vérifier
            sync.report = SyncReport()
            sync.analyze()
            sync.print_report()
        
        return 1 if report.has_mismatches and not args.fix else 0
        
    except Exception as e:
        if not args.quiet:
            print(f"\n❌ Script error: {e}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    sys.exit(main())
