"""ARIS Reactor Engine — Event-driven micro-agents that learn.

A Reactor is the 5th component type in ARIS:
- Skill: static prompt (instructs, doesn't execute)
- Agent: full Claude session (heavy, expensive)
- Script: bash/python (no intelligence)
- MCP Tool: function (reactive, waits to be called)
- Reactor: event-driven micro-agent (proactive, lightweight, learns)

Architecture:
  TRIGGER → CONDITION → ACTION → VERIFY → LEARN

Reactors run on:
- W3 (local cron-based, 24/7 sentinel)
- Supabase Edge Functions (cloud webhook-based)
- W2 (heavy compute when needed)
"""

import json
import os
import time
import asyncio
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Optional


@dataclass
class ReactorEvent:
    """An event that triggered a reactor."""
    source: str          # "cron", "webhook", "threshold", "pattern", "manual"
    name: str            # "guardian_alert", "stripe_payment", "skill_unused"
    data: dict[str, Any] = field(default_factory=dict)
    timestamp: float = field(default_factory=time.time)


@dataclass
class ReactorResult:
    """Result of a reactor execution."""
    success: bool
    action_taken: str
    details: dict[str, Any] = field(default_factory=dict)
    learned: Optional[str] = None  # What the reactor learned
    next_action: Optional[str] = None  # Chain to next reactor


class Reactor:
    """Base reactor class."""

    def __init__(self, name: str, description: str, trigger_type: str) -> None:
        self.name = name
        self.description = description
        self.trigger_type = trigger_type
        self._history: list[dict[str, Any]] = []
        self._config: dict[str, Any] = {}
        self._base = Path(os.path.expanduser("~/.aris/reactors"))
        self._base.mkdir(parents=True, exist_ok=True)
        self._load_state()

    def _load_state(self) -> None:
        state_file = self._base / f"{self.name}.json"
        if state_file.exists():
            try:
                data = json.loads(state_file.read_text())
                self._history = data.get("history", [])
                self._config = data.get("config", {})
            except json.JSONDecodeError:
                pass

    def _save_state(self) -> None:
        state_file = self._base / f"{self.name}.json"
        # Keep last 100 history entries
        self._history = self._history[-100:]
        state_file.write_text(json.dumps({
            "name": self.name,
            "history": self._history,
            "config": self._config,
            "updated_at": time.time(),
        }, indent=2))

    def _record(self, event: ReactorEvent, result: ReactorResult) -> None:
        self._history.append({
            "event": {"source": event.source, "name": event.name, "data": event.data},
            "result": {"success": result.success, "action": result.action_taken,
                       "learned": result.learned},
            "timestamp": time.time(),
        })
        self._save_state()

    async def should_fire(self, event: ReactorEvent) -> bool:
        """Condition check — should this reactor fire for this event?"""
        return True

    async def execute(self, event: ReactorEvent) -> ReactorResult:
        """Main action — override in subclasses."""
        raise NotImplementedError

    async def verify(self, result: ReactorResult) -> bool:
        """Verify the action worked."""
        return result.success

    async def learn(self, event: ReactorEvent, result: ReactorResult) -> Optional[str]:
        """Learn from the result — update config for future runs."""
        return None

    async def run(self, event: ReactorEvent) -> ReactorResult:
        """Full reactor cycle: TRIGGER → CONDITION → ACTION → VERIFY → LEARN."""
        if not await self.should_fire(event):
            return ReactorResult(success=False, action_taken="skipped_condition")

        result = await self.execute(event)
        verified = await self.verify(result)

        if not verified:
            result.details["verification_failed"] = True

        learned = await self.learn(event, result)
        result.learned = learned

        self._record(event, result)
        return result


# ============================================================
# CONCRETE REACTORS
# ============================================================

class GuardianReactor(Reactor):
    """Monitors cluster health and auto-remediates."""

    def __init__(self) -> None:
        super().__init__("guardian", "Auto-remediate cluster health issues", "cron")
        self._config.setdefault("ram_threshold", 85)
        self._config.setdefault("cpu_threshold", 90)
        self._config.setdefault("disk_threshold", 85)
        self._config.setdefault("auto_remediate", True)

    async def should_fire(self, event: ReactorEvent) -> bool:
        return event.name in ("health_check", "manual", "threshold_breach")

    async def execute(self, event: ReactorEvent) -> ReactorResult:
        from .guardian import guardian_scan
        nodes = event.data.get("nodes", [
            {"name": "W2", "ssh": "w2", "gpu": True},
            {"name": "W3", "ssh": "w3", "gpu": False},
        ])

        report = await guardian_scan(nodes)
        alerts = report.get("alerts", [])

        if not alerts:
            return ReactorResult(True, "healthy", {"report": report})

        actions_taken = []
        if self._config.get("auto_remediate"):
            for alert in alerts:
                if "RAM" in alert and "critical" in alert.lower():
                    # Auto-clear caches
                    node = alert.split("]")[0].replace("[", "")
                    try:
                        proc = await asyncio.create_subprocess_exec(
                            "ssh", node.lower(), "sync; echo 3 > /proc/sys/vm/drop_caches 2>/dev/null; docker system prune -f 2>/dev/null",
                            stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
                        )
                        await proc.communicate()
                        actions_taken.append(f"Cleared caches on {node}")
                    except Exception as e:
                        actions_taken.append(f"Failed to clear caches on {node}: {e}")

                if "Disk" in alert and "critical" in alert.lower():
                    node = alert.split("]")[0].replace("[", "")
                    try:
                        proc = await asyncio.create_subprocess_exec(
                            "ssh", node.lower(), "docker system prune -af --volumes 2>/dev/null; journalctl --vacuum-size=100M 2>/dev/null",
                            stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
                        )
                        await proc.communicate()
                        actions_taken.append(f"Cleaned disk on {node}")
                    except Exception as e:
                        actions_taken.append(f"Failed to clean disk on {node}: {e}")

        return ReactorResult(
            success=len(actions_taken) > 0 or len(alerts) == 0,
            action_taken="auto_remediate" if actions_taken else "alert_only",
            details={"alerts": alerts, "actions": actions_taken, "health": report["overall_health"]},
        )

    async def learn(self, event: ReactorEvent, result: ReactorResult) -> Optional[str]:
        """Adjust thresholds based on history."""
        # Count false positives (alerts that resolved themselves)
        recent = self._history[-10:]
        false_positives = sum(1 for h in recent
                            if h["result"]["action"] == "alert_only"
                            and h["result"]["success"])

        if false_positives > 5:
            # Too many alerts that resolved themselves — raise threshold
            old_thresh = self._config["ram_threshold"]
            self._config["ram_threshold"] = min(95, old_thresh + 5)
            return f"Raised RAM threshold from {old_thresh}% to {self._config['ram_threshold']}% (too many false positives)"
        return None


class CostReactor(Reactor):
    """Monitors AI spending and prevents overruns."""

    def __init__(self) -> None:
        super().__init__("cost", "Prevent AI cost overruns", "threshold")
        self._config.setdefault("daily_limit", 5.0)
        self._config.setdefault("monthly_limit", 50.0)

    async def execute(self, event: ReactorEvent) -> ReactorResult:
        from .budget import BudgetTracker
        budget = BudgetTracker()
        report = budget.get_report()

        daily = report["api_spend"]["today"]
        monthly = report["api_spend"]["this_month"]
        within = report["within_budget"]

        if not within:
            return ReactorResult(
                success=True,
                action_taken="budget_exceeded",
                details={"daily": daily, "monthly": monthly, "action": "block_api_calls"},
            )

        # Warning at 80%
        daily_pct = (daily / self._config["daily_limit"] * 100) if self._config["daily_limit"] > 0 else 0
        monthly_pct = (monthly / self._config["monthly_limit"] * 100) if self._config["monthly_limit"] > 0 else 0

        if daily_pct > 80 or monthly_pct > 80:
            return ReactorResult(
                success=True,
                action_taken="budget_warning",
                details={"daily_pct": round(daily_pct, 1), "monthly_pct": round(monthly_pct, 1)},
            )

        return ReactorResult(True, "within_budget", {"daily": daily, "monthly": monthly})


class SkillOptimizer(Reactor):
    """Analyzes skill usage and suggests retirements/improvements."""

    def __init__(self) -> None:
        super().__init__("skill-optimizer", "Retire unused skills, improve popular ones", "cron")
        self._config.setdefault("retirement_days", 30)
        self._config.setdefault("min_uses_to_keep", 3)

    async def execute(self, event: ReactorEvent) -> ReactorResult:
        from .analytics_store import AnalyticsStore
        analytics = AnalyticsStore()
        metrics = analytics.get_metrics("30d")

        skills_used = metrics.get("skills_breakdown", {})
        total_events = metrics.get("total_events", 0)

        # Find skills never used
        from .skills_registry import SkillsRegistry
        import os
        skills_dir = os.environ.get("ARIS_SKILLS_DIR",
            str(Path(__file__).parent.parent.parent.parent / "server-skills"))
        registry = SkillsRegistry(skills_dir)
        all_skills = [s["name"] for s in registry.list_skills(None)]
        unused = [s for s in all_skills if s not in skills_used]

        # Find most popular
        popular = sorted(skills_used.items(), key=lambda x: -x[1])[:5]

        suggestions = []
        if unused:
            suggestions.append(f"RETIRE: {len(unused)} skills unused in 30 days: {', '.join(unused[:5])}")
        if popular:
            suggestions.append(f"OPTIMIZE: Top 5 most used: {', '.join(n for n, _ in popular)}")
        if total_events < 10:
            suggestions.append("CONCERN: Very low usage — check if ARIS is properly integrated")

        return ReactorResult(
            success=True,
            action_taken="analysis_complete",
            details={
                "total_skills": len(all_skills),
                "used_30d": len(skills_used),
                "unused_30d": len(unused),
                "popular": popular[:5],
                "suggestions": suggestions,
            },
        )


class SecurityReactor(Reactor):
    """Monitors for security issues and auto-patches."""

    def __init__(self) -> None:
        super().__init__("security", "Auto-detect and fix security issues", "cron")

    async def execute(self, event: ReactorEvent) -> ReactorResult:
        issues = []
        actions = []

        # Check for exposed secrets in recent git commits
        try:
            proc = await asyncio.create_subprocess_exec(
                "ssh", "w2",
                "cd ~/projects/aris-product && git log --oneline -5 --diff-filter=A -- '*.env' '*.key' '*.pem' 2>/dev/null | head -5",
                stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
            )
            stdout, _ = await proc.communicate()
            if stdout.strip():
                issues.append(f"Potential secrets in recent commits: {stdout.decode().strip()}")
        except Exception:
            pass

        # Check for GitHub security alerts
        try:
            proc = await asyncio.create_subprocess_exec(
                "ssh", "w2",
                "cd ~/projects/aris-product && gh api repos/ama2alvarez-1990/aris/code-scanning/alerts --jq '.[].rule.id' 2>/dev/null | head -5",
                stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
            )
            stdout, _ = await proc.communicate()
            alerts = stdout.decode().strip()
            if alerts:
                issues.append(f"GitHub CodeQL alerts: {alerts}")
        except Exception:
            pass

        # Check SSL cert expiry
        try:
            proc = await asyncio.create_subprocess_exec(
                "ssh", "w2",
                "echo | openssl s_client -connect aris4u.dev:443 -servername aris4u.dev 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null",
                stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
            )
            stdout, _ = await proc.communicate()
            if stdout.strip():
                actions.append(f"SSL cert: {stdout.decode().strip()}")
        except Exception:
            pass

        return ReactorResult(
            success=True,
            action_taken="security_scan",
            details={"issues": issues, "info": actions},
        )


# ============================================================
# REACTOR REGISTRY
# ============================================================

class ReactorEngine:
    """Manages and runs all reactors."""

    def __init__(self) -> None:
        self._reactors: dict[str, Reactor] = {}
        self._register_defaults()

    def _register_defaults(self) -> None:
        self.register(GuardianReactor())
        self.register(CostReactor())
        self.register(SkillOptimizer())
        self.register(SecurityReactor())

    def register(self, reactor: Reactor) -> None:
        self._reactors[reactor.name] = reactor

    def get(self, name: str) -> Optional[Reactor]:
        return self._reactors.get(name)

    def list_reactors(self) -> list[dict[str, Any]]:
        return [
            {
                "name": r.name,
                "description": r.description,
                "trigger": r.trigger_type,
                "history_count": len(r._history),
                "last_run": r._history[-1]["timestamp"] if r._history else None,
            }
            for r in self._reactors.values()
        ]

    async def fire(self, event: ReactorEvent) -> dict[str, ReactorResult]:
        """Fire all reactors that match this event."""
        results = {}
        for name, reactor in self._reactors.items():
            try:
                result = await reactor.run(event)
                results[name] = result
            except Exception as e:
                results[name] = ReactorResult(False, "error", {"error": str(e)})
        return results

    async def fire_one(self, name: str, event: ReactorEvent) -> ReactorResult:
        """Fire a specific reactor."""
        reactor = self._reactors.get(name)
        if not reactor:
            return ReactorResult(False, "not_found", {"error": f"Reactor '{name}' not found"})
        return await reactor.run(event)
