#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Moteur de Consolidation Mémoire
 Transforme les épisodes répétés en connaissances sémantiques
═══════════════════════════════════════════════════════════════════════════════

Usage:
  python consolidation.py              # Mode détection (affiche les consolidations possibles)
  python consolidation.py --apply      # Applique les consolidations
  python consolidation.py --quiet      # Mode silencieux (exit code uniquement)
  python consolidation.py --json       # Sortie JSON

Exit codes:
  0 = Aucune consolidation nécessaire
  1 = Consolidations possibles détectées
  2 = Erreur d'exécution

Processus de consolidation (persona: memory.operations.consolidation):
  1. Scanner les épisodes non consolidés
  2. Identifier les patterns répétés (matched_patterns dans incidents, findings dans analyses)
  3. Renforcer les patterns sémantiques (confidence++, occurrences++, source_episodes)
  4. Marquer les épisodes comme consolidés
  5. Détecter les candidats pour de nouveaux patterns
"""

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

from config import CONFIDENCE_BOOST

logger = logging.getLogger(__name__)


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

@dataclass
class Reinforcement:
    """Un renforcement à appliquer à un pattern/heuristique sémantique."""
    target_id: str                  # PAT-xxx or HEU-xxx
    target_type: str                # "pattern" | "heuristic"
    source_episode: str             # ANL-xxx or INC-xxx
    source_file: str                # Chemin du fichier épisodique
    confidence_delta: float         # Combien augmenter la confidence
    new_occurrence: bool            # Incrémenter occurrences ?
    context: str = ""               # Description du lien

    def to_dict(self) -> dict:
        return {
            "target_id": self.target_id,
            "target_type": self.target_type,
            "source_episode": self.source_episode,
            "confidence_delta": self.confidence_delta,
            "new_occurrence": self.new_occurrence,
            "context": self.context,
        }


@dataclass
class NewPatternCandidate:
    """Un épisode qui suggère un nouveau pattern sémantique."""
    source_episode: str
    source_file: str
    description: str
    category_suggestion: str
    evidence: List[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        return {
            "source_episode": self.source_episode,
            "description": self.description,
            "category_suggestion": self.category_suggestion,
            "evidence": self.evidence,
        }


@dataclass
class ConsolidationReport:
    """Rapport de consolidation complet."""
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    episodes_scanned: int = 0
    episodes_unconsolidated: int = 0
    reinforcements: List[Reinforcement] = field(default_factory=list)
    new_pattern_candidates: List[NewPatternCandidate] = field(default_factory=list)
    associations_found: List[dict] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)

    @property
    def has_actions(self) -> bool:
        return bool(self.reinforcements or self.new_pattern_candidates or self.associations_found)

    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp,
            "summary": {
                "episodes_scanned": self.episodes_scanned,
                "episodes_unconsolidated": self.episodes_unconsolidated,
                "reinforcements": len(self.reinforcements),
                "new_pattern_candidates": len(self.new_pattern_candidates),
                "associations_found": len(self.associations_found),
                "errors": len(self.errors),
                "status": "ACTIONS_NEEDED" if self.has_actions else "UP_TO_DATE",
            },
            "reinforcements": [r.to_dict() for r in self.reinforcements],
            "new_pattern_candidates": [c.to_dict() for c in self.new_pattern_candidates],
            "associations_found": self.associations_found,
            "errors": self.errors,
        }


# ═══════════════════════════════════════════════════════════════════════════════
#  ID PATTERNS
#  Regex utilisées pour scanner du texte YAML brut et découvrir des
#  références à des patterns / heuristiques dans des champs non structurés
#  (descriptions, rationales, texte libre)
# ═══════════════════════════════════════════════════════════════════════════════

PATTERN_ID_RE = re.compile(r'\bPAT-[A-Z]+-\d{3}\b')
HEURISTIC_ID_RE = re.compile(r'\bHEU-[A-Z]+-\d{3}\b')


# ═══════════════════════════════════════════════════════════════════════════════
#  CONSOLIDATION ENGINE
# ═══════════════════════════════════════════════════════════════════════════════

class ConsolidationEngine:
    """
    Moteur de consolidation mémoire.
    Scanne les épisodes et renforce la mémoire sémantique.
    """

    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 = ConsolidationReport()

        # Caches
        self._known_pattern_ids: Set[str] = set()
        self._known_heuristic_ids: Set[str] = set()
        self._patterns_data: Dict[str, dict] = {}
        self._heuristics_data: Dict[str, dict] = {}

    def log(self, message: str, force: bool = False):
        if self.verbose or force:
            logger.info(message)

    # ─────────────────────────────────────────────────────────────────────────
    #  LOADING
    # ─────────────────────────────────────────────────────────────────────────

    def _load_yaml(self, path: Path) -> Optional[dict]:
        """Charge un fichier YAML, retourne None en cas d'erreur."""
        try:
            text = path.read_text(encoding="utf-8")
            return yaml.safe_load(text)
        except Exception as e:
            self.report.errors.append(f"Cannot load {path}: {e}")
            return None

    def _load_semantic_index(self):
        """Charge les IDs connus des patterns et heuristiques.
        
        Construit les caches _known_pattern_ids / _known_heuristic_ids
        pour pouvoir vérifier rapidement si un ID référencé dans un épisode
        correspond à un pattern ou une heuristique existantes.
        """
        # Patterns
        patterns_path = self.memory_dir / "semantic" / "defect_patterns.yaml"
        if patterns_path.exists():
            data = self._load_yaml(patterns_path)
            if data and "patterns" in data:
                for p in data["patterns"]:
                    pid = p.get("id", "")
                    self._known_pattern_ids.add(pid)
                    self._patterns_data[pid] = p

        # Heuristics - parse all sections
        heuristics_path = self.memory_dir / "semantic" / "testing_heuristics.yaml"
        if heuristics_path.exists():
            data = self._load_yaml(heuristics_path)
            if data:
                for key, value in data.items():
                    if isinstance(value, list):
                        for h in value:
                            if isinstance(h, dict) and "id" in h:
                                hid = h["id"]
                                self._known_heuristic_ids.add(hid)
                                self._heuristics_data[hid] = h

    # ─────────────────────────────────────────────────────────────────────────
    #  EPISODE SCANNING
    # ─────────────────────────────────────────────────────────────────────────

    def _scan_episodes(self) -> List[Tuple[Path, dict]]:
        """Scanne tous les épisodes et retourne les non-consolidés.
        
        Un épisode est "consolidé" une fois que ses références ont
        été extraites et que la mémoire sémantique a été renforcée.
        Seuls les épisodes non consolidés sont retournés.
        """
        episodes = []
        episodic_dir = self.memory_dir / "episodic"

        for subdir in ["analyses", "incidents", "decisions", "executions"]:
            dir_path = episodic_dir / subdir
            if not dir_path.exists():
                continue
            for f in sorted(dir_path.glob("*.yaml")):
                if f.name.startswith("_template"):
                    continue
                data = self._load_yaml(f)
                if data is None:
                    continue
                self.report.episodes_scanned += 1

                # Check consolidation status
                consolidation = data.get("consolidation", {})
                if isinstance(consolidation, dict) and consolidation.get("consolidated"):
                    continue

                self.report.episodes_unconsolidated += 1
                episodes.append((f, data))

        return episodes

    # ─────────────────────────────────────────────────────────────────────────
    #  EXTRACTION
    # ─────────────────────────────────────────────────────────────────────────

    def _extract_from_incident(self, path: Path, data: dict):
        """Extrait les consolidations depuis un incident.
        
        Deux sources de renforcement :
        1. matched_patterns dans root_cause → confidence boost (incident_match = +0.03)
        2. new_pattern_candidate dans root_cause → signal pour créer un nouveau pattern
        """
        episode_id = data.get("id", path.stem)

        # matched_patterns dans root_cause
        root_cause = data.get("root_cause", {})
        if isinstance(root_cause, dict):
            matched = root_cause.get("matched_patterns", [])
            if isinstance(matched, list):
                for pat_id in matched:
                    if pat_id in self._known_pattern_ids:
                        self.report.reinforcements.append(Reinforcement(
                            target_id=pat_id,
                            target_type="pattern",
                            source_episode=episode_id,
                            source_file=str(path.relative_to(self.base_dir)),
                            confidence_delta=CONFIDENCE_BOOST["incident_match"],
                            new_occurrence=True,
                            context=f"Pattern matched in incident {episode_id}",
                        ))

            # New pattern candidate?
            if root_cause.get("new_pattern_candidate"):
                desc = root_cause.get("description", "Unknown")
                factors = root_cause.get("contributing_factors", [])
                categories = [f.get("category", "") for f in factors if isinstance(f, dict)]
                self.report.new_pattern_candidates.append(NewPatternCandidate(
                    source_episode=episode_id,
                    source_file=str(path.relative_to(self.base_dir)),
                    description=str(desc).strip(),
                    category_suggestion=categories[0] if categories else "unknown",
                    evidence=[str(f) for f in factors],
                ))

    def _extract_from_analysis(self, path: Path, data: dict):
        """Extrait les consolidations depuis une analyse.
        
        Deux mécanismes :
        1. Champs structurés : findings[].pattern_id référencés directement
        2. Scan texte brut : regex sur le YAML dump pour capturer les IDs
           mentionnés dans des descriptions ou rationales (champs non structurés)
        """
        episode_id = data.get("id") or data.get("metadata", {}).get("id", path.stem)

        # findings → patterns referenced
        findings = data.get("findings", [])
        if isinstance(findings, list):
            for finding in findings:
                if not isinstance(finding, dict):
                    continue
                # Look for pattern references in finding text
                text = str(finding)
                for pat_id in PATTERN_ID_RE.findall(text):
                    if pat_id in self._known_pattern_ids:
                        self.report.reinforcements.append(Reinforcement(
                            target_id=pat_id,
                            target_type="pattern",
                            source_episode=episode_id,
                            source_file=str(path.relative_to(self.base_dir)),
                            confidence_delta=CONFIDENCE_BOOST["analysis_finding"],
                            new_occurrence=True,
                            context=f"Pattern referenced in analysis finding",
                        ))

        # Also scan full text for pattern/heuristic references
        full_text = yaml.dump(data, default_flow_style=False, allow_unicode=True)
        self._extract_references_from_text(path, episode_id, full_text, "analysis_finding")

    def _extract_from_decision(self, path: Path, data: dict):
        """Extrait les consolidations depuis une décision."""
        episode_id = data.get("id", path.stem)

        knowledge = data.get("knowledge_applied", {})
        if isinstance(knowledge, dict):
            for pat_id in knowledge.get("patterns_considered", []):
                if pat_id in self._known_pattern_ids:
                    self.report.reinforcements.append(Reinforcement(
                        target_id=pat_id,
                        target_type="pattern",
                        source_episode=episode_id,
                        source_file=str(path.relative_to(self.base_dir)),
                        confidence_delta=CONFIDENCE_BOOST["analysis_finding"],
                        new_occurrence=False,
                        context="Pattern considered in decision",
                    ))

            for heu_id in knowledge.get("heuristics_used", []):
                if heu_id in self._known_heuristic_ids:
                    self.report.reinforcements.append(Reinforcement(
                        target_id=heu_id,
                        target_type="heuristic",
                        source_episode=episode_id,
                        source_file=str(path.relative_to(self.base_dir)),
                        confidence_delta=CONFIDENCE_BOOST["heuristic_applied"],
                        new_occurrence=False,
                        context="Heuristic applied in decision",
                    ))

    def _extract_references_from_text(self, path: Path, episode_id: str,
                                       text: str, boost_type: str):
        """Scanne du texte brut pour trouver des références dans des
        champs non structurés (descriptions, rationales, etc.).
        
        Déduplication : les IDs déjà détectés via les champs structurés
        ne sont pas comptabilisés une seconde fois (new_occurrence=False).
        """
        # Deduplicated — only register IDs not already in reinforcements
        already = {r.target_id for r in self.report.reinforcements
                   if r.source_episode == episode_id}

        for pat_id in set(PATTERN_ID_RE.findall(text)):
            if pat_id in self._known_pattern_ids and pat_id not in already:
                self.report.reinforcements.append(Reinforcement(
                    target_id=pat_id,
                    target_type="pattern",
                    source_episode=episode_id,
                    source_file=str(path.relative_to(self.base_dir)),
                    confidence_delta=CONFIDENCE_BOOST[boost_type],
                    new_occurrence=False,
                    context=f"Pattern referenced in text of {episode_id}",
                ))

        for heu_id in set(HEURISTIC_ID_RE.findall(text)):
            if heu_id in self._known_heuristic_ids and heu_id not in already:
                self.report.reinforcements.append(Reinforcement(
                    target_id=heu_id,
                    target_type="heuristic",
                    source_episode=episode_id,
                    source_file=str(path.relative_to(self.base_dir)),
                    confidence_delta=CONFIDENCE_BOOST.get(boost_type, 0.01),
                    new_occurrence=False,
                    context=f"Heuristic referenced in text of {episode_id}",
                ))

    # ─────────────────────────────────────────────────────────────────────────
    #  ASSOCIATION DISCOVERY
    # ─────────────────────────────────────────────────────────────────────────

    def _discover_associations(self, episodes: List[Tuple[Path, dict]]):
        """Détecte des associations relationnelles dans les épisodes."""
        for path, data in episodes:
            episode_id = data.get("id") or data.get("metadata", {}).get("id", "")

            # file ↔ defect associations from incidents
            if "affected_components" in data or "root_cause" in data:
                components = data.get("affected_components", [])
                severity = data.get("severity", "P3")
                if components and episode_id:
                    for comp in components:
                        self.report.associations_found.append({
                            "type": "component_to_defect",
                            "source": comp,
                            "target": episode_id,
                            "severity": severity,
                        })

    # ─────────────────────────────────────────────────────────────────────────
    #  APPLY
    # ─────────────────────────────────────────────────────────────────────────

    def _apply_reinforcements(self):
        """Applique les renforcements à la mémoire sémantique.
        
        Pour chaque pattern/heuristique renforcé :
        - confidence += somme des deltas (plafonné à 1.0)
        - occurrences += nombre de new_occurrence=True
        - source_episodes += IDs des épisodes sources (dédupliqués)
        - last_seen = today
        """
        today = datetime.now().strftime("%Y-%m-%d")

        # Group reinforcements by target
        by_target: Dict[str, List[Reinforcement]] = {}
        for r in self.report.reinforcements:
            by_target.setdefault(r.target_id, []).append(r)

        # Apply to patterns
        patterns_path = self.memory_dir / "semantic" / "defect_patterns.yaml"
        if patterns_path.exists():
            text = patterns_path.read_text(encoding="utf-8")
            data = yaml.safe_load(text)
            modified = False
            if data and "patterns" in data:
                for p in data["patterns"]:
                    pid = p.get("id", "")
                    if pid in by_target:
                        reinforcements = by_target[pid]
                        # Confidence boost (capped at 1.0)
                        total_boost = sum(r.confidence_delta for r in reinforcements)
                        old_conf = p.get("confidence", 0.5)
                        p["confidence"] = min(1.0, round(old_conf + total_boost, 4))

                        # Occurrences
                        new_occ = sum(1 for r in reinforcements if r.new_occurrence)
                        p["occurrences"] = p.get("occurrences", 0) + new_occ

                        # Source episodes
                        existing = set(p.get("source_episodes", []) or [])
                        for r in reinforcements:
                            existing.add(r.source_episode)
                        p["source_episodes"] = sorted(existing)

                        # Last seen
                        p["last_seen"] = today
                        modified = True

            if modified:
                self._write_yaml_preserve_comments(patterns_path, data)

        # Apply to heuristics
        heuristics_path = self.memory_dir / "semantic" / "testing_heuristics.yaml"
        if heuristics_path.exists():
            text = heuristics_path.read_text(encoding="utf-8")
            data = yaml.safe_load(text)
            modified = False
            if data:
                for key, value in data.items():
                    if isinstance(value, list):
                        for h in value:
                            if isinstance(h, dict) and h.get("id") in by_target:
                                hid = h["id"]
                                reinforcements = by_target[hid]
                                total_boost = sum(r.confidence_delta for r in reinforcements)
                                old_conf = h.get("confidence", 0.5)
                                h["confidence"] = min(1.0, round(old_conf + total_boost, 4))
                                h["last_seen"] = today
                                modified = True

            if modified:
                self._write_yaml_preserve_comments(heuristics_path, data)

    def _mark_episodes_consolidated(self, episodes: List[Tuple[Path, dict]]):
        """Marque les épisodes scannés comme consolidés."""
        today = datetime.now().isoformat()
        for path, data in episodes:
            if "consolidation" not in data:
                data["consolidation"] = {}
            data["consolidation"]["consolidated"] = True
            data["consolidation"]["consolidated_at"] = today
            data["consolidation"]["reinforcements_generated"] = sum(
                1 for r in self.report.reinforcements
                if r.source_file == str(path.relative_to(self.base_dir))
            )
            self._write_yaml_preserve_comments(path, data)

    def _update_index(self):
        """Met à jour l'index avec la date de dernière consolidation."""
        index_path = self.memory_dir / "_index.yaml"
        if not index_path.exists():
            return
        data = self._load_yaml(index_path)
        if data and "statistics" in data and "activity" in data["statistics"]:
            data["statistics"]["activity"]["last_consolidation"] = datetime.now().strftime("%Y-%m-%d")
            self._write_yaml_preserve_comments(index_path, data)

    def _write_yaml_preserve_comments(self, path: Path, data: dict):
        """Ecrit du YAML.
        
        Note : les commentaires YAML d'origine sont perdus car PyYAML
        ne les préserve pas. Pour un système de production, ruamel.yaml
        serait nécessaire pour un round-trip fidèle.
        """
        path.write_text(
            yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
        )

    # ─────────────────────────────────────────────────────────────────────────
    #  MAIN WORKFLOW
    # ─────────────────────────────────────────────────────────────────────────

    def scan(self) -> ConsolidationReport:
        """Scanne et identifie les consolidations possibles."""
        self.log("\n═══ ARGUS — Consolidation Scanner ═══\n")

        # 1. Load semantic index
        self._load_semantic_index()
        self.log(f"  Semantic index: {len(self._known_pattern_ids)} patterns, "
                 f"{len(self._known_heuristic_ids)} heuristics")

        # 2. Scan episodes
        episodes = self._scan_episodes()
        self.log(f"  Episodes scanned: {self.report.episodes_scanned}")
        self.log(f"  Unconsolidated: {self.report.episodes_unconsolidated}")

        if not episodes:
            self.log("\n  ✓ All episodes already consolidated.")
            return self.report

        # 3. Extract from each episode type
        for path, data in episodes:
            episode_id = data.get("id") or data.get("metadata", {}).get("id", "")
            parts = path.parts
            if "incidents" in parts:
                self._extract_from_incident(path, data)
            elif "analyses" in parts:
                self._extract_from_analysis(path, data)
            elif "decisions" in parts:
                self._extract_from_decision(path, data)
            # executions don't directly reinforce patterns

        # 4. Discover associations
        self._discover_associations(episodes)

        # 5. Report
        self.log(f"\n  Reinforcements found: {len(self.report.reinforcements)}")
        for r in self.report.reinforcements:
            self.log(f"    → {r.target_id} +{r.confidence_delta:.2f} "
                     f"(from {r.source_episode})")

        if self.report.new_pattern_candidates:
            self.log(f"\n  New pattern candidates: {len(self.report.new_pattern_candidates)}")
            for c in self.report.new_pattern_candidates:
                self.log(f"    ⊕ {c.description[:60]} (from {c.source_episode})")

        if self.report.associations_found:
            self.log(f"\n  Associations discovered: {len(self.report.associations_found)}")

        return self.report

    def apply(self) -> ConsolidationReport:
        """Scanne ET applique les consolidations."""
        self.scan()

        if not self.report.has_actions:
            return self.report

        self.log("\n  Applying reinforcements...")
        self._apply_reinforcements()

        self.log("  Marking episodes as consolidated...")
        # Re-scan to get fresh data for marking
        episodic_dir = self.memory_dir / "episodic"
        all_episodes = []
        for subdir in ["analyses", "incidents", "decisions", "executions"]:
            dir_path = episodic_dir / subdir
            if not dir_path.exists():
                continue
            for f in sorted(dir_path.glob("*.yaml")):
                if f.name.startswith("_template"):
                    continue
                data = self._load_yaml(f)
                if data is None:
                    continue
                consolidation = data.get("consolidation", {})
                if isinstance(consolidation, dict) and consolidation.get("consolidated"):
                    continue
                all_episodes.append((f, data))

        self._mark_episodes_consolidated(all_episodes)

        self.log("  Updating index...")
        self._update_index()

        self.log("\n  ✓ Consolidation applied.")
        return self.report


# ═══════════════════════════════════════════════════════════════════════════════
#  CLI
# ═══════════════════════════════════════════════════════════════════════════════

def main():
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    parser = argparse.ArgumentParser(
        description="Argus — Memory Consolidation Engine"
    )
    parser.add_argument(
        "--apply", action="store_true",
        help="Apply consolidations (default: detect only)"
    )
    parser.add_argument(
        "--quiet", action="store_true",
        help="Suppress output (exit code only)"
    )
    parser.add_argument(
        "--json", action="store_true",
        help="Output JSON report"
    )
    parser.add_argument(
        "--base-dir", type=str, default=None,
        help="Project root directory (default: auto-detect)"
    )
    args = parser.parse_args()

    base_dir = args.base_dir or str(Path(__file__).parent.parent)
    verbose = not args.quiet and not args.json

    engine = ConsolidationEngine(base_dir, verbose=verbose)

    try:
        if args.apply:
            report = engine.apply()
        else:
            report = engine.scan()
    except Exception as e:
        if args.json:
            print(json.dumps({"error": str(e)}, indent=2))
        elif not args.quiet:
            print(f"\n  ✗ Error: {e}", file=sys.stderr)
        sys.exit(2)

    if args.json:
        print(json.dumps(report.to_dict(), indent=2, ensure_ascii=False))

    sys.exit(1 if report.has_actions else 0)


if __name__ == "__main__":
    main()
