#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Validateur de références croisées inter-fichiers
 Détecte les références orphelines dans le système de mémoire
═══════════════════════════════════════════════════════════════════════════════

Usage:
  python validate_references.py              # Mode détection (affiche les orphelins)
  python validate_references.py --graph      # Génère un graphe de dépendances
  python validate_references.py --quiet      # Mode silencieux (exit code uniquement)
  python validate_references.py --json       # Sortie JSON (pour intégration CI)

Exit codes:
  0 = Toutes les références sont valides
  1 = Références orphelines détectées
  2 = Erreur d'exécution du script

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

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


# ═══════════════════════════════════════════════════════════════════════════════
#  PATTERNS D'ID RECONNUS
#  Chaque entité Argus a un format d'ID unique : {PREFIX}-{CATEGORY/DATE}-{SEQ}
#  Ces regex permettent d'extraire les références par scan texte des fichiers YAML
# ═══════════════════════════════════════════════════════════════════════════════

ID_PATTERNS = {
    # Patterns de défauts : PAT-{CATEGORY}-{SEQ}
    "pattern": re.compile(r'\bPAT-[A-Z]+-\d{3}\b'),
    
    # Heuristics : HEU-{CATEGORY}-{SEQ}
    "heuristic": re.compile(r'\bHEU-[A-Z]+-\d{3}\b'),
    
    # Analyses : ANL-{YYYYMMDD}-{SEQ}
    "analysis": re.compile(r'\bANL-\d{8}-\d{3}\b'),
    
    # Décisions : DEC-{YYYYMMDD}-{SEQ}
    "decision": re.compile(r'\bDEC-\d{8}-\d{3}\b'),
    
    # Incidents : INC-{YYYYMMDD}-{SEQ}
    "incident": re.compile(r'\bINC-\d{8}-\d{3}\b'),
    
    # Exécutions : EXE-{YYYYMMDD}-{SEQ}
    "execution": re.compile(r'\bEXE-\d{8}-\d{3}\b'),
}

# Références de projet : recherche le champ YAML "project: <id>"
# Ignorer les placeholders (ex: "project_identifier") via IGNORE_VALUES
PROJECT_REF_PATTERN = re.compile(r'^\s*project:\s*["\']?([a-z0-9_-]+)["\']?\s*$', re.IGNORECASE | re.MULTILINE)

# Références de fichier : recherche les champs link/path/file pointant vers des .yaml
FILE_REF_PATTERN = re.compile(r'(?:link|path|file):\s*["\']?([^"\'\n]+\.yaml)["\']?', re.IGNORECASE)

# Valeurs à ignorer (placeholders, exemples)
IGNORE_VALUES = {
    "project_identifier", "project-slug", "example", "null", "none",
    "your-project", "my-project", "test-project"
}


# ═══════════════════════════════════════════════════════════════════════════════
#  DATA CLASSES
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class DefinedId:
    """Un ID défini quelque part dans le système."""
    id: str
    id_type: str
    source_file: str
    context: str = ""  # Contexte supplémentaire (nom, description, etc.)


@dataclass
class Reference:
    """Une référence vers un ID."""
    target_id: str
    id_type: str
    source_file: str
    line_number: int
    context: str = ""  # Ligne ou contexte où la référence apparaît


@dataclass
class OrphanReference:
    """Une référence vers un ID qui n'existe pas."""
    reference: Reference
    suggestion: Optional[str] = None  # ID similaire qui pourrait être le bon
    
    def to_dict(self) -> dict:
        return {
            "target_id": self.reference.target_id,
            "id_type": self.reference.id_type,
            "source_file": self.reference.source_file,
            "line_number": self.reference.line_number,
            "context": self.reference.context,
            "suggestion": self.suggestion
        }


@dataclass
class ValidationReport:
    """Rapport complet de validation des références."""
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    defined_ids: Dict[str, List[DefinedId]] = field(default_factory=dict)
    references: List[Reference] = field(default_factory=list)
    orphans: List[OrphanReference] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)
    files_checked: int = 0
    
    @property
    def has_orphans(self) -> bool:
        return len(self.orphans) > 0
    
    @property
    def total_defined(self) -> int:
        return sum(len(ids) for ids in self.defined_ids.values())
    
    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp,
            "summary": {
                "files_checked": self.files_checked,
                "total_ids_defined": self.total_defined,
                "total_references": len(self.references),
                "orphan_references": len(self.orphans),
                "errors": len(self.errors),
                "status": "ORPHANS_FOUND" if self.has_orphans else "ALL_VALID"
            },
            "defined_ids_by_type": {
                id_type: [{"id": d.id, "source": d.source_file} for d in ids]
                for id_type, ids in self.defined_ids.items()
            },
            "orphan_references": [o.to_dict() for o in self.orphans],
            "errors": self.errors
        }


# ═══════════════════════════════════════════════════════════════════════════════
#  VALIDATEUR DE RÉFÉRENCES
# ═══════════════════════════════════════════════════════════════════════════════

class ReferenceValidator:
    """Valide les références croisées dans le système de mémoire Argus."""
    
    def __init__(self, base_dir: str, verbose: bool = True):
        self.base_dir = Path(base_dir)
        self.memory_dir = self.base_dir / "memory"
        self.verbose = verbose
        self.report = ValidationReport()
        
        # Initialiser les catégories d'ID
        for id_type in ID_PATTERNS.keys():
            self.report.defined_ids[id_type] = []
        self.report.defined_ids["project"] = []
        self.report.defined_ids["file"] = []
    
    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 load_yaml_content(self, filepath: Path) -> Optional[str]:
        """Charge le contenu brut d'un fichier YAML."""
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                return f.read()
        except Exception as e:
            self.report.errors.append(f"Error reading {filepath}: {e}")
            return None
    
    # ─────────────────────────────────────────────────────────────────────────
    # PHASE 1: Extraction des IDs définis
    # ─────────────────────────────────────────────────────────────────────────
    
    def extract_defined_ids(self):
        """Extrait tous les IDs définis dans le système."""
        self.log("\n📋 Phase 1: Extracting defined IDs...")
        
        # 1. Patterns de défauts
        self._extract_from_semantic_file(
            "defect_patterns.yaml",
            "patterns",
            "pattern",
            "id"
        )
        
        # 2. Heuristics
        self._extract_heuristics()
        
        # 3. Projets
        self._extract_projects()
        
        # 4. Fichiers épisodiques
        self._extract_episodic_ids()
        
        # 5. Fichiers existants (pour validation des références de fichier)
        self._extract_existing_files()
    
    def _extract_from_semantic_file(self, filename: str, list_key: str, 
                                     id_type: str, id_field: str):
        """Extrait les IDs d'un fichier sémantique."""
        filepath = self.memory_dir / "semantic" / filename
        if not filepath.exists():
            return
        
        data = self.load_yaml(filepath)
        if not data or list_key not in data:
            return
        
        self.report.files_checked += 1
        items = data[list_key]
        if not isinstance(items, list):
            return
        
        for item in items:
            if isinstance(item, dict) and id_field in item:
                self.report.defined_ids[id_type].append(DefinedId(
                    id=item[id_field],
                    id_type=id_type,
                    source_file=str(filepath.relative_to(self.base_dir)),
                    context=item.get("name", "")
                ))
        
        self.log(f"  ✓ {filename}: {len(items)} {id_type}(s)")
    
    def _extract_heuristics(self):
        """Extrait les IDs des heuristics (dynamique — parcourt toutes les sections list)."""
        filepath = self.memory_dir / "semantic" / "testing_heuristics.yaml"
        if not filepath.exists():
            return
        
        data = self.load_yaml(filepath)
        if not data:
            return
        
        self.report.files_checked += 1
        
        # Parcourir dynamiquement toutes les sections qui sont des listes
        HEURISTIC_META_KEYS = {'version', 'last_updated', 'metadata'}
        
        count = 0
        for key, value in data.items():
            if key in HEURISTIC_META_KEYS:
                continue
            if isinstance(value, list):
                for item in value:
                    if isinstance(item, dict) and "id" in item:
                        self.report.defined_ids["heuristic"].append(DefinedId(
                            id=item["id"],
                            id_type="heuristic",
                            source_file="memory/semantic/testing_heuristics.yaml",
                            context=item.get("name", "")
                        ))
                        count += 1
        
        self.log(f"  ✓ testing_heuristics.yaml: {count} heuristic(s)")
    
    def _extract_projects(self):
        """Extrait les IDs de projets."""
        projects_dir = self.memory_dir / "projects"
        if not projects_dir.exists():
            return
        
        count = 0
        for project_file in projects_dir.glob("*.yaml"):
            if project_file.name.startswith("_template"):
                continue
            
            self.report.files_checked += 1
            
            # L'ID du projet est le nom du fichier sans extension
            project_id = project_file.stem
            
            # Essayer d'extraire le nom complet
            data = self.load_yaml(project_file)
            context = ""
            if data and "identity" in data:
                context = data["identity"].get("name", "")
            
            self.report.defined_ids["project"].append(DefinedId(
                id=project_id,
                id_type="project",
                source_file=str(project_file.relative_to(self.base_dir)),
                context=context
            ))
            count += 1
        
        self.log(f"  ✓ projects/: {count} project(s)")
    
    def _extract_episodic_ids(self):
        """Extrait les IDs des fichiers épisodiques."""
        episodic_dir = self.memory_dir / "episodic"
        if not episodic_dir.exists():
            return
        
        categories = {
            "analyses": "analysis",
            "decisions": "decision",
            "incidents": "incident",
            "executions": "execution"
        }
        
        for folder, id_type in categories.items():
            folder_path = episodic_dir / folder
            if not folder_path.exists():
                continue
            
            count = 0
            for episode_file in folder_path.glob("*.yaml"):
                if episode_file.name.startswith("_template"):
                    continue
                
                self.report.files_checked += 1
                
                data = self.load_yaml(episode_file)
                if not data:
                    continue
                
                # L'ID peut être à la racine (incidents, analyses récentes)
                # ou dans metadata.id (anciennes analyses)
                episode_id = None
                context = ""
                
                if "id" in data:
                    episode_id = data["id"]
                    context = data.get("title", data.get("summary", ""))
                elif "metadata" in data and isinstance(data["metadata"], dict):
                    episode_id = data["metadata"].get("id")
                    context = data["metadata"].get("title", data["metadata"].get("type", ""))
                
                if episode_id:
                    self.report.defined_ids[id_type].append(DefinedId(
                        id=episode_id,
                        id_type=id_type,
                        source_file=self._normalize_path(episode_file.relative_to(self.base_dir)),
                        context=context
                    ))
                    count += 1
            
            if count > 0:
                self.log(f"  ✓ episodic/{folder}/: {count} {id_type}(s)")
    
    def _normalize_path(self, path) -> str:
        """Normalise un chemin avec des forward slashes (cross-platform)."""
        return str(path).replace('\\', '/')
    
    def _extract_existing_files(self):
        """Extrait la liste des fichiers YAML existants (pour validation des refs de fichier)."""
        for yaml_file in self.memory_dir.rglob("*.yaml"):
            if yaml_file.name.startswith("_template"):
                continue
            
            # Stocker le chemin sous 3 formes différentes pour éviter les faux
            # positifs d'orphelins (les fichiers YAML réfèrent les chemins de façon variable)
            rel_from_memory = self._normalize_path(yaml_file.relative_to(self.memory_dir))
            rel_from_base = self._normalize_path(yaml_file.relative_to(self.base_dir))
            source = self._normalize_path(yaml_file.relative_to(self.base_dir))
            
            # Format: semantic/defect_patterns.yaml
            self.report.defined_ids["file"].append(DefinedId(
                id=rel_from_memory,
                id_type="file",
                source_file=source,
                context=""
            ))
            
            # Format: memory/semantic/defect_patterns.yaml
            self.report.defined_ids["file"].append(DefinedId(
                id=rel_from_base,
                id_type="file",
                source_file=source,
                context=""
            ))
            
            # Aussi stocker juste le nom du fichier (format court)
            self.report.defined_ids["file"].append(DefinedId(
                id=yaml_file.name,
                id_type="file",
                source_file=source,
                context=""
            ))
        
        # Ajouter les fichiers YAML à la racine
        for yaml_file in self.base_dir.glob("*.yaml"):
            self.report.defined_ids["file"].append(DefinedId(
                id=yaml_file.name,
                id_type="file",
                source_file=yaml_file.name,
                context=""
            ))
    
    # ─────────────────────────────────────────────────────────────────────────
    # PHASE 2: Extraction des références
    # ─────────────────────────────────────────────────────────────────────────
    
    def extract_references(self):
        """Extrait toutes les références vers des IDs."""
        self.log("\n🔗 Phase 2: Extracting references...")
        
        # Scanner tous les fichiers YAML
        yaml_files = list(self.memory_dir.rglob("*.yaml"))
        yaml_files.append(self.base_dir / "argus.persona.yaml")
        
        for yaml_file in yaml_files:
            if not yaml_file.exists():
                continue
            
            content = self.load_yaml_content(yaml_file)
            if not content:
                continue
            
            rel_path = str(yaml_file.relative_to(self.base_dir))
            
            # Extraire les références par type d'ID (scan texte)
            for id_type, pattern in ID_PATTERNS.items():
                self._extract_refs_from_content(content, pattern, id_type, rel_path)
            
            # Extraire les références de projet
            self._extract_project_refs(content, rel_path)
            
            # Extraire les références de fichier
            self._extract_file_refs(content, rel_path)
            
            # Extraire les références depuis les arrays YAML (champs enrichis R002)
            data = self.load_yaml(yaml_file)
            if data:
                self._extract_refs_from_yaml_arrays(data, rel_path)
        
        self.log(f"  Found {len(self.report.references)} reference(s)")
    
    def _extract_refs_from_content(self, content: str, pattern: re.Pattern, 
                                    id_type: str, source_file: str):
        """Extrait les références correspondant à un pattern."""
        lines = content.split('\n')
        in_next_ids_section = False
        
        for line_num, line in enumerate(lines, 1):
            # Ignorer les lignes de commentaire
            stripped = line.strip()
            if stripped.startswith('#'):
                continue
            
            # La section next_ids contient des IDs futurs (réservés mais pas encore créés)
            # — ils ne doivent pas être comptabilisés comme des références orphelines
            if stripped.startswith('next_ids:'):
                in_next_ids_section = True
                continue
            
            # Sortir de next_ids si on trouve une clé de niveau supérieur
            if in_next_ids_section:
                if line and not line[0].isspace() and not line.startswith('#'):
                    in_next_ids_section = False
                else:
                    continue  # Ignorer tout ce qui est dans next_ids
            
            # Les lignes "id: PAT-XXX" définissent l'ID, elles ne le référencent pas
            if re.match(r'^\s*id:\s*', line):
                continue
            
            # Ignorer les exemples de documentation
            if re.match(r'^\s*example:', line):
                continue
            
            for match in pattern.finditer(line):
                ref_id = match.group()
                self.report.references.append(Reference(
                    target_id=ref_id,
                    id_type=id_type,
                    source_file=source_file,
                    line_number=line_num,
                    context=stripped[:100]  # Premiers 100 chars comme contexte
                ))
    
    def _extract_refs_from_yaml_arrays(self, data: Any, source_file: str,
                                        parent_key: str = ""):
        """Extrait les références depuis les champs array des fichiers YAML.
        
        Cible les champs enrichis des templates : related_patterns,
        related_heuristics, matched_patterns, missed_heuristics.existing,
        patterns_considered, heuristics_used, covered, not_covered, etc.
        """
        REFERENCE_ARRAY_KEYS = {
            'related_patterns', 'related_heuristics', 'matched_patterns',
            'patterns_considered', 'heuristics_used', 'patterns_extracted',
            'contributed_to_heuristics', 'related_analyses',
            'existing',  # missed_heuristics.existing
            'covered', 'not_covered',  # api_coverage.heuristics_coverage / patterns_tested
            'source_episodes',
        }
        
        if isinstance(data, dict):
            for key, value in data.items():
                if key in REFERENCE_ARRAY_KEYS and isinstance(value, list):
                    for item in value:
                        if isinstance(item, str):
                            # Match against known ID patterns
                            for id_type, pattern in ID_PATTERNS.items():
                                if pattern.match(item):
                                    self.report.references.append(Reference(
                                        target_id=item,
                                        id_type=id_type,
                                        source_file=source_file,
                                        line_number=0,
                                        context=f"array field: {parent_key}.{key}" if parent_key else f"array field: {key}"
                                    ))
                                    break
                        elif isinstance(item, dict):
                            # Handle dicts like {type: "analysis", id: "ANL-..."}
                            if 'id' in item and isinstance(item['id'], str):
                                ref_id = item['id']
                                for id_type, pattern in ID_PATTERNS.items():
                                    if pattern.match(ref_id):
                                        self.report.references.append(Reference(
                                            target_id=ref_id,
                                            id_type=id_type,
                                            source_file=source_file,
                                            line_number=0,
                                            context=f"array field: {parent_key}.{key}" if parent_key else f"array field: {key}"
                                        ))
                                        break
                else:
                    self._extract_refs_from_yaml_arrays(
                        value, source_file,
                        f"{parent_key}.{key}" if parent_key else key
                    )
        elif isinstance(data, list):
            for item in data:
                self._extract_refs_from_yaml_arrays(item, source_file, parent_key)
    
    def _extract_project_refs(self, content: str, source_file: str):
        """Extrait les références de projet."""
        lines = content.split('\n')
        
        for line_num, line in enumerate(lines, 1):
            stripped = line.strip()
            if stripped.startswith('#'):
                continue
            
            # Ignorer les définitions de project_id
            if 'project_id:' in line:
                continue
            
            for match in PROJECT_REF_PATTERN.finditer(line):
                project_id = match.group(1)
                # Ignorer les valeurs spéciales et placeholders
                if project_id.lower() in IGNORE_VALUES:
                    continue
                
                self.report.references.append(Reference(
                    target_id=project_id,
                    id_type="project",
                    source_file=source_file,
                    line_number=line_num,
                    context=stripped[:100]
                ))
    
    def _extract_file_refs(self, content: str, source_file: str):
        """Extrait les références de fichier."""
        lines = content.split('\n')
        
        for line_num, line in enumerate(lines, 1):
            stripped = line.strip()
            if stripped.startswith('#'):
                continue
            
            for match in FILE_REF_PATTERN.finditer(line):
                file_ref = match.group(1)
                # Ignorer les templates et placeholders
                if '_template' in file_ref or '{' in file_ref:
                    continue
                
                self.report.references.append(Reference(
                    target_id=file_ref,
                    id_type="file",
                    source_file=source_file,
                    line_number=line_num,
                    context=stripped[:100]
                ))
    
    # ─────────────────────────────────────────────────────────────────────────
    # PHASE 3: Validation
    # ─────────────────────────────────────────────────────────────────────────
    
    def validate(self):
        """Valide que toutes les références pointent vers des IDs existants."""
        self.log("\n✓ Phase 3: Validating references...")
        
        # Construire un set de tous les IDs définis
        defined_ids_set: Set[str] = set()
        for id_list in self.report.defined_ids.values():
            for defined in id_list:
                defined_ids_set.add(defined.id)
        
        # Vérifier chaque référence
        for ref in self.report.references:
            if ref.target_id not in defined_ids_set:
                # Trouver une suggestion si possible
                suggestion = self._find_similar_id(ref.target_id, ref.id_type)
                
                self.report.orphans.append(OrphanReference(
                    reference=ref,
                    suggestion=suggestion
                ))
        
        self.log(f"  Validated {len(self.report.references)} reference(s)")
        self.log(f"  Found {len(self.report.orphans)} orphan(s)")
    
    def _find_similar_id(self, target_id: str, id_type: str) -> Optional[str]:
        """Trouve un ID similaire qui pourrait être la bonne cible."""
        if id_type not in self.report.defined_ids:
            return None
        
        # Simple matching: chercher un ID qui contient une partie significative
        target_parts = target_id.split('-')
        
        for defined in self.report.defined_ids[id_type]:
            defined_parts = defined.id.split('-')
            
            # Si les préfixes correspondent et les dernières parties sont proches
            if len(target_parts) >= 2 and len(defined_parts) >= 2:
                if target_parts[0] == defined_parts[0]:
                    # Vérifier si c'est une variation proche
                    try:
                        if abs(int(target_parts[-1]) - int(defined_parts[-1])) <= 5:
                            return defined.id
                    except ValueError:
                        pass
        
        return None
    
    # ─────────────────────────────────────────────────────────────────────────
    # GÉNÉRATION DU GRAPHE
    # ─────────────────────────────────────────────────────────────────────────
    
    def generate_dependency_graph(self) -> str:
        """Génère un graphe Mermaid des dépendances."""
        lines = ["```mermaid", "graph LR"]
        
        # Grouper par fichier source
        refs_by_source: Dict[str, List[Reference]] = {}
        for ref in self.report.references:
            if ref.source_file not in refs_by_source:
                refs_by_source[ref.source_file] = []
            refs_by_source[ref.source_file].append(ref)
        
        # Générer les nœuds et arêtes
        node_id = 0
        file_nodes: Dict[str, str] = {}
        
        for source_file, refs in refs_by_source.items():
            # Créer un nœud pour le fichier source si nécessaire
            if source_file not in file_nodes:
                file_nodes[source_file] = f"F{node_id}"
                short_name = Path(source_file).name
                lines.append(f"    F{node_id}[{short_name}]")
                node_id += 1
            
            # Créer des arêtes vers les cibles
            for ref in refs:
                target_label = f"{ref.id_type}:{ref.target_id}"
                target_node = f"T{node_id}"
                
                # Style différent pour les orphelins
                is_orphan = any(o.reference.target_id == ref.target_id for o in self.report.orphans)
                if is_orphan:
                    lines.append(f"    {target_node}[/{target_label}/]")
                    lines.append(f"    {file_nodes[source_file]} -.->|orphan| {target_node}")
                else:
                    lines.append(f"    {target_node}({target_label})")
                    lines.append(f"    {file_nodes[source_file]} --> {target_node}")
                
                node_id += 1
        
        lines.append("```")
        return "\n".join(lines)
    
    # ─────────────────────────────────────────────────────────────────────────
    # AFFICHAGE
    # ─────────────────────────────────────────────────────────────────────────
    
    def print_report(self):
        """Affiche le rapport de validation."""
        self.log("\n" + "═" * 60)
        self.log("📊 REFERENCE VALIDATION REPORT")
        self.log("═" * 60)
        
        # Résumé des IDs définis
        self.log("\n📋 Defined IDs by type:")
        for id_type, ids in self.report.defined_ids.items():
            if ids and id_type != "file":  # Ignorer la liste de fichiers
                self.log(f"  {id_type}: {len(ids)}")
        
        self.log(f"\n🔗 Total references found: {len(self.report.references)}")
        self.log(f"👻 Orphan references: {len(self.report.orphans)}")
        
        if self.report.orphans:
            self.log(f"\n{'─' * 60}")
            self.log("❌ ORPHAN REFERENCES:\n")
            
            for orphan in self.report.orphans:
                ref = orphan.reference
                self.log(f"  📍 {ref.target_id} ({ref.id_type})")
                self.log(f"     Source: {ref.source_file}:{ref.line_number}")
                self.log(f"     Context: {ref.context[:60]}...")
                if orphan.suggestion:
                    self.log(f"     💡 Did you mean: {orphan.suggestion}?")
                self.log("")
            
            self.log(f"{'─' * 60}")
            self.log("💡 Fix the orphan references or remove them.", force=True)
        else:
            self.log("\n✅ All references are valid!", force=True)
        
        if self.report.errors:
            self.log(f"\n⚠ Errors encountered ({len(self.report.errors)}):")
            for error in self.report.errors:
                self.log(f"  - {error}")
        
        self.log("═" * 60 + "\n")

        # Overall status includes both orphans and errors
        if not self.report.orphans and not self.report.errors:
            self.log("✅ All validations passed!", force=True)
        elif self.report.orphans:
            self.log(f"❌ {len(self.report.orphans)} orphan(s) found", force=True)
        elif self.report.errors:
            self.log(f"⚠ {len(self.report.errors)} consistency error(s) found", force=True)
    
    # ─────────────────────────────────────────────────────────────────────────
    # POINT D'ENTRÉE
    # ─────────────────────────────────────────────────────────────────────────
    
    # ─────────────────────────────────────────────────────────────────────────
    # PHASE 4: Validation de cohérence de _index.yaml
    # ─────────────────────────────────────────────────────────────────────────
    
    def validate_index_consistency(self):
        """Vérifie la cohérence interne de _index.yaml."""
        self.log("\n🔢 Phase 4: Validating _index.yaml consistency...")
        
        index_path = self.memory_dir / "_index.yaml"
        if not index_path.exists():
            self.report.errors.append("_index.yaml not found")
            return
        
        index = self.load_yaml(index_path)
        if not index:
            return
        
        checks_passed = 0
        checks_failed = 0
        
        # --- Episodic counts ---
        episodic = index.get('episodic', {})
        categories = {
            'analyses': 'analysis',
            'decisions': 'decision',
            'incidents': 'incident',
            'executions': 'execution'
        }
        
        for folder, id_type in categories.items():
            expected_count = episodic.get(folder, {}).get('count', 0)
            folder_path = self.memory_dir / "episodic" / folder
            actual_files = [f for f in folder_path.glob("*.yaml") if not f.name.startswith("_")] if folder_path.exists() else []
            actual_count = len(actual_files)
            
            if expected_count == actual_count:
                self.log(f"  ✓ episodic.{folder}.count: {actual_count}")
                checks_passed += 1
            else:
                msg = f"_index.yaml episodic.{folder}.count is {expected_count} but {actual_count} file(s) found"
                self.report.errors.append(msg)
                self.log(f"  ✗ {msg}")
                checks_failed += 1
            
            # Validate 'latest' ID exists
            latest_id = episodic.get(folder, {}).get('latest')
            if latest_id:
                defined_ids = {d.id for d in self.report.defined_ids.get(id_type, [])}
                if latest_id in defined_ids:
                    self.log(f"  ✓ episodic.{folder}.latest: {latest_id} exists")
                    checks_passed += 1
                else:
                    msg = f"_index.yaml episodic.{folder}.latest '{latest_id}' not found in files"
                    self.report.errors.append(msg)
                    self.log(f"  ✗ {msg}")
                    checks_failed += 1
        
        # --- Semantic counts ---
        semantic = index.get('semantic', {})
        
        # Patterns count
        expected_patterns = semantic.get('defect_patterns', {}).get('count', 0)
        actual_patterns = len(self.report.defined_ids.get('pattern', []))
        if expected_patterns == actual_patterns:
            self.log(f"  ✓ semantic.defect_patterns.count: {actual_patterns}")
            checks_passed += 1
        else:
            msg = f"_index.yaml semantic.defect_patterns.count is {expected_patterns} but {actual_patterns} found"
            self.report.errors.append(msg)
            self.log(f"  ✗ {msg}")
            checks_failed += 1
        
        # Heuristics count
        expected_heuristics = semantic.get('testing_heuristics', {}).get('count', 0)
        actual_heuristics = len(self.report.defined_ids.get('heuristic', []))
        if expected_heuristics == actual_heuristics:
            self.log(f"  ✓ semantic.testing_heuristics.count: {actual_heuristics}")
            checks_passed += 1
        else:
            msg = f"_index.yaml semantic.testing_heuristics.count is {expected_heuristics} but {actual_heuristics} found"
            self.report.errors.append(msg)
            self.log(f"  ✗ {msg}")
            checks_failed += 1
        
        # --- Projects count ---
        expected_projects = index.get('projects', {}).get('count', 0)
        actual_projects = len(self.report.defined_ids.get('project', []))
        if expected_projects == actual_projects:
            self.log(f"  ✓ projects.count: {actual_projects}")
            checks_passed += 1
        else:
            msg = f"_index.yaml projects.count is {expected_projects} but {actual_projects} found"
            self.report.errors.append(msg)
            self.log(f"  ✗ {msg}")
            checks_failed += 1
        
        # --- Sequence counters ---
        sequences = index.get('sequences', {})
        seq_checks = [
            ('analysis', 'analysis'),
            ('pattern', 'pattern'),
            ('heuristic', 'heuristic'),
        ]
        for seq_key, id_type in seq_checks:
            seq_val = sequences.get(seq_key, 0)
            actual = len(self.report.defined_ids.get(id_type, []))
            if seq_val >= actual:
                self.log(f"  ✓ sequences.{seq_key}: {seq_val} (>= {actual} defined)")
                checks_passed += 1
            else:
                msg = f"_index.yaml sequences.{seq_key} is {seq_val} but {actual} IDs exist (counter too low)"
                self.report.errors.append(msg)
                self.log(f"  ✗ {msg}")
                checks_failed += 1
        
        # --- Paths exist ---
        # Context profiles count
        ctx_profiles = semantic.get('context_profiles', {})
        expected_ctx = ctx_profiles.get('count', 0)
        ctx_path = self.memory_dir / "semantic" / "context_profiles.yaml"
        actual_ctx = 0
        if ctx_path.exists():
            ctx_data = self.load_yaml(ctx_path)
            if ctx_data and 'profiles' in ctx_data:
                actual_ctx = len(ctx_data['profiles'])
        if expected_ctx == actual_ctx:
            self.log(f"  ✓ semantic.context_profiles.count: {actual_ctx}")
            checks_passed += 1
        else:
            msg = f"_index.yaml semantic.context_profiles.count is {expected_ctx} but {actual_ctx} found"
            self.report.errors.append(msg)
            self.log(f"  ✗ {msg}")
            checks_failed += 1
        
        path_fields = [
            ('semantic.defect_patterns.path', semantic.get('defect_patterns', {}).get('path')),
            ('semantic.testing_heuristics.path', semantic.get('testing_heuristics', {}).get('path')),
            ('semantic.technology_knowledge.path', semantic.get('technology_knowledge', {}).get('path')),
            ('semantic.istqb_knowledge.path', semantic.get('istqb_knowledge', {}).get('path')),
            ('semantic.context_profiles.path', ctx_profiles.get('path')),
            ('relational.path', index.get('relational', {}).get('path')),
        ]
        for field_name, path_val in path_fields:
            if path_val:
                full_path = self.base_dir / path_val
                if full_path.exists():
                    self.log(f"  ✓ {field_name}: exists")
                    checks_passed += 1
                else:
                    msg = f"_index.yaml {field_name} '{path_val}' does not exist"
                    self.report.errors.append(msg)
                    self.log(f"  ✗ {msg}")
                    checks_failed += 1
        
        self.log(f"  Index consistency: {checks_passed} passed, {checks_failed} failed")
    
    # ─────────────────────────────────────────────────────────────────────────
    # POINT D'ENTRÉE
    # ─────────────────────────────────────────────────────────────────────────
    
    def run(self) -> ValidationReport:
        """Exécute la validation complète."""
        self.log("\n" + "═" * 60)
        self.log("🔍 ARGUS REFERENCE VALIDATOR")
        self.log("═" * 60)
        self.log(f"  Memory root: {self.memory_dir}")
        
        # Phase 1: Extraire tous les IDs définis
        self.extract_defined_ids()
        
        # Phase 2: Extraire toutes les références
        self.extract_references()
        
        # Phase 3: Valider les références
        self.validate()
        
        # Phase 4: Valider la cohérence de _index.yaml
        self.validate_index_consistency()
        
        return self.report


# ═══════════════════════════════════════════════════════════════════════════════
#  FONCTIONS UTILITAIRES
# ═══════════════════════════════════════════════════════════════════════════════

def find_project_root() -> Path:
    """Trouve la racine du projet (le dossier contenant argus.persona.yaml et memory/)."""
    # Priorité 1 : remonter depuis le script
    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='Validate cross-file references in Argus memory system',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__
    )
    
    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'
    )
    
    parser.add_argument(
        '--graph', '-g',
        action='store_true',
        help='Generate Mermaid dependency graph'
    )
    
    args = parser.parse_args()
    
    try:
        project_root = find_project_root()
        validator = ReferenceValidator(
            str(project_root), 
            verbose=not args.quiet and not args.json
        )
        
        # Exécuter la validation
        report = validator.run()
        
        # Sortie JSON si demandé
        if args.json:
            print(json.dumps(report.to_dict(), indent=2))
            return 1 if (report.has_orphans or report.errors) else 0
        
        # Afficher le rapport
        validator.print_report()
        
        # Générer le graphe si demandé
        if args.graph:
            print("\n📊 DEPENDENCY GRAPH:\n")
            print(validator.generate_dependency_graph())
        
        return 1 if (report.has_orphans or report.errors) else 0
        
    except Exception as e:
        if not args.quiet:
            print(f"\n❌ Script error: {e}", file=sys.stderr)
            import traceback
            traceback.print_exc()
        return 2


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