#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
═══════════════════════════════════════════════════════════════════════════════
 ARGUS — Orchestrateur de Cycle Cognitif
 Enchaîne consolidation → decay → reflection en séquence
═══════════════════════════════════════════════════════════════════════════════

Usage:
  python cognitive_cycle.py                   # Cycle complet (détection seule)
  python cognitive_cycle.py --apply           # Applique consolidation + decay
  python cognitive_cycle.py --quiet           # Mode silencieux (exit code)
  python cognitive_cycle.py --json            # Sortie JSON unifiée
  python cognitive_cycle.py --phase consolidation   # Phase unique
  python cognitive_cycle.py --phase decay           # Phase unique
  python cognitive_cycle.py --phase reflection      # Phase unique
  python cognitive_cycle.py --dry-run         # Alias de scan sans --apply

Exit codes:
  0 = Mémoire saine, aucune action requise
  1 = Actions détectées ou appliquées (consolidation/decay/reflection warnings)
  2 = Erreur d'exécution

Cycle cognitif (persona: memory.operations):
  Phase 1 — Consolidation : renforcer les patterns observés dans les épisodes
  Phase 2 — Decay : oubli contrôlé des connaissances non renforcées
  Phase 3 — Reflection : auto-diagnostic complet de la santé mémoire

L'ordre est important :
  - Consolidation d'abord → met à jour les confidences depuis les épisodes
  - Decay ensuite → calcule le decay sur les confidences *après* renforcement
  - Reflection en dernier → audite l'état final de la mémoire
"""

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

logger = logging.getLogger(__name__)

# Import des moteurs
sys.path.insert(0, str(Path(__file__).parent))
from consolidation import ConsolidationEngine, ConsolidationReport
from decay import DecayEngine, DecayReport
from reflection import ReflectionEngine, ReflectionReport


# ═══════════════════════════════════════════════════════════════════════════════
#  CONSTANTS
# ═══════════════════════════════════════════════════════════════════════════════

VALID_PHASES = ("consolidation", "decay", "reflection")

PHASE_LABELS = {
    "consolidation": "Phase 1 — Consolidation",
    "decay":         "Phase 2 — Decay",
    "reflection":    "Phase 3 — Reflection",
}


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

@dataclass
class PhaseResult:
    """Résultat d'une phase individuelle du cycle."""
    phase: str                         # "consolidation" | "decay" | "reflection"
    status: str                        # "ok" | "actions_needed" | "issues_found" | "skipped" | "error"
    duration_ms: float = 0.0
    summary: Dict[str, Any] = field(default_factory=dict)
    error: Optional[str] = None

    def to_dict(self) -> dict:
        d = {
            "phase": self.phase,
            "status": self.status,
            "duration_ms": round(self.duration_ms, 1),
            "summary": self.summary,
        }
        if self.error:
            d["error"] = self.error
        return d


@dataclass
class CycleReport:
    """Rapport complet d'un cycle cognitif."""
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    mode: str = "scan"                 # "scan" | "apply"
    phases_requested: List[str] = field(default_factory=list)
    phases_executed: List[PhaseResult] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)

    # Reports individuels (accessibles pour les tests)
    consolidation_report: Optional[ConsolidationReport] = None
    decay_report: Optional[DecayReport] = None
    reflection_report: Optional[ReflectionReport] = None

    @property
    def has_actions(self) -> bool:
        """Y a-t-il des actions à prendre ou des problèmes détectés ?"""
        return any(p.status in ("actions_needed", "issues_found", "error")
                   for p in self.phases_executed)

    @property
    def overall_status(self) -> str:
        """Statut global du cycle."""
        if any(p.status == "error" for p in self.phases_executed):
            return "ERROR"
        if any(p.status == "issues_found" for p in self.phases_executed):
            return "ISSUES_FOUND"
        if any(p.status == "actions_needed" for p in self.phases_executed):
            return "ACTIONS_NEEDED"
        return "HEALTHY"

    @property
    def total_duration_ms(self) -> float:
        return sum(p.duration_ms for p in self.phases_executed)

    def to_dict(self) -> dict:
        d = {
            "timestamp": self.timestamp,
            "mode": self.mode,
            "overall_status": self.overall_status,
            "total_duration_ms": round(self.total_duration_ms, 1),
            "phases_requested": self.phases_requested,
            "phases": [p.to_dict() for p in self.phases_executed],
        }
        if self.errors:
            d["errors"] = self.errors

        # Inclure les rapports détaillés s'ils existent
        if self.consolidation_report:
            d["consolidation_detail"] = self.consolidation_report.to_dict()
        if self.decay_report:
            d["decay_detail"] = self.decay_report.to_dict()
        if self.reflection_report:
            d["reflection_detail"] = self.reflection_report.to_dict()

        return d


# ═══════════════════════════════════════════════════════════════════════════════
#  COGNITIVE CYCLE ORCHESTRATOR
# ═══════════════════════════════════════════════════════════════════════════════

class CognitiveCycle:
    """
    Orchestrateur du cycle cognitif d'Argus.
    
    Enchaîne les phases dans l'ordre correct :
      consolidation → decay → reflection
      
    Chaque phase est indépendante et peut être exécutée seule,
    mais l'enchaînement garantit la cohérence du résultat.
    """

    def __init__(self, base_dir: str, verbose: bool = True,
                 reference_date: Optional[datetime] = None):
        self.base_dir = Path(base_dir)
        self.verbose = verbose
        self.reference_date = reference_date
        self.report = CycleReport()

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

    # ─────────────────────────────────────────────────────────────────────────
    #  PHASE RUNNERS
    # ─────────────────────────────────────────────────────────────────────────

    def _run_consolidation(self, apply: bool = False) -> PhaseResult:
        """Exécute la phase de consolidation."""
        result = PhaseResult(phase="consolidation", status="ok")
        t0 = datetime.now()

        try:
            engine = ConsolidationEngine(str(self.base_dir), verbose=self.verbose)
            if apply:
                report = engine.apply()
            else:
                report = engine.scan()

            self.report.consolidation_report = report
            result.summary = report.to_dict()["summary"]

            if report.errors:
                result.status = "error"
                result.error = "; ".join(report.errors[:3])
            elif report.has_actions:
                result.status = "actions_needed"
            else:
                result.status = "ok"

        except Exception as e:
            result.status = "error"
            result.error = str(e)
            self.report.errors.append(f"Consolidation error: {e}")

        result.duration_ms = (datetime.now() - t0).total_seconds() * 1000
        return result

    def _run_decay(self, apply: bool = False) -> PhaseResult:
        """Exécute la phase de decay."""
        result = PhaseResult(phase="decay", status="ok")
        t0 = datetime.now()

        try:
            engine = DecayEngine(
                str(self.base_dir),
                verbose=self.verbose,
                reference_date=self.reference_date,
            )
            if apply:
                report = engine.apply()
            else:
                report = engine.scan()

            self.report.decay_report = report
            result.summary = report.to_dict()["summary"]

            if report.errors:
                result.status = "error"
                result.error = "; ".join(report.errors[:3])
            elif report.has_actions:
                result.status = "actions_needed"
            else:
                result.status = "ok"

        except Exception as e:
            result.status = "error"
            result.error = str(e)
            self.report.errors.append(f"Decay error: {e}")

        result.duration_ms = (datetime.now() - t0).total_seconds() * 1000
        return result

    def _run_reflection(self) -> PhaseResult:
        """Exécute la phase de réflexion."""
        result = PhaseResult(phase="reflection", status="ok")
        t0 = datetime.now()

        try:
            engine = ReflectionEngine(
                str(self.base_dir),
                verbose=self.verbose,
                reference_date=self.reference_date,
            )
            report = engine.reflect()

            self.report.reflection_report = report
            result.summary = report.to_dict()["summary"]

            if report.errors:
                result.status = "error"
                result.error = "; ".join(report.errors[:3])
            elif report.has_issues:
                result.status = "issues_found"
            else:
                result.status = "ok"

        except Exception as e:
            result.status = "error"
            result.error = str(e)
            self.report.errors.append(f"Reflection error: {e}")

        result.duration_ms = (datetime.now() - t0).total_seconds() * 1000
        return result

    # ─────────────────────────────────────────────────────────────────────────
    #  MAIN ENTRY POINTS
    # ─────────────────────────────────────────────────────────────────────────

    def run(self, phases: Optional[List[str]] = None,
            apply: bool = False) -> CycleReport:
        """
        Exécute le cycle cognitif.
        
        L'ordre consolidation → decay → reflection est critique :
        - Consolidation d'abord : renforce les confidences depuis les épisodes
        - Decay ensuite : calcule le decay sur les confidences **après** renforcement
        - Reflection en dernier : audite l'état final (post-consolidation et post-decay)
        
        Args:
            phases: Liste de phases à exécuter. None = toutes dans l'ordre.
            apply: Si True, applique consolidation + decay. Reflection est toujours lecture seule.
        """
        if phases is None:
            phases = list(VALID_PHASES)

        # Valider les phases
        for p in phases:
            if p not in VALID_PHASES:
                self.report.errors.append(f"Unknown phase: {p}")
                return self.report

        self.report.mode = "apply" if apply else "scan"
        self.report.phases_requested = list(phases)

        self.log("\n═══ ARGUS — Cognitive Cycle ═══\n")
        self.log(f"  Mode: {'APPLY' if apply else 'SCAN (dry-run)'}")
        self.log(f"  Phases: {' → '.join(phases)}")
        if self.reference_date:
            self.log(f"  Reference date: {self.reference_date.strftime('%Y-%m-%d')}")
        self.log("")

        # Exécuter les phases dans l'ordre demandé
        phase_runners = {
            "consolidation": lambda: self._run_consolidation(apply=apply),
            "decay":         lambda: self._run_decay(apply=apply),
            "reflection":    lambda: self._run_reflection(),
        }

        for phase_name in phases:
            label = PHASE_LABELS.get(phase_name, phase_name)
            self.log(f"  ┌─ {label} {'─' * (50 - len(label))}")

            result = phase_runners[phase_name]()
            self.report.phases_executed.append(result)

            # Status icon
            icon = {
                "ok": "✓",
                "actions_needed": "⚡",
                "issues_found": "⚠",
                "skipped": "○",
                "error": "✗",
            }.get(result.status, "?")

            self.log(f"  └─ {icon} {result.status.upper()} ({result.duration_ms:.0f}ms)\n")

            # Chaque phase est indépendante : en mode apply, on continue
            # même si une phase échoue, car les suivantes peuvent quand même
            # fournir des diagnostics utiles

        # Résumé final
        self._print_summary()

        return self.report

    def _print_summary(self):
        """Affiche le résumé du cycle."""
        self.log("  ══ Cycle Summary ══")
        
        for pr in self.report.phases_executed:
            icon = {"ok": "✓", "actions_needed": "⚡", "issues_found": "⚠",
                    "skipped": "○", "error": "✗"}.get(pr.status, "?")
            self.log(f"  {icon} {pr.phase}: {pr.status}")

            # Détails clés selon la phase
            if pr.phase == "consolidation" and self.report.consolidation_report:
                cr = self.report.consolidation_report
                self.log(f"    Episodes scanned: {cr.episodes_scanned}, "
                         f"Reinforcements: {len(cr.reinforcements)}, "
                         f"New candidates: {len(cr.new_pattern_candidates)}")

            elif pr.phase == "decay" and self.report.decay_report:
                dr = self.report.decay_report
                self.log(f"    Items checked: {dr.patterns_checked + dr.heuristics_checked}, "
                         f"Decays: {dr.decay_count}, "
                         f"Protected: {dr.protected_count}")

            elif pr.phase == "reflection" and self.report.reflection_report:
                rr = self.report.reflection_report
                self.log(f"    Checks: {len(rr.checks)} "
                         f"({rr.ok_count} ok, {rr.warning_count} warnings, {rr.error_count} errors)")

        overall = self.report.overall_status
        icon = {"HEALTHY": "✓", "ACTIONS_NEEDED": "⚡",
                "ISSUES_FOUND": "⚠", "ERROR": "✗"}[overall]
        self.log(f"\n  {icon} Overall: {overall} "
                 f"(total: {self.report.total_duration_ms:.0f}ms)")

    # ─────────────────────────────────────────────────────────────────────────
    #  WORKING SESSION INTEGRATION
    # ─────────────────────────────────────────────────────────────────────────

    def create_session_record(self, session_id: Optional[str] = None) -> dict:
        """
        Génère un enregistrement de session de travail à partir du cycle.
        Peut être sérialisé en YAML et sauvé dans memory/working/.
        
        Args:
            session_id: ID de session explicite. Auto-généré si None.
        """
        now = datetime.now()
        if session_id is None:
            session_id = f"SES-{now.strftime('%Y%m%d')}-001"

        # Collecter les patterns consultés et heuristiques activées
        patterns_consulted = []
        heuristics_applied = []

        if self.report.consolidation_report:
            for r in self.report.consolidation_report.reinforcements:
                if r.target_type == "pattern" and r.target_id not in patterns_consulted:
                    patterns_consulted.append(r.target_id)
                elif r.target_type == "heuristic" and r.target_id not in heuristics_applied:
                    heuristics_applied.append(r.target_id)

        # Collecter les triggers de consolidation
        consolidation_triggers = []
        if self.report.consolidation_report and self.report.consolidation_report.has_actions:
            consolidation_triggers.append({
                "reason": f"{len(self.report.consolidation_report.reinforcements)} reinforcements detected",
                "action": "Cognitive cycle consolidation phase",
            })

        # Collecter les patterns observés
        patterns_observed = []
        if self.report.consolidation_report:
            for c in self.report.consolidation_report.new_pattern_candidates:
                patterns_observed.append({
                    "description": c.description,
                    "candidate_pattern": None,
                    "is_new_pattern": True,
                })

        record = {
            "session_id": session_id,
            "started_at": now.isoformat() + "Z",
            "ended_at": None,
            "status": "active",
            "context": {
                "project": "argus",
                "trigger": "scheduled",
                "user_role": None,
                "objective": "Cycle cognitif automatique — maintenance mémoire",
            },
            "focus": {
                "files_in_scope": [],
                "active_analyses": [],
                "pending_decisions": [],
                "identified_risks": [],
            },
            "scratchpad": {
                "hypotheses": [],
                "intermediate_results": [],
                "notes": [
                    f"Cycle cognitif exécuté en mode {self.report.mode}",
                    f"Résultat global: {self.report.overall_status}",
                ],
            },
            "session_decisions": [],
            "knowledge_activated": {
                "patterns_consulted": patterns_consulted,
                "heuristics_applied": heuristics_applied,
                "project_context_loaded": True,
            },
            "archival": {
                "episodes_to_record": [],
                "patterns_observed": patterns_observed,
                "associations_discovered": [],
                "consolidation_triggers": consolidation_triggers,
            },
        }

        return record

    def save_session(self, session_id: Optional[str] = None) -> Path:
        """
        Sauvegarde la session de travail dans memory/working/.
        
        Returns:
            Path du fichier créé.
        """
        record = self.create_session_record(session_id)
        sid = record["session_id"]
        
        working_dir = self.base_dir / "memory" / "working"
        working_dir.mkdir(parents=True, exist_ok=True)

        filename = f"{sid.lower().replace('-', '_')}.yaml"
        filepath = working_dir / filename
        filepath.write_text(
            yaml.dump(record, default_flow_style=False, allow_unicode=True, sort_keys=False),
            encoding="utf-8",
        )
        self.log(f"\n  📝 Session saved: {filepath.relative_to(self.base_dir)}")
        return filepath


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

def main():
    logging.basicConfig(level=logging.INFO, format='%(message)s')
    parser = argparse.ArgumentParser(
        description="Argus — Cognitive Cycle Orchestrator"
    )
    parser.add_argument(
        "--apply", action="store_true",
        help="Apply consolidation and decay (default: scan/dry-run)"
    )
    parser.add_argument(
        "--dry-run", action="store_true",
        help="Scan only, do not apply (same as no --apply)"
    )
    parser.add_argument(
        "--phase", type=str, default=None,
        choices=VALID_PHASES,
        help="Run a single phase instead of full cycle"
    )
    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(
        "--save-session", action="store_true",
        help="Save a working session record after the cycle"
    )
    parser.add_argument(
        "--base-dir", type=str, default=None,
        help="Project root directory (default: auto-detect)"
    )
    parser.add_argument(
        "--reference-date", type=str, default=None,
        help="Reference date for decay/reflection (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")

    phases = [args.phase] if args.phase else None
    apply = args.apply and not args.dry_run

    cycle = CognitiveCycle(base_dir, verbose=verbose, reference_date=ref_date)

    try:
        report = cycle.run(phases=phases, apply=apply)
    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)

    # Sauvegarder la session si demandé
    if args.save_session:
        try:
            cycle.save_session()
        except Exception as e:
            if not args.quiet:
                print(f"\n  ⚠ Session save failed: {e}", file=sys.stderr)

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