#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Moteur de Decay (Oubli Contrôlé)
 Réduit la confidence des connaissances non renforcées
═══════════════════════════════════════════════════════════════════════════════

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

Exit codes:
  0 = Aucun decay nécessaire
  1 = Decays détectés / appliqués
  2 = Erreur d'exécution

Logique de decay (persona: memory.operations.forgetting):
  - Confidence diminue de DECAY_RATE par mois sans renforcement
  - Pattern supprimé si confidence < DECAY_THRESHOLD et pas d'usage récent
  - Protection: incidents critiques (P0/P1) ne decayent jamais
  - Protection: patterns avec occurrences > 0 decayent plus lentement
"""

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

from config import (
    DECAY_RATE, DECAY_RATE_REINFORCED, DECAY_THRESHOLD,
    DECAY_GRACE_DAYS, PROTECTED_CATEGORIES,
)

logger = logging.getLogger(__name__)


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

@dataclass
class DecayAction:
    """Une action de decay sur un élément sémantique."""
    target_id: str
    target_type: str            # "pattern" | "heuristic"
    current_confidence: float
    new_confidence: float
    decay_amount: float
    days_since_activity: int
    reason: str
    protected: bool = False
    flagged_for_removal: bool = False

    def to_dict(self) -> dict:
        return {
            "target_id": self.target_id,
            "target_type": self.target_type,
            "current_confidence": self.current_confidence,
            "new_confidence": round(self.new_confidence, 4),
            "decay_amount": round(self.decay_amount, 4),
            "days_since_activity": self.days_since_activity,
            "reason": self.reason,
            "protected": self.protected,
            "flagged_for_removal": self.flagged_for_removal,
        }


@dataclass
class DecayReport:
    """Rapport de decay complet."""
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    reference_date: str = ""        # Date utilisée pour calculer la durée
    patterns_checked: int = 0
    heuristics_checked: int = 0
    actions: List[DecayAction] = field(default_factory=list)
    protected_count: int = 0
    errors: List[str] = field(default_factory=list)

    @property
    def has_actions(self) -> bool:
        return any(not a.protected for a in self.actions)

    @property
    def decay_count(self) -> int:
        return sum(1 for a in self.actions if not a.protected)

    @property
    def removal_candidates(self) -> int:
        return sum(1 for a in self.actions if a.flagged_for_removal)

    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp,
            "reference_date": self.reference_date,
            "summary": {
                "patterns_checked": self.patterns_checked,
                "heuristics_checked": self.heuristics_checked,
                "decays_needed": self.decay_count,
                "protected": self.protected_count,
                "flagged_for_removal": self.removal_candidates,
                "errors": len(self.errors),
                "status": "DECAY_NEEDED" if self.has_actions else "HEALTHY",
            },
            "actions": [a.to_dict() for a in self.actions if not a.protected],
            "protected": [a.to_dict() for a in self.actions if a.protected],
            "errors": self.errors,
        }


# ═══════════════════════════════════════════════════════════════════════════════
#  DECAY ENGINE
# ═══════════════════════════════════════════════════════════════════════════════

class DecayEngine:
    """
    Moteur d'oubli contrôlé.
    'Oublier n'est pas une faiblesse — c'est de l'hygiène cognitive.'
    """

    def __init__(self, base_dir: str, verbose: bool = True,
                 reference_date: Optional[datetime] = None):
        self.base_dir = Path(base_dir)
        self.memory_dir = self.base_dir / "memory"
        self.verbose = verbose
        self.reference_date = reference_date or datetime.now()
        self.report = DecayReport(reference_date=self.reference_date.strftime("%Y-%m-%d"))

        # IDs of episodes linked to critical incidents
        self._critical_episode_ids: Set[str] = set()

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

    def _load_yaml(self, path: Path) -> Optional[dict]:
        try:
            return yaml.safe_load(path.read_text(encoding="utf-8"))
        except Exception as e:
            self.report.errors.append(f"Cannot load {path}: {e}")
            return None

    def _write_yaml(self, path: Path, data: dict):
        path.write_text(
            yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
        )

    # ─────────────────────────────────────────────────────────────────────────
    #  PROTECTION ANALYSIS
    # ─────────────────────────────────────────────────────────────────────────

    def _load_critical_incidents(self):
        """Charge les IDs liés à des incidents critiques (P0/P1).
        
        Les patterns référencés dans les incidents P0/P1 sont protégés
        contre le decay — on ne veut jamais oublier ce qui a causé
        un incident critique en production.
        """
        incident_dir = self.memory_dir / "episodic" / "incidents"
        if not incident_dir.exists():
            return
        for f in incident_dir.glob("*.yaml"):
            if f.name.startswith("_template"):
                continue
            data = self._load_yaml(f)
            if data and data.get("severity") in ("P0", "P1"):
                eid = data.get("id", "")
                if eid:
                    self._critical_episode_ids.add(eid)
                # Also protect patterns matched
                rc = data.get("root_cause", {})
                if isinstance(rc, dict):
                    for pid in rc.get("matched_patterns", []):
                        self._critical_episode_ids.add(pid)

    def _is_protected(self, item_id: str, item: dict) -> str:
        """Retourne la raison de protection, ou chaîne vide si non protégé.
        
        3 règles de protection (persona: memory.operations.forgetting.protected) :
        1. Catégorie protégée (security, critical) → on ne décroît jamais
        2. Sévérité critique du pattern lui-même
        3. Lien à un incident P0/P1 (directement ou via matched_patterns)
        """
        # Protected by category
        category = item.get("category", "")
        if category in PROTECTED_CATEGORIES:
            return f"Protected category: {category}"

        # Protected by severity
        severity = item.get("severity_typical", "")
        if severity in ("critical", "P0"):
            return f"Critical severity pattern"

        # Protected by link to critical incident
        source_episodes = item.get("source_episodes", [])
        if isinstance(source_episodes, list):
            for ep in source_episodes:
                if ep in self._critical_episode_ids:
                    return f"Linked to critical incident {ep}"

        # Protected if ID itself is in critical set (pattern matched in P0/P1)
        if item_id in self._critical_episode_ids:
            return f"Matched in critical incident"

        return ""

    # ─────────────────────────────────────────────────────────────────────────
    #  DECAY CALCULATION
    # ─────────────────────────────────────────────────────────────────────────

    def _calculate_decay(self, item_id: str, item: dict, item_type: str) -> Optional[DecayAction]:
        """Calcule le decay pour un élément sémantique.
        
        Algorithme :
        1. Calculer les jours depuis last_seen (365 si jamais vu)
        2. Vérifier la période de grâce (DECAY_GRACE_DAYS = 60j) → aucun decay si récent
        3. Vérifier la protection (catégorie, sévérité, incident critique)
        4. Calculer : months_inactive × rate (0.05/mois, ou 0.02 si occurrences > 0)
        5. Flaguer pour suppression si new_confidence < DECAY_THRESHOLD (0.20)
        """
        confidence = item.get("confidence", 0.5)
        last_seen = item.get("last_seen")
        occurrences = item.get("occurrences", 0)

        # Determine days since last activity
        if last_seen:
            try:
                last_date = datetime.strptime(str(last_seen), "%Y-%m-%d")
            except ValueError:
                try:
                    last_date = datetime.fromisoformat(str(last_seen))
                except ValueError:
                    last_date = None
        else:
            last_date = None

        if last_date:
            days_since = (self.reference_date - last_date).days
        else:
            # Never seen = use a high default
            days_since = 365

        # Période de grâce : pas de decay si l'élément a été vu récemment
        # (seuil configuré dans config.DECAY_GRACE_DAYS)
        if days_since < DECAY_GRACE_DAYS:
            return None

        # Protection check
        protection_reason = self._is_protected(item_id, item)
        if protection_reason:
            self.report.protected_count += 1
            return DecayAction(
                target_id=item_id,
                target_type=item_type,
                current_confidence=confidence,
                new_confidence=confidence,
                decay_amount=0.0,
                days_since_activity=days_since,
                reason=protection_reason,
                protected=True,
            )

        # Calculate decay amount
        months_inactive = days_since / 30.0
        # Decay plus lent pour les patterns renforcés par des épisodes réels
        # (occurrences > 0 signifie que le pattern a été observé en pratique)
        rate = DECAY_RATE_REINFORCED if occurrences > 0 else DECAY_RATE
        decay_amount = rate * months_inactive

        # Plancher à 0.0 — la confidence ne peut pas devenir négative
        new_confidence = max(0.0, round(confidence - decay_amount, 4))
        # Sous le seuil = candidat à la suppression (review humaine recommandée)
        flagged = new_confidence < DECAY_THRESHOLD

        reason = (f"{months_inactive:.1f} months inactive, "
                  f"rate={rate}/month, occurrences={occurrences}")

        return DecayAction(
            target_id=item_id,
            target_type=item_type,
            current_confidence=confidence,
            new_confidence=new_confidence,
            decay_amount=decay_amount,
            days_since_activity=days_since,
            reason=reason,
            flagged_for_removal=flagged,
        )

    # ─────────────────────────────────────────────────────────────────────────
    #  SCANNING
    # ─────────────────────────────────────────────────────────────────────────

    def _scan_patterns(self) -> List[DecayAction]:
        """Scanne les patterns pour le decay."""
        actions = []
        path = self.memory_dir / "semantic" / "defect_patterns.yaml"
        if not path.exists():
            return actions

        data = self._load_yaml(path)
        if not data or "patterns" not in data:
            return actions

        for p in data["patterns"]:
            pid = p.get("id", "")
            if not pid:
                continue
            self.report.patterns_checked += 1
            action = self._calculate_decay(pid, p, "pattern")
            if action:
                actions.append(action)

        return actions

    def _scan_heuristics(self) -> List[DecayAction]:
        """Scanne les heuristiques pour le decay."""
        actions = []
        path = self.memory_dir / "semantic" / "testing_heuristics.yaml"
        if not path.exists():
            return actions

        data = self._load_yaml(path)
        if not data:
            return actions

        for key, value in data.items():
            if isinstance(value, list):
                for h in value:
                    if isinstance(h, dict) and "id" in h:
                        self.report.heuristics_checked += 1
                        action = self._calculate_decay(h["id"], h, "heuristic")
                        if action:
                            actions.append(action)

        return actions

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

    def _apply_decay(self):
        """Applique les decays calculés aux fichiers sémantiques."""
        active_decays = {a.target_id: a for a in self.report.actions if not a.protected}
        if not active_decays:
            return

        # Patterns
        patterns_path = self.memory_dir / "semantic" / "defect_patterns.yaml"
        if patterns_path.exists():
            data = self._load_yaml(patterns_path)
            modified = False
            if data and "patterns" in data:
                for p in data["patterns"]:
                    pid = p.get("id", "")
                    if pid in active_decays:
                        p["confidence"] = active_decays[pid].new_confidence
                        modified = True
            if modified:
                self._write_yaml(patterns_path, data)

        # Heuristics
        heuristics_path = self.memory_dir / "semantic" / "testing_heuristics.yaml"
        if heuristics_path.exists():
            data = self._load_yaml(heuristics_path)
            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 active_decays:
                                h["confidence"] = active_decays[h["id"]].new_confidence
                                modified = True
            if modified:
                self._write_yaml(heuristics_path, data)

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

    def scan(self) -> DecayReport:
        """Scanne et identifie les decays nécessaires."""
        self.log("\n═══ ARGUS — Decay Scanner ═══\n")
        self.log(f"  Reference date: {self.reference_date.strftime('%Y-%m-%d')}")
        self.log(f"  Grace period: {DECAY_GRACE_DAYS} days")
        self.log(f"  Decay rate: {DECAY_RATE}/month (reinforced: {DECAY_RATE_REINFORCED}/month)")
        self.log(f"  Removal threshold: {DECAY_THRESHOLD}\n")

        # Load critical incidents for protection
        self._load_critical_incidents()

        # Scan
        self.report.actions.extend(self._scan_patterns())
        self.report.actions.extend(self._scan_heuristics())

        # Report
        self.log(f"  Patterns checked: {self.report.patterns_checked}")
        self.log(f"  Heuristics checked: {self.report.heuristics_checked}")
        self.log(f"  Decays needed: {self.report.decay_count}")
        self.log(f"  Protected: {self.report.protected_count}")
        self.log(f"  Flagged for removal: {self.report.removal_candidates}")

        if self.report.has_actions:
            self.log("\n  Decay details:")
            for a in self.report.actions:
                if not a.protected:
                    status = " ⚠ REMOVE?" if a.flagged_for_removal else ""
                    self.log(f"    {a.target_id}: {a.current_confidence:.2f} → "
                             f"{a.new_confidence:.2f} (-{a.decay_amount:.3f}){status}")
        else:
            self.log("\n  ✓ All knowledge is healthy (within grace period or protected).")

        return self.report

    def apply(self) -> DecayReport:
        """Scanne ET applique le decay."""
        self.scan()
        if self.report.has_actions:
            self.log("\n  Applying decay...")
            self._apply_decay()
            self.log("  ✓ Decay applied.")
        return self.report


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

def main():
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    parser = argparse.ArgumentParser(
        description="Argus — Memory Decay Engine"
    )
    parser.add_argument(
        "--apply", action="store_true",
        help="Apply decay (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"
    )
    parser.add_argument(
        "--reference-date", type=str, default=None,
        help="Reference date for decay calculation (YYYY-MM-DD). Default: today"
    )
    args = parser.parse_args()

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

    ref_date = None
    if args.reference_date:
        ref_date = datetime.strptime(args.reference_date, "%Y-%m-%d")

    engine = DecayEngine(base_dir, verbose=verbose, reference_date=ref_date)

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