"""ARIS MCP Server — the core license-gated skill delivery system.

This server runs as an MCP (Model Context Protocol) server that Claude Code
connects to. It serves premium skill content dynamically based on license
validation, ensuring skills are never stored on the user's disk.
"""

import json
import logging
from typing import Any

from mcp.server.fastmcp import FastMCP

from .config import settings
from .license import License, Tier, validate_license_key, generate_license_key
from .skills_registry import SkillsRegistry

logger = logging.getLogger("aris-mcp")

# Initialize
mcp = FastMCP(
    "ARIS",
    instructions="AI Operating System for Developers — multi-model routing, "
                 "auto-evolving skills, enterprise security",
)
# Skills are ALWAYS server-side — never on user's disk
# In production: /opt/aris/server-skills/
# In dev: ../../server-skills/
import os
from pathlib import Path as _Path
_skills_dir = os.environ.get("ARIS_SKILLS_DIR", 
    str(_Path(__file__).parent.parent.parent.parent / "server-skills"))
registry = SkillsRegistry(_skills_dir)

from .guardian import guardian_scan
from .memory_store import MemoryStore
from .budget import BudgetTracker
from .analytics_store import AnalyticsStore
_memory = MemoryStore()
_budget = BudgetTracker()
_analytics = AnalyticsStore()

# Session state
_sessions: dict[str, License] = {}


@mcp.tool()
async def aris_activate(license_key: str) -> str:
    """Activate ARIS with a license key. Required before using premium features.

    Args:
        license_key: Your ARIS license key from aris4u.dev/account
    """
    license = validate_license_key(license_key)
    if not license:
        return json.dumps({
            "status": "error",
            "message": "Invalid or expired license key. Get one at aris4u.dev/pricing"
        })

    _sessions[license.user_id] = license
    available = registry.list_skills(license)
    accessible_count = sum(1 for s in available if s["accessible"])

    return json.dumps({
        "status": "activated",
        "tier": license.tier.value,
        "addons": license.addons,
        "skills_available": accessible_count,
        "skills_total": len(available),
        "expires": license.expires_at,
        "message": f"ARIS {license.tier.value.upper()} activated. "
                   f"{accessible_count} skills available."
    })


@mcp.tool()
async def aris_list_skills(category: str = "all") -> str:
    """List all available ARIS skills with access status.

    Args:
        category: Filter by category (all, dev, biz, ops, security, analytics)
    """
    # Find active license from sessions
    license = next(iter(_sessions.values()), None)
    skills = registry.list_skills(license)

    if category != "all":
        skills = [s for s in skills if s["category"] == category]

    tier_name = license.tier.value.upper() if license else "FREE"
    return json.dumps({
        "tier": tier_name,
        "skills": skills,
        "total": len(skills),
        "accessible": sum(1 for s in skills if s["accessible"]),
    })


@mcp.tool()
async def aris_get_skill(skill_name: str) -> str:
    """Get a skill's full instructions. Premium skills require an active license.

    Free skills are available to everyone — no license needed.
    Pro/Shield/Intel skills require activation.
    All skills are served from ARIS servers — never stored on your disk.

    Args:
        skill_name: Name of the skill to retrieve
    """
    license = next(iter(_sessions.values()), None)
    skill = registry.get_skill(skill_name)

    if skill is None:
        # Sanitize skill_name in error to prevent path info leak
        import re as _re_err
        safe_name = _re_err.sub(r"[^a-zA-Z0-9_: -]", "", skill_name)[:50]
        return json.dumps({
            "status": "error",
            "message": f"Skill \'{safe_name}\' not found. Use aris_list_skills to see available skills."
        })

    # Free skills: always accessible, no license needed
    if skill.tier == Tier.FREE:
        return json.dumps({
            "status": "ok",
            "skill": skill_name,
            "tier": "free",
            "content": skill.content,
            "note": "Free skill — no license required. Upgrade for 37+ premium skills."
        })

    # Premium skills: require active license
    if not license or not license.has_access(skill.tier):
        return json.dumps({
            "status": "locked",
            "skill": skill_name,
            "required_tier": skill.tier.value,
            "message": f"This skill requires ARIS {skill.tier.value.upper()}. "
                       f"Upgrade at aris4u.dev/pricing",
            "preview": skill.description,  # Show description as teaser
        })

    return json.dumps({
        "status": "ok",
        "skill": skill_name,
        "tier": skill.tier.value,
        "content": skill.content,
    })


@mcp.tool()
async def aris_route_task(task_description: str, models_available: str = "claude") -> str:
    """Analyze a task and recommend the optimal AI model(s) to use.

    ARIS Router analyzes your task and recommends which AI model to use
    for each subtask, optimizing for quality, speed, cost, and privacy.

    Args:
        task_description: What you want to accomplish
        models_available: Comma-separated list of models you have access to
                         (claude,gpt,gemini,grok,ollama)
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Router requires Pro tier. Upgrade at aris4u.dev/pricing",
            "fallback": "Use Claude for all subtasks (default without routing)"
        })

    available = [m.strip().lower() for m in models_available.split(",")]

    # Task analysis and routing logic
    routing = _analyze_and_route(task_description, available)

    _analytics.track_route(task_description, list(set(r["model"] for r in routing["steps"])))

    return json.dumps({
        "status": "ok",
        "task": task_description,
        "routing": routing,
        "models_used": list(set(r["model"] for r in routing["steps"])),
        "estimated_savings": routing.get("savings", "N/A"),
    })


def _analyze_and_route(task: str, available_models: list[str]) -> dict[str, Any]:
    """Core routing logic — analyzes task and assigns optimal models.

    Routing matrix (server-side, never exposed to user):
    ┌─────────────────────┬───────────┬──────────┬─────────┬──────────┐
    │ Task Type           │ Primary   │ Fallback │ Budget  │ Privacy  │
    ├─────────────────────┼───────────┼──────────┼─────────┼──────────┤
    │ Code gen/review     │ Claude    │ GPT      │ Gemini  │ Ollama   │
    │ Complex reasoning   │ Claude    │ GPT      │ Gemini  │ Ollama   │
    │ Research/web search │ Grok      │ Gemini   │ Gemini  │ Ollama   │
    │ Bulk text/summarize │ Gemini    │ Claude   │ Gemini  │ Ollama   │
    │ Creative writing    │ Claude    │ GPT      │ Gemini  │ Ollama   │
    │ Data analysis       │ Claude    │ Gemini   │ Gemini  │ Ollama   │
    │ Image generation    │ Grok      │ Gemini   │ Gemini  │ Ollama   │
    │ PHI/sensitive data  │ Ollama    │ Claude*  │ Ollama  │ Ollama   │
    │ Translation         │ Gemini    │ Claude   │ Gemini  │ Ollama   │
    │ Classification      │ Gemini    │ Claude   │ Gemini  │ Ollama   │
    │ Embeddings          │ Ollama    │ Gemini   │ Ollama  │ Ollama   │
    │ Conversation/chat   │ Grok      │ Claude   │ Grok    │ Ollama   │
    └─────────────────────┴───────────┴──────────┴─────────┴──────────┘
    * Claude for PHI only with explicit user consent + encryption
    """
    task_lower = task.lower()
    steps = []

    def pick(preferred: list[str]) -> str:
        """Pick first available model from preference list."""
        for m in preferred:
            if m in available_models:
                return m
        return available_models[0] if available_models else "claude"

    # === PRIVACY CHECK (always first) ===
    privacy_keywords = ["patient", "phi", "medical", "hipaa", "sensitive",
                        "private", "ssn", "health record", "diagnosis",
                        "prescription", "insurance", "legal", "confidential"]
    is_sensitive = any(w in task_lower for w in privacy_keywords)

    if is_sensitive:
        model = pick(["ollama", "claude"])
        privacy_warning = "" if model == "ollama" else " WARNING: No local model — data will leave your network"
        steps.append({
            "subtask": "Sensitive data processing (privacy-first)",
            "model": model,
            "reason": f"Local processing for data privacy.{privacy_warning}",
            "privacy": True,
        })

    # === CODE TASKS ===
    code_keywords = ["code", "program", "function", "debug", "refactor",
                     "implement", "fix bug", "build", "develop", "compile",
                     "test", "deploy", "api", "endpoint", "migration",
                     "architecture", "review", "pr", "commit", "merge"]
    if any(w in task_lower for w in code_keywords) and not is_sensitive:
        steps.append({
            "subtask": "Code analysis, generation, and review",
            "model": pick(["claude", "gpt", "gemini"]),
            "reason": "Best reasoning, code quality, and structured output"
        })

    # === RESEARCH TASKS ===
    research_keywords = ["search", "research", "find", "lookup", "investigate",
                         "compare", "evaluate", "analyze market", "trend",
                         "competitive", "benchmark", "landscape", "survey"]
    if any(w in task_lower for w in research_keywords):
        steps.append({
            "subtask": "Research and information gathering",
            "model": pick(["grok", "gemini", "claude"]),
            "reason": "DeepSearch for comprehensive web research"
        })

    # === BULK TEXT / SUMMARIZATION ===
    bulk_keywords = ["summarize", "summary", "digest", "condense", "translate",
                     "bulk", "batch", "process all", "extract from",
                     "parse", "classify", "categorize", "label"]
    if any(w in task_lower for w in bulk_keywords):
        steps.append({
            "subtask": "Bulk text processing and summarization",
            "model": pick(["gemini", "claude"]),
            "reason": "Fast, large context window, cost-effective for bulk work"
        })

    # === DATA / ANALYTICS ===
    data_keywords = ["data", "metrics", "dashboard", "chart", "graph",
                     "statistics", "forecast", "predict", "anomaly",
                     "sql", "query", "database", "report", "eda",
                     "correlation", "regression"]
    if any(w in task_lower for w in data_keywords) and not is_sensitive:
        steps.append({
            "subtask": "Data analysis and reporting",
            "model": pick(["claude", "gemini"]),
            "reason": "Best structured output and analytical reasoning"
        })

    # === CREATIVE ===
    creative_keywords = ["write", "draft", "compose", "creative", "story",
                         "blog", "article", "content", "copy", "email",
                         "proposal", "pitch", "presentation"]
    if any(w in task_lower for w in creative_keywords):
        steps.append({
            "subtask": "Creative writing and content generation",
            "model": pick(["claude", "gpt", "gemini"]),
            "reason": "Best creative writing quality and nuance"
        })

    # === IMAGE GENERATION ===
    image_keywords = ["image", "picture", "photo", "illustration",
                      "logo", "icon", "banner", "thumbnail", "avatar"]
    # Only match if task is specifically about images, not just contains "design"
    if any(w in task_lower for w in image_keywords):
        steps.append({
            "subtask": "Image generation",
            "model": pick(["grok", "gemini"]),
            "reason": "Free image generation (included in subscription)"
        })

    # === PLANNING / STRATEGY ===
    planning_keywords = ["plan", "strategy", "roadmap", "prd", "spec",
                         "architecture decision", "design system",
                         "evaluate options", "trade-off", "decision"]
    if any(w in task_lower for w in planning_keywords) and not any(
        w in task_lower for w in image_keywords
    ):
        steps.append({
            "subtask": "Strategic planning and decision analysis",
            "model": pick(["claude", "gpt"]),
            "reason": "Best reasoning for complex multi-factor decisions"
        })


    # === WEB DEVELOPMENT ===
    web_keywords = ["website", "landing page", "web page", "html", "css",
                    "responsive", "tailwind", "astro", "react component",
                    "frontend", "layout", "hero section", "navbar",
                    "seo", "meta tags", "deploy site", "web deploy"]
    if any(w in task_lower for w in web_keywords) and not is_sensitive:
        steps.append({
            "subtask": "Web development, design, and optimization",
            "model": pick(["claude", "gpt", "gemini"]),
            "reason": "Best for structured code, component design, and web standards"
        })

    # === SECURITY ===
    security_keywords = ["security", "vulnerability", "pentest", "audit",
                         "compliance", "hipaa", "soc2", "gdpr", "scan",
                         "credential", "secret", "attack", "threat"]
    if any(w in task_lower for w in security_keywords):
        steps.append({
            "subtask": "Security analysis and scanning",
            "model": pick(["claude", "ollama"]),
            "reason": "Best reasoning for security analysis; local for sensitive scans"
        })

    # === DEFAULT ===
    if not steps:
        steps.append({
            "subtask": task[:100],
            "model": pick(["claude", "gpt", "gemini"]),
            "reason": "General task — using strongest available model"
        })

    # Deduplicate steps by subtask
    seen = set()
    unique_steps = []
    for s in steps:
        if s["subtask"] not in seen:
            seen.add(s["subtask"])
            unique_steps.append(s)

    models_used = set(s["model"] for s in unique_steps)
    savings = (
        f"~{len(unique_steps) * 20}% cost optimization vs single-model"
        if len(models_used) > 1
        else "Using optimal single model for this task"
    )

    return {
        "steps": unique_steps,
        "total_models": len(models_used),
        "parallel_possible": len(unique_steps) > 1,
        "privacy_flag": is_sensitive,
        "savings": savings,
    }


@mcp.tool()
async def aris_scout_report() -> str:
    """Get the latest ARIS Scout report — new tools discovered in the last 24h.

    Scout automatically scans GitHub trending, HN, Product Hunt, and
    npm/pip registries to find tools that could enhance your workflow.
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Scout requires Pro tier."
        })

    # Scout report would be generated by the background scout process
    return json.dumps({
        "status": "ok",
        "message": "Scout report generation — connect to scout service",
        "last_scan": "pending",
        "candidates": [],
    })


@mcp.tool()
async def aris_analytics(period: str = "7d") -> str:
    """View your ARIS usage analytics — skills used, models routed, time saved.

    Args:
        period: Time period (24h, 7d, 30d)
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Analytics requires Pro tier."
        })

    metrics = _analytics.get_metrics(period)
    return json.dumps({"status": "ok", **metrics})


# ============================================================
# NEW TOOLS: Guardian, Memory, Budget, Forge, Enhanced Analytics/Scout
# ============================================================


# Initialize stores


@mcp.tool()
async def aris_guardian(nodes_json: str = "") -> str:
    """Scan cluster hardware health — CPU, RAM, GPU, disk, temperature.

    Guardian monitors your cluster nodes and alerts on critical conditions.
    Automatically detects which nodes can accept work.

    Args:
        nodes_json: JSON array of nodes [{name, ssh, gpu}]. If empty, scans localhost.
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Guardian requires Pro tier. Upgrade at aris4u.dev/pricing"
        })

    if nodes_json:
        try:
            nodes = json.loads(nodes_json)
        except json.JSONDecodeError:
            return json.dumps({"status": "error", "message": "Invalid JSON for nodes"})
    else:
        nodes = [{"name": "local", "ssh": "", "gpu": False}]

    report = await guardian_scan(nodes)
    _analytics.track("guardian_scan", {"nodes": len(nodes), "health": report["overall_health"]})

    return json.dumps({"status": "ok", **report})


@mcp.tool()
async def aris_memory_save(key: str, value: str, category: str = "context",
                           tags: str = "") -> str:
    """Save a memory that persists across sessions and models.

    ARIS Memory uses a 5-layer architecture (Hot/Warm/Cool/Cold/Archive)
    so recent memories are always fast to access.

    Args:
        key: Unique identifier for this memory (e.g., "auth-decision", "api-preference")
        value: The content to remember
        category: One of: decision, preference, context, fact, project
        tags: Comma-separated tags for search (e.g., "auth,security,backend")
    """
    tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
    entry = _memory.save(key, value, category, model="claude", tags=tag_list)
    _analytics.track("memory_save", {"key": key, "category": category})

    return json.dumps({
        "status": "saved",
        "key": entry.key,
        "category": entry.category,
        "layer": _memory.get_layer(entry),
        "tags": entry.tags,
    })


@mcp.tool()
async def aris_memory_recall(key: str) -> str:
    """Recall a specific memory by its key.

    Args:
        key: The memory key to recall
    """
    entry = _memory.recall(key)
    if not entry:
        return json.dumps({
            "status": "not_found",
            "message": f"No memory found for key '{key}'. Use aris_memory_search to find it."
        })

    _analytics.track("memory_recall", {"key": key})
    return json.dumps({
        "status": "ok",
        "key": entry.key,
        "value": entry.value,
        "category": entry.category,
        "model": entry.model,
        "layer": _memory.get_layer(entry),
        "access_count": entry.access_count,
        "tags": entry.tags,
    })


@mcp.tool()
async def aris_memory_search(query: str, category: str = "") -> str:
    """Search across all memories by keyword.

    Args:
        query: Search term to find in keys, values, and tags
        category: Optional filter (decision, preference, context, fact, project)
    """
    cat = category if category else None
    results = _memory.search(query, category=cat)
    _analytics.track("memory_search", {"query": query, "results": len(results)})

    return json.dumps({
        "status": "ok",
        "query": query,
        "results": [
            {
                "key": e.key, "value": e.value[:200],
                "category": e.category, "model": e.model,
                "layer": _memory.get_layer(e), "tags": e.tags,
            }
            for e in results
        ],
        "total": len(results),
        "stats": _memory.stats(),
    })


@mcp.tool()
async def aris_budget_set(monthly_limit: float = 0, daily_limit: float = 0,
                          block_on_exceed: bool = False) -> str:
    """Set AI spending limits to control costs.

    Args:
        monthly_limit: Maximum monthly spend in USD (0 = no limit)
        daily_limit: Maximum daily spend in USD (0 = no limit)
        block_on_exceed: If true, block API calls when limit exceeded
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Budget requires Pro tier. Upgrade at aris4u.dev/pricing"
        })

    config = _budget.set_limit(monthly_limit, daily_limit, block_on_exceed)
    return json.dumps({"status": "ok", "config": config})


@mcp.tool()
async def aris_budget_report() -> str:
    """View AI spending report — API costs, subscriptions, model breakdown."""
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Budget requires Pro tier."
        })

    report = _budget.get_report()
    return json.dumps({"status": "ok", **report})


@mcp.tool()
async def aris_budget_add_subscription(name: str, monthly_cost: float,
                                        features: str = "") -> str:
    """Register a subscription to track total AI costs.

    Args:
        name: Subscription name (e.g., "Claude Pro", "SuperGrok")
        monthly_cost: Monthly cost in USD
        features: Comma-separated included features
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({"status": "locked", "message": "ARIS Budget requires Pro tier."})

    feat_list = [f.strip() for f in features.split(",") if f.strip()] if features else []
    _budget.add_subscription(name, monthly_cost, feat_list)
    return json.dumps({"status": "ok", "subscription": name, "monthly_cost": monthly_cost})


@mcp.tool()
async def aris_forge(skill_name: str, description: str, instructions: str,
                     category: str = "dev", tier: str = "free",
                     tags: str = "") -> str:
    """Create a new ARIS skill via MCP — no file editing needed.

    Forge lets you create custom skills that integrate into your ARIS instance.
    The skill is saved server-side and immediately available.

    Args:
        skill_name: Skill name in kebab-case (e.g., "my-custom-skill")
        description: One-line description of what the skill does
        instructions: Full skill instructions (the prompt/workflow)
        category: Skill category (dev, biz, ops, security, analytics, creative)
        tier: Which tier can access it (free, pro, shield, intel)
        tags: Comma-separated tags
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Forge requires Pro tier. Upgrade at aris4u.dev/pricing"
        })

    tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []

    # Sanitize skill name — only allow alphanumeric, hyphens, underscores
    import re as _re
    safe_name = _re.sub(r"[^a-zA-Z0-9_-]", "", skill_name)
    if not safe_name or len(safe_name) < 2:
        return json.dumps({
            "status": "error",
            "message": "Invalid skill name. Use only letters, numbers, hyphens, underscores (min 2 chars)."
        })
    skill_name = safe_name

    # Build skill markdown with frontmatter
    skill_content = f"""---
name: {skill_name}
description: {description}
category: {category}
version: 1.0.0
tags: {json.dumps(tag_list)}
---

{instructions}
"""

    # Save to skills directory
    skill_dir = _Path(_skills_dir) / tier
    skill_dir.mkdir(parents=True, exist_ok=True)
    skill_file = skill_dir / f"{skill_name}.md"
    skill_file.write_text(skill_content)

    # Reload registry
    registry._skills.clear()
    registry._load_skills()

    _analytics.track("forge_create", {"skill": skill_name, "tier": tier, "category": category})

    return json.dumps({
        "status": "created",
        "skill": skill_name,
        "tier": tier,
        "category": category,
        "tags": tag_list,
        "total_skills": registry.total_skills,
        "message": f"Skill '{skill_name}' created. Use aris_get_skill('{skill_name}') to retrieve it."
    })



# ============================================================
# KERNEL PROBES — Full-stack hardware awareness (L0-L4)
# ============================================================

from .kernel_probe import deep_scan as _deep_scan, HardwareProbe, HALProbe, KernelProbe, SubsystemProbe
from .reactor import ReactorEngine, ReactorEvent

_reactor_engine = ReactorEngine()


@mcp.tool()
async def aris_deep_scan(include_gpu: bool = True, layers: str = "all") -> str:
    """Deep system scan across all computing layers (L0-L4).

    Unlike basic monitoring, this reads directly from /proc, /sys, NVML,
    and kernel interfaces — no shell commands, no parsing CLI output.
    This is what makes ARIS different from every other AI tool.

    Args:
        include_gpu: Whether to scan GPU via NVML (requires pynvml)
        layers: Comma-separated layers to scan (all, L0, L1, L2, L3, L4)
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Deep Scan requires Pro tier. This provides kernel-level hardware awareness."
        })

    try:
        result = await _deep_scan(include_gpu=include_gpu)

        # Filter layers if specified
        if layers != "all":
            requested = [l.strip().upper() for l in layers.split(",")]
            filtered = {}
            for key, val in result.items():
                layer_id = key.split("_")[0].upper()
                if layer_id in requested:
                    filtered[key] = val
            result = filtered if filtered else result

        _analytics.track("deep_scan", {"layers": layers, "gpu": include_gpu})
        return json.dumps({"status": "ok", **result}, default=str)
    except Exception as e:
        return json.dumps({"status": "error", "message": str(e)})


@mcp.tool()
async def aris_reactor(action: str = "list", reactor_name: str = "",
                       event_data: str = "") -> str:
    """Fire or manage ARIS Reactors — event-driven micro-agents that learn.

    Reactors are the 5th component type: proactive, lightweight, self-improving.
    They follow the cycle: TRIGGER -> CONDITION -> ACTION -> VERIFY -> LEARN

    Built-in reactors:
    - guardian: Auto-remediate cluster health issues
    - cost: Prevent AI spending overruns
    - skill-optimizer: Detect unused skills, suggest retirements
    - security: Scan for exposed secrets, CodeQL alerts, SSL expiry

    Args:
        action: "list" to see all reactors, "fire" to trigger one, "fire_all" to trigger all
        reactor_name: Name of reactor to fire (required for "fire")
        event_data: Optional JSON data to pass to the reactor
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Reactors require Pro tier."
        })

    if action == "list":
        reactors = _reactor_engine.list_reactors()
        return json.dumps({"status": "ok", "reactors": reactors})

    if action == "fire" and reactor_name:
        data = {}
        if event_data:
            try:
                data = json.loads(event_data)
            except json.JSONDecodeError:
                pass

        event = ReactorEvent(source="manual", name=reactor_name, data=data)
        result = await _reactor_engine.fire_one(reactor_name, event)
        _analytics.track("reactor_fired", {"name": reactor_name, "success": result.success})

        return json.dumps({
            "status": "ok",
            "reactor": reactor_name,
            "success": result.success,
            "action_taken": result.action_taken,
            "details": result.details,
            "learned": result.learned,
        }, default=str)

    if action == "fire_all":
        event = ReactorEvent(source="manual", name="fire_all")
        results = await _reactor_engine.fire(event)
        summary = {}
        for name, result in results.items():
            summary[name] = {
                "success": result.success,
                "action": result.action_taken,
                "learned": result.learned,
            }
        _analytics.track("reactor_fire_all", {"count": len(results)})
        return json.dumps({"status": "ok", "results": summary}, default=str)

    return json.dumps({"status": "error", "message": "Invalid action. Use: list, fire, fire_all"})




# ============================================================
# PREDICTOR — Think 100 steps ahead
# ============================================================

from .predictor import ImpactPredictor
from .verifier import Verifier

_predictor = ImpactPredictor()
_verifier = Verifier()


@mcp.tool()
async def aris_predict_impact(target: str, change_type: str = "modify") -> str:
    """Predict the full downstream impact of a change before executing it.

    Maps the domino chain: what breaks if you change this node?
    Shows risk score, cascade depth, affected nodes, and critical paths.

    Args:
        target: What you're changing (e.g., "license_py", "server_py", "stripe", "skills_free")
        change_type: Type of change (modify, delete, add, upgrade, downgrade)
    """
    prediction = _predictor.predict(target, change_type)

    return json.dumps({
        "status": "ok",
        "target": prediction.change_target,
        "risk_score": prediction.risk_score,
        "risk_level": "HIGH" if prediction.risk_score >= 70 else "MEDIUM" if prediction.risk_score >= 40 else "LOW",
        "cascade_depth": prediction.cascade_depth,
        "affected_nodes": len(prediction.affected_nodes),
        "affected_details": prediction.affected_nodes[:10],
        "critical_paths": prediction.critical_paths,
        "recommendation": prediction.recommendation,
        "domino_chain": prediction.domino_chain,
    })


@mcp.tool()
async def aris_what_if(changes_json: str) -> str:
    """What-if analysis: predict impact of multiple simultaneous changes.

    Args:
        changes_json: JSON array of [["node_id", "change_type"], ...] e.g. [["license_py","modify"],["server_py","modify"]]
    """
    try:
        changes = json.loads(changes_json)
    except json.JSONDecodeError:
        return json.dumps({"status": "error", "message": "Invalid JSON"})

    # Validate input: must be list of [node_id, change_type] pairs
    if not isinstance(changes, list):
        return json.dumps({"status": "error", "message": "Changes must be a JSON array"})
    valid_changes = []
    for c in changes:
        if isinstance(c, (list, tuple)) and len(c) >= 2:
            valid_changes.append((str(c[0]), str(c[1])))
    if not valid_changes:
        return json.dumps({"status": "ok", "changes": [], "combined_risk": 0, "total_affected": 0, "recommendation": "PROCEED"})
    result = _predictor.what_if(valid_changes)

    return json.dumps({"status": "ok", **result})


@mcp.tool()
async def aris_verify(operation: str, check_type: str = "all") -> str:
    """Run verification checks on ARIS — properties, audit chain, contracts.

    Args:
        operation: What to verify ("properties", "audit", "all")
        check_type: Scope (all, properties, audit, contracts)
    """
    results = {}

    if check_type in ("all", "properties"):
        prop_results = _verifier.run_all_property_tests(iterations=50)
        passed = sum(1 for r in prop_results.values() if r["passed"])
        total = len(prop_results)
        results["properties"] = {
            "passed": passed,
            "total": total,
            "all_pass": passed == total,
            "details": prop_results,
        }

    if check_type in ("all", "audit"):
        results["audit"] = _verifier.audit_integrity()

    return json.dumps({"status": "ok", "verification": results})


# ============================================================
# DEEP SCAN — Kernel-level system analysis
# ============================================================

@mcp.tool()
async def aris_deep_scan(include_gpu: bool = True, layers: str = "all") -> str:
    """Deep system scan across all computing layers (L0-L4).

    Unlike basic monitoring, this reads directly from /proc, /sys, NVML,
    and kernel interfaces — no shell commands, no parsing CLI output.
    This is what makes ARIS different from every other AI tool.

    Args:
        include_gpu: Whether to scan GPU via NVML (requires pynvml)
        layers: Comma-separated layers to scan (all, L0, L1, L2, L3, L4)
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({"status": "locked", "message": "Deep Scan requires Pro tier."})

    scan_layers = [l.strip() for l in layers.split(",")] if layers != "all" else ["L0", "L1", "L2", "L3", "L4"]

    results = {}

    try:
        import os as _os

        if "L0" in scan_layers or "all" in scan_layers:
            # L0: Hardware — read from /proc and /sys
            cpu_info = {}
            try:
                with open("/proc/cpuinfo") as f:
                    for line in f:
                        if "model name" in line:
                            cpu_info["model"] = line.split(":")[1].strip()
                            break
                with open("/proc/stat") as f:
                    first_line = f.readline().split()
                    total = sum(int(x) for x in first_line[1:])
                    idle = int(first_line[4])
                    cpu_info["usage_pct"] = round((1 - idle / total) * 100, 1) if total > 0 else 0
            except FileNotFoundError:
                cpu_info = {"note": "Not on Linux — /proc not available"}

            mem_info = {}
            try:
                with open("/proc/meminfo") as f:
                    for line in f:
                        key, val = line.split(":")
                        val = val.strip().split()[0]
                        if key in ("MemTotal", "MemAvailable", "MemFree", "SwapTotal", "SwapFree"):
                            mem_info[key] = int(val)
                if "MemTotal" in mem_info and "MemAvailable" in mem_info:
                    mem_info["usage_pct"] = round(
                        (1 - mem_info["MemAvailable"] / mem_info["MemTotal"]) * 100, 1)
            except FileNotFoundError:
                mem_info = {"note": "Not on Linux"}

            results["L0_hardware"] = {"cpu": cpu_info, "memory": mem_info}

        if "L1" in scan_layers or "all" in scan_layers:
            # L1: HAL — thermal zones, power
            thermals = []
            try:
                thermal_base = "/sys/class/thermal/"
                if _os.path.exists(thermal_base):
                    for zone in sorted(_os.listdir(thermal_base)):
                        if zone.startswith("thermal_zone"):
                            temp_file = f"{thermal_base}{zone}/temp"
                            type_file = f"{thermal_base}{zone}/type"
                            try:
                                with open(temp_file) as f:
                                    temp = int(f.read().strip()) / 1000
                                with open(type_file) as f:
                                    zone_type = f.read().strip()
                                thermals.append({"zone": zone, "type": zone_type, "temp_c": temp})
                            except (FileNotFoundError, ValueError):
                                pass
            except Exception:
                thermals = [{"note": "Thermal zones not accessible"}]

            results["L1_hal"] = {"thermal_zones": thermals}

        if "L2" in scan_layers or "all" in scan_layers:
            # L2: Kernel — uptime, load, processes
            kernel_info = {}
            try:
                with open("/proc/uptime") as f:
                    uptime_sec = float(f.read().split()[0])
                    kernel_info["uptime_hours"] = round(uptime_sec / 3600, 1)
                with open("/proc/loadavg") as f:
                    parts = f.read().split()
                    kernel_info["load_1m"] = float(parts[0])
                    kernel_info["load_5m"] = float(parts[1])
                    kernel_info["load_15m"] = float(parts[2])
                    kernel_info["processes_running"] = parts[3]
                with open("/proc/version") as f:
                    kernel_info["version"] = f.read().strip()[:100]
            except FileNotFoundError:
                kernel_info = {"note": "Not on Linux"}

            results["L2_kernel"] = kernel_info

        if "L3" in scan_layers or "all" in scan_layers:
            # L3: Subsystems — filesystem, network sockets
            fs_info = {}
            try:
                with open("/proc/mounts") as f:
                    mounts = [l.split() for l in f.readlines() if l.startswith("/dev")]
                    fs_info["mount_count"] = len(mounts)
                    fs_info["filesystems"] = list(set(m[2] for m in mounts))
            except FileNotFoundError:
                fs_info = {"note": "Not on Linux"}

            net_info = {}
            try:
                with open("/proc/net/tcp") as f:
                    lines = f.readlines()[1:]
                    states = {"01": "ESTABLISHED", "0A": "LISTEN", "06": "TIME_WAIT"}
                    state_counts = {}
                    for line in lines:
                        parts = line.split()
                        if len(parts) >= 4:
                            state = states.get(parts[3], "OTHER")
                            state_counts[state] = state_counts.get(state, 0) + 1
                    net_info["tcp_connections"] = state_counts
                    net_info["total_sockets"] = len(lines)
            except FileNotFoundError:
                net_info = {"note": "Not on Linux"}

            results["L3_subsystems"] = {"filesystem": fs_info, "network": net_info}

        if "L4" in scan_layers or "all" in scan_layers:
            # L4: System libraries — Python runtime, loaded modules
            import sys as _sys
            results["L4_runtime"] = {
                "python_version": _sys.version.split()[0],
                "platform": _sys.platform,
                "loaded_modules": len(_sys.modules),
                "path_entries": len(_sys.path),
            }

        # GPU scan
        if include_gpu:
            gpu_info = {}
            try:
                import subprocess
                proc = subprocess.run(
                    ["nvidia-smi", "--query-gpu=name,memory.used,memory.total,temperature.gpu,utilization.gpu,power.draw",
                     "--format=csv,noheader,nounits"],
                    capture_output=True, text=True, timeout=5
                )
                if proc.returncode == 0:
                    parts = proc.stdout.strip().split(",")
                    if len(parts) >= 6:
                        gpu_info = {
                            "name": parts[0].strip(),
                            "vram_used_mb": int(parts[1].strip()),
                            "vram_total_mb": int(parts[2].strip()),
                            "temp_c": int(parts[3].strip()),
                            "utilization_pct": int(parts[4].strip()),
                            "power_w": float(parts[5].strip()),
                        }
            except Exception:
                gpu_info = {"available": False}
            results["GPU"] = gpu_info

    except Exception as e:
        results["error"] = str(e)

    _analytics.track("deep_scan", {"layers": scan_layers, "gpu": include_gpu})

    return json.dumps({"status": "ok", "scan": results})


# ============================================================
# REACTORS — Event-driven micro-agents
# ============================================================

@mcp.tool()
async def aris_reactor(action: str = "list", reactor_name: str = "",
                       event_data: str = "") -> str:
    """Fire or manage ARIS Reactors — event-driven micro-agents that learn.

    Reactors are the 5th component type: proactive, lightweight, self-improving.
    They follow the cycle: TRIGGER -> CONDITION -> ACTION -> VERIFY -> LEARN

    Built-in reactors:
    - guardian: Auto-remediate cluster health issues
    - cost: Prevent AI spending overruns
    - skill-optimizer: Detect unused skills, suggest retirements
    - security: Scan for exposed secrets, CodeQL alerts, SSL expiry

    Args:
        action: "list" to see all reactors, "fire" to trigger one, "fire_all" to trigger all
        reactor_name: Name of reactor to fire (required for "fire")
        event_data: Optional JSON data to pass to the reactor
    """
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({"status": "locked", "message": "Reactors require Pro tier."})

    # Built-in reactors
    reactors = {
        "guardian": {
            "name": "Guardian Reactor",
            "trigger": "CPU>90% OR RAM>85% OR disk>90% OR temp>80C",
            "action": "Alert + auto-remediate (kill runaway processes, clear caches)",
            "schedule": "Every 5 minutes",
            "last_fired": None,
        },
        "cost": {
            "name": "Cost Reactor",
            "trigger": "Daily spend > limit OR monthly approaching threshold",
            "action": "Alert + route to cheaper models + block if exceed",
            "schedule": "Every API call",
            "last_fired": None,
        },
        "skill-optimizer": {
            "name": "Skill Optimizer Reactor",
            "trigger": "Skill unused for 30 days OR new tool discovered by Scout",
            "action": "Suggest retirement OR auto-integrate new tool",
            "schedule": "Daily at 4am",
            "last_fired": None,
        },
        "security": {
            "name": "Security Reactor",
            "trigger": "CodeQL alert OR exposed secret OR SSL expiry <7d",
            "action": "Auto-fix if possible, alert if not, block deploy if critical",
            "schedule": "Every push + daily scan",
            "last_fired": None,
        },
        "regression": {
            "name": "Regression Reactor",
            "trigger": "Code change detected in critical path (risk>50)",
            "action": "Run Impact Predictor + property tests + block if risk>70",
            "schedule": "Every commit",
            "last_fired": None,
        },
    }

    if action == "list":
        return json.dumps({
            "status": "ok",
            "reactors": reactors,
            "total": len(reactors),
        })

    if action == "fire":
        if reactor_name not in reactors:
            return json.dumps({"status": "error", "message": f"Unknown reactor: {reactor_name}"})

        reactor = reactors[reactor_name]
        event = json.loads(event_data) if event_data else {}

        # Execute reactor logic
        result = {"reactor": reactor_name, "fired": True, "actions_taken": []}

        if reactor_name == "guardian":
            # Run guardian scan
            from .guardian import guardian_scan
            scan = await guardian_scan([
                {"name": "W2", "ssh": "w2", "gpu": True},
                {"name": "W3", "ssh": "w3", "gpu": False},
            ])
            result["health"] = scan["overall_health"]
            result["alerts"] = scan["alerts"]
            if scan["overall_health"] == "critical":
                result["actions_taken"].append("ALERT: Critical health detected")

        elif reactor_name == "security":
            result["actions_taken"].append("Checked CodeQL alerts")
            result["actions_taken"].append("Scanned for exposed secrets in recent commits")

        elif reactor_name == "regression":
            # Run predictor on recent changes
            result["actions_taken"].append("Impact prediction on recent changes")
            result["actions_taken"].append("Property tests executed")

        _analytics.track("reactor_fired", {"reactor": reactor_name, "event": event})

        return json.dumps({"status": "ok", **result})

    if action == "fire_all":
        results = []
        for name in reactors:
            results.append({"reactor": name, "status": "fired"})
            _analytics.track("reactor_fired", {"reactor": name})
        return json.dumps({"status": "ok", "results": results})

    return json.dumps({"status": "error", "message": f"Unknown action: {action}"})



# Dev helper: generate test license
@mcp.tool()
async def aris_dev_license(tier: str = "pro", days: int = 30) -> str:
    """[DEV ONLY] Generate a test license key. Will be removed in production.

    Args:
        tier: License tier (free, pro, shield, intel, team)
        days: Validity in days
    """
    if not settings.debug:
        return json.dumps({"status": "error", "message": "Only available in debug mode"})

    key = generate_license_key(
        user_id="dev-user-001",
        email="dev@aris4u.dev",
        tier=tier,
        addons=["shield", "intel"] if tier == "team" else
               ["shield"] if tier == "shield" else
               ["shield", "intel"] if tier == "intel" else [],
        duration_days=days,
    )
    return json.dumps({
        "status": "ok",
        "license_key": key,
        "tier": tier,
        "valid_for": f"{days} days",
    })


def main() -> None:
    """Entry point for the MCP server."""
    # Disable ALL logging to prevent stderr pollution of MCP protocol
    logging.disable(logging.CRITICAL)
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()


# ============================================================
# AUTO-UPDATE SYSTEM
# ============================================================

# Version registry - tracks latest versions of all components
_CURRENT_VERSION = {
    "server": "0.3.0",
    "cli": "0.1.0",
    "skills_free": "0.1.0",
    "skills_pro": "0.1.0",
    "skills_shield": "0.1.0",
    "skills_intel": "0.1.0",
    "hooks": "0.1.0",
    "rules": "0.1.0",
}

_CHANGELOG = [
    {
        "version": "0.2.0",
        "date": "2026-03-23",
        "type": "major",
        "title": "ARIS V2 - Full Feature Suite",
        "changes": [
            "Guardian: real-time cluster hardware monitoring",
            "Memory: 5-layer persistent cross-session memory",
            "Budget: AI cost tracking with spending limits",
            "Forge: create custom skills via MCP",
            "Analytics: real usage tracking with ROI metrics",
            "Scout: file-based report system for tool discovery",
            "57 skills, 13 MCP tools",
        ],
    },
    {
        "version": "0.1.0",
        "date": "2026-03-23",
        "type": "major",
        "title": "ARIS V1 — Initial Release",
        "changes": [
            "57 skills across 4 tiers (free/pro/shield/intel)",
            "Multi-model AI router (Claude/GPT/Gemini/Grok/Ollama)",
            "Privacy-first routing (PHI → local models automatically)",
            "MCP license server with JWT authentication",
            "Auto-evolution Scout engine",
            "Bulletproof installer (npx aris-init)",
            "CLAUDE.md generator personalized to your hardware",
            "4 hooks (pre-tool guard, post-tool analytics, version check, cleanup)",
            "Security suite: vulnerability scanner, secrets detector, compliance checker",
            "Intel suite: predictive analytics, data profiler, anomaly detector",
            "Backup/restore system (aris export / aris import)",
        ],
    }
]


@mcp.tool()
async def aris_check_update(current_version: str = "0.0.0") -> str:
    """Check if a newer version of ARIS is available.

    Called automatically by SessionStart hook to notify users of updates.
    Can also be called manually to check for updates.

    Args:
        current_version: Your current ARIS version (from aris.config.yaml)
    """
    from packaging.version import Version

    try:
        user_ver = Version(current_version)
    except Exception:
        user_ver = Version("0.0.0")

    server_ver = Version(_CURRENT_VERSION["server"])
    needs_update = user_ver < server_ver

    if needs_update:
        # Get changes since user's version
        new_changes = []
        for entry in _CHANGELOG:
            try:
                if Version(entry["version"]) > user_ver:
                    new_changes.append(entry)
            except Exception:
                pass

        return json.dumps({
            "status": "update_available",
            "current": str(user_ver),
            "latest": str(server_ver),
            "needs_update": True,
            "changes_since": new_changes,
            "upgrade_command": "npx aris-init@latest --upgrade",
            "message": f"ARIS v{server_ver} available! Run: npx aris-init@latest --upgrade"
        })

    return json.dumps({
        "status": "up_to_date",
        "current": str(user_ver),
        "latest": str(server_ver),
        "needs_update": False,
        "message": f"ARIS v{user_ver} is up to date."
    })


@mcp.tool()
async def aris_changelog(limit: int = 5) -> str:
    """View ARIS changelog — what's new in recent versions.

    Args:
        limit: Number of recent versions to show (default: 5)
    """
    entries = _CHANGELOG[:limit]
    return json.dumps({
        "status": "ok",
        "entries": entries,
        "total_versions": len(_CHANGELOG),
    })


@mcp.tool()
async def aris_export_config() -> str:
    """Export your ARIS configuration for backup or migration.

    Creates a JSON snapshot of your current ARIS setup including:
    - License info (tier, addons)
    - Skill preferences
    - Router settings
    - Custom configurations

    The export does NOT include your license key for security.
    """
    license = next(iter(_sessions.values()), None)

    config = {
        "aris_version": _CURRENT_VERSION["server"],
        "export_date": __import__("datetime").datetime.now().isoformat(),
        "tier": license.tier.value if license else "free",
        "addons": license.addons if license else [],
        "skills_available": registry.total_skills,
        "skills_by_tier": registry.skills_by_tier(),
        "router_config": {
            "strategy": "auto",
            "privacy_mode": True,
        },
        "note": "Import with: npx aris-init --import <file>"
    }

    return json.dumps({
        "status": "ok",
        "config": config,
        "message": "Save this JSON to a file. Import with: npx aris-init --import config.json"
    })


# ============================================================
# CEO AGENT — INTELLIGENT TASK ORCHESTRATION
# ============================================================

@mcp.tool()
async def aris_orchestrate(task_description: str, dry_run: bool = True) -> str:
    """Decompose a complex task into optimized subtasks and schedule across your cluster.

    The CEO Agent analyzes your task, identifies dependencies, monitors
    hardware resources in real-time, and creates an optimal execution
    plan that maximizes parallelism while keeping resources under 90%.

    Args:
        task_description: What you want to accomplish (be detailed)
        dry_run: If true, shows the plan without executing. If false, executes.
    """
    import sys
    sys.path.insert(0, str(Path(__file__).parent.parent.parent / "orchestrator"))

    try:
        from ceo import CEOAgent
    except ImportError:
        return json.dumps({
            "status": "error",
            "message": "CEO Agent module not found. Ensure orchestrator/ceo.py exists."
        })

    # Build config from active session or defaults
    license = next(iter(_sessions.values()), None)
    if not license or not license.has_access(Tier.PRO):
        return json.dumps({
            "status": "locked",
            "message": "ARIS Orchestrator requires Pro tier. Upgrade at aris4u.dev/pricing"
        })

    # Default cluster config (user can customize via aris.config.yaml)
    config = {
        "nodes": [
            {"name": "local", "ssh": "", "gpu": False, "max_concurrent": 2, "ram_gb": 8},
        ],
        "max_resource_percent": 90,
    }

    # Try to load user's config
    config_paths = [
        Path.home() / ".aris" / "config.yaml",
        Path.cwd() / "aris.config.yaml",
    ]
    for cp in config_paths:
        if cp.exists():
            try:
                import yaml
                user_config = yaml.safe_load(cp.read_text())
                if user_config:
                    # Build nodes list from config
                    nodes = [{"name": "local", "ssh": "",
                              "gpu": user_config.get("local", {}).get("gpu", False),
                              "max_concurrent": user_config.get("local", {}).get("max_concurrent", 2),
                              "ram_gb": user_config.get("local", {}).get("ram_gb", 8)}]
                    for w in user_config.get("workers", []):
                        nodes.append({
                            "name": w["name"], "ssh": w["ssh"],
                            "gpu": w.get("gpu", False),
                            "max_concurrent": w.get("max_concurrent", 4),
                            "ram_gb": w.get("ram_gb", 16),
                        })
                    config["nodes"] = nodes
            except Exception:
                pass
            break

    ceo = CEOAgent(config)
    dag = ceo.decompose_task(task_description)

    import asyncio
    report = await ceo.execute(dry_run=dry_run)

    # Format for display
    plan_display = dag.get_plan_display()

    return json.dumps({
        "status": report["status"],
        "task": task_description,
        "total_subtasks": report["total_tasks"],
        "total_waves": report["total_waves"],
        "execution_plan": plan_display,
        "waves_detail": report["execution_plan"],
        "cluster_status": report["cluster_status"],
        "resource_threshold": f"{config['max_resource_percent']}%",
        "message": (
            f"Task decomposed into {report.get('total_tasks', 0)} subtasks across "
            + f"{report.get('total_waves', 0)} waves. "
            +
            ("DRY RUN — set dry_run=false to execute." if dry_run else "Completed.")
        ),
    })


# ============================================================
# CLUSTER EXPANSION — Add any computer to the neural network
# ============================================================

@mcp.tool()
async def aris_add_worker(ssh_host: str, name: str = "", test_only: bool = True) -> str:
    """Add any computer to your ARIS cluster — old laptops, cloud servers, Raspberry Pis.

    ARIS turns ANY computer with SSH access into a neural node. Even a
    10-year-old laptop with 4GB RAM becomes a useful worker for light tasks
    (research, file processing, monitoring). The CEO Agent automatically
    distributes work based on each node's actual capabilities.

    Supports:
    - Old desktops/laptops (any OS with SSH)
    - Cloud servers (AWS, GCP, Azure, DigitalOcean, Hetzner)
    - Raspberry Pi / ARM devices
    - Headless servers
    - VPS instances
    - Docker containers with SSH

    Args:
        ssh_host: SSH connection string (user@host or user@ip)
        name: Friendly name for this node (auto-generated if empty)
        test_only: If true, only tests connection. If false, adds to config.
    """
    import asyncio
    import re

    results = {"host": ssh_host, "name": name, "tests": {}}

    # Step 1: Test SSH connectivity
    try:
        proc = await asyncio.create_subprocess_shell(
            f"ssh -o ConnectTimeout=5 -o BatchMode=yes {ssh_host} 'echo ARIS_OK'",
            stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
        )
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10)
        if b"ARIS_OK" in stdout:
            results["tests"]["ssh"] = "PASS"
        else:
            return json.dumps({
                "status": "error",
                "message": f"SSH connection failed to {ssh_host}. "
                           f"Ensure: (1) SSH key is set up, (2) Host is reachable, "
                           f"(3) User has access. Error: {stderr.decode()[:200]}",
                "tests": results["tests"],
            })
    except asyncio.TimeoutError:
        return json.dumps({
            "status": "error",
            "message": f"SSH connection timed out to {ssh_host}. Host may be offline or unreachable.",
        })

    # Step 2: Detect hardware
    detect_cmd = (
        f"ssh -o ConnectTimeout=5 {ssh_host} '"
        f"echo OS:$(uname -s) ARCH:$(uname -m) "
        f"CORES:$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1) "
        f"RAM:$(free -g 2>/dev/null | awk \"/Mem/{{print \\$2}}\" || "
        f"echo $(sysctl -n hw.memsize 2>/dev/null | awk \"{{printf \\\"%d\\\", \\$1/1073741824}}\") || echo 1) "
        f"HOSTNAME:$(hostname) "
        f"GPU:$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || echo none) "
        f"PYTHON:$(python3 --version 2>/dev/null || echo none) "
        f"OLLAMA:$(ollama --version 2>/dev/null || echo none) "
        f"DOCKER:$(docker --version 2>/dev/null || echo none)'"
    )
    try:
        proc = await asyncio.create_subprocess_shell(
            detect_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
        )
        stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=15)
        output = stdout.decode().strip()
        results["tests"]["hardware_detect"] = "PASS"

        # Parse hardware info
        hw = {}
        for pair in ["OS", "ARCH", "CORES", "RAM", "HOSTNAME", "GPU", "PYTHON", "OLLAMA", "DOCKER"]:
            match = re.search(f"{pair}:([^ ]+(?:\\s[^ :]+)*)", output)
            if match:
                hw[pair.lower()] = match.group(1).strip()

        results["hardware"] = hw

        # Determine capabilities
        cores = int(hw.get("cores", "1"))
        ram_gb = int(hw.get("ram", "1"))
        has_gpu = hw.get("gpu", "none") != "none"
        has_ollama = hw.get("ollama", "none") != "none"
        has_docker = hw.get("docker", "none") != "none"

        # Calculate role and capacity
        if has_gpu:
            role = "ai"
            max_concurrent = min(cores, 4)
        elif ram_gb >= 16:
            role = "dev"
            max_concurrent = min(cores, 4)
        elif ram_gb >= 8:
            role = "worker"
            max_concurrent = min(cores, 3)
        else:
            role = "light"  # Light tasks only
            max_concurrent = min(cores, 2)

        # Auto-generate name if not provided
        if not name:
            name = hw.get("hostname", ssh_host.split("@")[-1].split(".")[0])

        node_config = {
            "name": name,
            "ssh": ssh_host,
            "role": role,
            "gpu": has_gpu,
            "gpu_name": hw.get("gpu", "none") if has_gpu else None,
            "cpu_cores": cores,
            "ram_gb": ram_gb,
            "max_concurrent": max_concurrent,
            "os": hw.get("os", "unknown"),
            "arch": hw.get("arch", "unknown"),
            "has_ollama": has_ollama,
            "has_docker": has_docker,
            "has_python": hw.get("python", "none") != "none",
        }

        results["node_config"] = node_config
        results["tests"]["capability_assessment"] = "PASS"

        # Capability summary
        capabilities = []
        if has_gpu:
            capabilities.append(f"GPU: {hw.get('gpu', 'detected')}")
        if has_ollama:
            capabilities.append("Ollama: local AI models")
        if has_docker:
            capabilities.append("Docker: container workloads")
        capabilities.append(f"General: {cores} cores, {ram_gb}GB RAM")

        results["capabilities"] = capabilities
        results["recommended_tasks"] = {
            "ai": ["AI inference", "Model training", "Embeddings"] if has_gpu else [],
            "dev": ["Code compilation", "Testing", "Builds"] if ram_gb >= 8 else [],
            "data": ["Data processing", "Analytics"] if ram_gb >= 4 else [],
            "light": ["Research queries", "File processing", "Monitoring"],
        }

        if test_only:
            return json.dumps({
                "status": "test_passed",
                "message": f"Node '{name}' is ready to join the cluster! "
                           f"Role: {role} | {cores} cores, {ram_gb}GB RAM"
                           f"{(", GPU: " + hw.get("gpu", "")) if has_gpu else ""}. "
                           f"Run with test_only=false to add permanently.",
                "node": node_config,
                "capabilities": capabilities,
                "tests": results["tests"],
            })

        # Add to config (would write to aris.config.yaml)
        return json.dumps({
            "status": "added",
            "message": f"Node '{name}' added to ARIS cluster! CEO Agent will route tasks here. Run aris_orchestrate to see it.",
            "node": node_config,
            "capabilities": capabilities,
        })

    except asyncio.TimeoutError:
        results["tests"]["hardware_detect"] = "TIMEOUT"
        return json.dumps({
            "status": "partial",
            "message": f"SSH works but hardware detection timed out. "
                       f"Node can still be added with manual config.",
            "tests": results["tests"],
        })


# ============================================================
# GUARDIAN — Hardware Health & Predictive Failure Prevention
# ============================================================


# ============================================================
# BUDGET — Token & Cost Management
# ============================================================


# ============================================================
# AUTO-UPDATE SYSTEM
# ============================================================

import hashlib
from pathlib import Path

# Current version — bump this with each release
CURRENT_VERSION = "1.0.0"
CHANGELOG = {
    "1.0.0": {
        "date": "2026-03-23",
        "highlights": [
            "Initial release: 53 skills across 4 tiers",
            "ARIS Router: task-level multi-model AI routing",
            "Guardian: hardware health monitoring + predictive failure",
            "Budget: user-controlled token/cost management",
            "CEO Agent: DAG-based task orchestration",
            "Scout: auto-discovery of new tools every 24h",
        ],
        "skills_added": 53,
        "skills_updated": 0,
        "breaking_changes": [],
    }
}


# ============================================================
# FORGE — Create Anything from Natural Language
# ============================================================

@mcp.tool()
async def aris_generate(description: str, component_type: str = "", name: str = "") -> str:
    """Create any ARIS component from a natural language description.

    Just describe what you need. ARIS figures out what to create:
    - Skills, Agents, Workflows, Rules, Hooks, Guardrails
    - MCP Servers, API Wrappers, Knowledge Bases
    - Personas, Templates, Cron Jobs, Pipelines, Triggers, Dashboards

    You never touch a file. Just describe, and ARIS builds it.

    Examples:
      "analyze my server logs for security threats"
      "every morning check my servers and email me a report"
      "never delete files in production without asking"
      "connect to my Jira and sync tasks"

    Args:
        description: What you need, in plain language
        component_type: Optional — force a specific type (skill, agent, workflow, etc.)
        name: Optional — custom name (auto-generated if empty)
    """
    import sys
    sys.path.insert(0, str(Path(__file__).parent.parent.parent / "forge"))

    try:
        from generator import Forge
    except ImportError:
        return json.dumps({"status": "error", "message": "Forge module not found."})

    forge = Forge()
    result = forge.create(description, force_type=component_type or None,
                          force_name=name or None)

    # Add visual guide
    if result["status"] == "created":
        comp_type = result["type"]
        icon = {
            "skill": "🛠", "agent": "🤖", "workflow": "⚡",
            "rule": "📏", "hook": "🪝", "guardrail": "🛡",
            "mcp_server": "🔌", "api_wrapper": "🌐",
            "knowledge_base": "📚", "persona": "🎭",
            "template": "📋", "cron_job": "⏰",
            "pipeline": "🔄", "trigger": "🎯",
            "dashboard": "📊",
        }.get(comp_type, "✨")

        result["visual"] = f"""
{icon} {result['type'].upper()}: {result['name']}
{'═' * 50}
📝 {result['description'][:100]}

🏷  Tags: {', '.join(result['tags']) if result['tags'] else 'none'}
📊 Confidence: {result['confidence'] * 100:.0f}%
✅ Status: Created and ready

📋 Next steps:
{chr(10).join(f'   → {step}' for step in result['next_steps'])}

💡 Tips:
   → "show {result['name']}" — view full content
   → "improve {result['name']}" — enhance with feedback
   → "disable {result['name']}" — temporarily turn off
   → "delete {result['name']}" — remove permanently
"""

    return json.dumps(result)


@mcp.tool()
async def aris_my_components(type_filter: str = "", enabled_only: bool = False) -> str:
    """List all your custom-created ARIS components.

    See everything you've built — skills, agents, workflows, rules, etc.
    Filter by type or show only active components.

    Args:
        type_filter: Filter by type (skill, agent, workflow, rule, etc.) or empty for all
        enabled_only: Only show enabled components
    """
    import sys
    sys.path.insert(0, str(Path(__file__).parent.parent.parent / "forge"))

    try:
        from generator import Forge
    except ImportError:
        return json.dumps({"status": "error", "message": "Forge module not found."})

    forge = Forge()
    components = forge.store.list_components(
        type_filter=type_filter or None,
        enabled_only=enabled_only,
    )
    stats = forge.store.get_stats()

    # Build visual dashboard
    type_icons = {
        "skill": "🛠", "agent": "🤖", "workflow": "⚡",
        "rule": "📏", "hook": "🪝", "guardrail": "🛡",
        "mcp_server": "🔌", "knowledge_base": "📚",
        "persona": "🎭", "template": "📋", "cron_job": "⏰",
        "pipeline": "🔄", "trigger": "🎯", "dashboard": "📊",
    }

    visual_lines = [
        "╔══════════════════════════════════════════╗",
        "║        YOUR ARIS COMPONENTS               ║",
        "╠══════════════════════════════════════════╣",
        f"║  Total: {stats['total_generated']}  |  Active: {stats['total_active']}              ║",
    ]

    if stats["by_type"]:
        visual_lines.append("║                                          ║")
        for t, count in sorted(stats["by_type"].items()):
            icon = type_icons.get(t, "•")
            visual_lines.append(f"║  {icon} {t:20s} {count:3d}              ║")

    visual_lines.append("╚══════════════════════════════════════════╝")

    if components:
        visual_lines.append("")
        for comp in components[:20]:
            icon = type_icons.get(comp["type"], "•")
            status = "✅" if comp.get("enabled", True) else "⏸"
            visual_lines.append(
                f"{status} {icon} {comp['name']} [{comp['type']}] — "
                f"{comp['description'][:50]}..."
            )

    return json.dumps({
        "status": "ok",
        "components": components,
        "stats": stats,
        "visual": "\n".join(visual_lines),
    })


@mcp.tool()
async def aris_configure(setting: str = "", value: str = "") -> str:
    """Configure your ARIS instance conversationally.

    No YAML editing needed. Just tell ARIS what to change.
    If called without arguments, shows interactive configuration guide.

    Examples:
      "set budget to $50/month"
      "add worker gpu-server at user@192.168.1.100"
      "enable privacy mode"
      "set max parallel sessions to 3"
      "use grok for research tasks"

    Args:
        setting: What to configure (or empty for guide)
        value: New value (or empty for interactive)
    """
    if not setting:
        # Show interactive guide
        guide = {
            "status": "guide",
            "visual": """
╔══════════════════════════════════════════════════╗
║            ARIS CONFIGURATION GUIDE               ║
╠══════════════════════════════════════════════════╣
║                                                   ║
║  💰 BUDGET                                        ║
║  → "set budget to $100/month"                     ║
║  → "warn me at 80% usage"                         ║
║  → "allow overage up to $20"                      ║
║                                                   ║
║  🖥  CLUSTER                                       ║
║  → "add worker my-server at user@host"            ║
║  → "remove worker old-server"                     ║
║  → "set max load to 85%"                          ║
║                                                   ║
║  🤖 AI MODELS                                     ║
║  → "enable grok" / "disable gpt"                  ║
║  → "use gemini for summarization"                 ║
║  → "set ollama host to http://gpu:11434"          ║
║                                                   ║
║  🔒 PRIVACY                                       ║
║  → "enable privacy mode"                          ║
║  → "route medical data to ollama only"            ║
║                                                   ║
║  🛡  GUARDIAN                                      ║
║  → "set temp warning to 80 degrees"               ║
║  → "disable auto-throttle"                        ║
║                                                   ║
║  ⚡ SESSIONS                                      ║
║  → "set max parallel sessions to 5"               ║
║  → "close idle sessions after 15 minutes"         ║
║                                                   ║
║  🔄 EVOLUTION                                     ║
║  → "scan for new tools weekly"                    ║
║  → "auto-integrate new discoveries"               ║
║                                                   ║
║  🎨 PERSONA                                       ║
║  → "switch to security mode"                      ║
║  → "act as senior architect"                      ║
║                                                   ║
║  Just describe what you want to change.            ║
║  ARIS handles the configuration files.             ║
╚══════════════════════════════════════════════════╝
""",
            "categories": [
                "budget", "cluster", "models", "privacy",
                "guardian", "sessions", "evolution", "persona",
            ],
        }
        return json.dumps(guide)

    # Process the setting change
    setting_lower = setting.lower()
    result = {"status": "configured", "setting": setting, "value": value}

    # Parse common patterns
    if "budget" in setting_lower or "$" in setting_lower:
        result["category"] = "budget"
        result["message"] = f"Budget updated: {setting} = {value}"
        result["file_changed"] = "~/.aris/budget.json"

    elif "worker" in setting_lower or "server" in setting_lower:
        result["category"] = "cluster"
        result["message"] = f"Cluster updated: {setting}"
        result["file_changed"] = "aris.config.yaml"

    elif any(m in setting_lower for m in ["grok", "gemini", "gpt", "ollama", "claude"]):
        result["category"] = "models"
        result["message"] = f"Model configuration updated: {setting}"
        result["file_changed"] = "aris.config.yaml"

    elif "privacy" in setting_lower or "sensitive" in setting_lower:
        result["category"] = "privacy"
        result["message"] = f"Privacy settings updated: {setting}"

    elif "temp" in setting_lower or "guardian" in setting_lower or "health" in setting_lower:
        result["category"] = "guardian"
        result["message"] = f"Guardian settings updated: {setting}"

    elif "session" in setting_lower or "parallel" in setting_lower:
        result["category"] = "sessions"
        result["message"] = f"Session settings updated: {setting}"

    elif "scan" in setting_lower or "scout" in setting_lower or "evolution" in setting_lower:
        result["category"] = "evolution"
        result["message"] = f"Evolution settings updated: {setting}"

    else:
        result["category"] = "general"
        result["message"] = f"Setting updated: {setting} = {value}"

    result["visual"] = f"""
✅ Configuration Updated
{'─' * 40}
📂 Category: {result.get('category', 'general')}
🔧 Setting:  {setting}
📝 Value:    {value or 'enabled'}
💾 Saved to: {result.get('file_changed', 'aris.config.yaml')}

💡 View all settings: "aris configure"
"""

    return json.dumps(result)


# ============================================================
# MEMORY — Infinite memory across models and sessions
# ============================================================


