"""
EasyClaw MCP Server v3.0.0
13 MCP tools, admin REST API, raw ASGI dispatcher.
"""

import json
import os
import re
import time
import uuid
from datetime import datetime, timezone

import httpx
from dotenv import load_dotenv
from fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

from auth import (
    AuthResult,
    _auth_context,
    _require_auth,
    authenticate_request,
    extract_auth_header,
    log_tool_usage,
    verify_admin_secret,
)
from database import (
    init_db,
    search_knowledge_fts,
    get_guide,
    list_guide_topics,
    create_user,
    get_user_by_email,
    create_subscription,
    update_subscription_status,
    get_usage_stats,
    lookup_by_email,
    upsert_knowledge_entry,
    list_knowledge_entries,
    delete_knowledge_entry,
    generate_otp,
    verify_otp,
    create_guide_purchase,
    validate_download_token,
    KNOWLEDGE_BASE,
    _rebuild_knowledge_fts,
)

load_dotenv()

SEARXNG_URL = os.environ.get("SEARXNG_URL", "http://searxng:8080")
VERSION = "3.0.0"

# ===================================================================
# FastMCP setup
# ===================================================================

mcp = FastMCP("EasyClaw Knowledge Hub")

# ===================================================================
# TOOL 1: search_knowledge
# ===================================================================

@mcp.tool()
def search_knowledge(query: str) -> str:
    """Search the OpenClaw knowledge base across 18 guides.
    Returns the top 5 matches with relevance ranking and 600-character previews.
    Use get_full_guide with the topic key to retrieve complete guide content.
    """
    start = time.time()
    auth = _require_auth("search_knowledge", "starter")

    results = search_knowledge_fts(query, limit=5)

    if not results:
        log_tool_usage(auth, "search_knowledge", int((time.time() - start) * 1000))
        return f"No results found for '{query}'. Available topics: {', '.join(KNOWLEDGE_BASE.keys())}"

    output_parts = [f"## Search Results for: {query}\n"]
    for i, r in enumerate(results, 1):
        preview = r["content"][:600].strip()
        if len(r["content"]) > 600:
            preview += "..."
        output_parts.append(
            f"### {i}. {r['title']}\n"
            f"**Topic key**: `{r['topic']}`\n\n"
            f"{preview}\n\n"
            f"*Use `get_full_guide(\"{r['topic']}\")` for the complete guide.*\n"
        )

    log_tool_usage(auth, "search_knowledge", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# TOOL 2: get_full_guide
# ===================================================================

@mcp.tool()
def get_full_guide(topic: str) -> str:
    """Retrieve the complete content of a knowledge guide by topic key.
    Use search_knowledge first to find relevant topics.

    Available topics: quick_start, openrouter_models, persistent_memory,
    channels, troubleshooting, skills, security_hardening, automation_crons,
    sub_agents, browser_automation, voice_tts, docker_deployment,
    cost_optimization, clawhub_skills_detailed, mcp_integration,
    configuration_reference, onboarding_wizard, easyclaw_code_guide
    """
    start = time.time()
    auth = _require_auth("get_full_guide", "starter")

    topic = topic.lower().strip().replace(" ", "_")
    guide = get_guide(topic)

    if not guide:
        all_topics = list_guide_topics()
        topic_list = ", ".join(t["topic"] for t in all_topics)
        log_tool_usage(auth, "get_full_guide", int((time.time() - start) * 1000), False)
        return f"Topic '{topic}' not found. Available topics: {topic_list}"

    log_tool_usage(auth, "get_full_guide", int((time.time() - start) * 1000))
    return guide["content"]


# ===================================================================
# TOOL 3: get_model_recommendation
# ===================================================================

@mcp.tool()
def get_model_recommendation(use_case: str) -> str:
    """Get AI model recommendations based on your use case.

    Supported use cases: coding, general, reasoning, fast, cheap, creative,
    subagent, production, budget, long_context, multimodal
    """
    start = time.time()
    auth = _require_auth("get_model_recommendation", "starter")

    use_case_lower = use_case.lower().strip()

    recommendations = {
        "coding": {
            "primary": "deepseek/deepseek-v3-0324",
            "alternative": "anthropic/claude-sonnet-4",
            "budget": "deepseek/deepseek-v3-0324",
            "reasoning": "Code generation, debugging, refactoring. DeepSeek excels at coding tasks at budget price. Claude Sonnet is premium choice.",
        },
        "general": {
            "primary": "moonshotai/kimi-k2.5",
            "alternative": "openai/gpt-4.1",
            "budget": "deepseek/deepseek-v3-0324",
            "reasoning": "General-purpose chat and assistance. Kimi K2.5 offers excellent quality/cost ratio.",
        },
        "reasoning": {
            "primary": "moonshotai/kimi-k2.5",
            "alternative": "anthropic/claude-sonnet-4",
            "budget": "qwen/qwen3-235b",
            "reasoning": "Complex reasoning, analysis, problem-solving. Kimi K2.5 has strong reasoning at mid-range price.",
        },
        "fast": {
            "primary": "minimax/minimax-m2.1",
            "alternative": "google/gemini-2.5-flash-lite",
            "budget": "google/gemini-2.5-flash-lite",
            "reasoning": "Speed-critical tasks. MiniMax M2.1 and Gemini Flash Lite are the fastest options.",
        },
        "cheap": {
            "primary": "google/gemini-2.5-flash-lite",
            "alternative": "minimax/minimax-m2.1",
            "budget": "google/gemini-2.5-flash-lite",
            "reasoning": "Minimum cost. Gemini Flash Lite at $0.02/M input is the cheapest quality option.",
        },
        "creative": {
            "primary": "moonshotai/kimi-k2.5",
            "alternative": "openai/gpt-4.1",
            "budget": "deepseek/deepseek-v3-0324",
            "reasoning": "Creative writing, brainstorming, content generation. Kimi K2.5 handles creative tasks well.",
        },
        "subagent": {
            "primary": "minimax/minimax-m2.1",
            "alternative": "google/gemini-2.5-flash-lite",
            "budget": "minimax/minimax-m2.1",
            "reasoning": "Sub-agent tasks (simple tool calls, data processing). Use cheapest fast model.",
        },
        "production": {
            "primary": "anthropic/claude-sonnet-4",
            "alternative": "openai/gpt-4.1",
            "budget": "moonshotai/kimi-k2.5",
            "reasoning": "Production reliability. Claude Sonnet 4 and GPT-4.1 are the most reliable.",
        },
        "budget": {
            "primary": "deepseek/deepseek-v3-0324",
            "alternative": "minimax/minimax-m2.1",
            "budget": "google/gemini-2.5-flash-lite",
            "reasoning": "Best overall value. DeepSeek V3.2 offers strong performance at $0.14/M input.",
        },
        "long_context": {
            "primary": "google/gemini-2.5-pro",
            "alternative": "openai/gpt-4.1",
            "budget": "google/gemini-2.5-flash",
            "reasoning": "Large context windows (1M tokens). Gemini models support the largest contexts.",
        },
        "multimodal": {
            "primary": "google/gemini-2.5-flash",
            "alternative": "openai/gpt-4.1",
            "budget": "google/gemini-2.5-flash-lite",
            "reasoning": "Image/video/audio input. Gemini models have the best multimodal support.",
        },
    }

    # Find best match
    matched = None
    for key, rec in recommendations.items():
        if key in use_case_lower:
            matched = (key, rec)
            break

    if not matched:
        # Default recommendation
        matched = ("general", recommendations["general"])

    key, rec = matched
    output = (
        f"## Model Recommendation: {key.title()}\n\n"
        f"**Use case**: {rec['reasoning']}\n\n"
        f"| Priority | Model | Approx. Cost |\n"
        f"|----------|-------|-------------|\n"
        f"| Primary | `{rec['primary']}` | See openrouter.ai/models |\n"
        f"| Alternative | `{rec['alternative']}` | See openrouter.ai/models |\n"
        f"| Budget | `{rec['budget']}` | See openrouter.ai/models |\n\n"
        f"### Suggested Config\n"
        f"```json\n"
        f'{{\n'
        f'  "openrouter": {{\n'
        f'    "model": "{rec["primary"]}",\n'
        f'    "subAgentModel": "minimax/minimax-m2.1",\n'
        f'    "thinkingModel": "{rec["alternative"]}"\n'
        f'  }}\n'
        f'}}\n'
        f"```\n\n"
        f"*Tip: Check current pricing at https://openrouter.ai/models*"
    )

    log_tool_usage(auth, "get_model_recommendation", int((time.time() - start) * 1000))
    return output


# ===================================================================
# TOOL 4: validate_config
# ===================================================================

@mcp.tool()
def validate_config(config_json: str = "") -> str:
    """Validate an OpenClaw configuration against best practices.
    Pass the JSON contents of openclaw.json as a string.
    If no config provided, returns the complete checklist.
    """
    start = time.time()
    auth = _require_auth("validate_config", "starter")

    checks = []

    if config_json:
        try:
            config = json.loads(config_json)
        except json.JSONDecodeError as e:
            log_tool_usage(auth, "validate_config", int((time.time() - start) * 1000), False)
            return f"Invalid JSON: {e}"
    else:
        config = {}

    # Model checks
    or_config = config.get("openrouter", {})
    if or_config.get("apiKey"):
        if or_config["apiKey"].startswith("sk-or-v1-"):
            checks.append("✓ Model: API key format valid")
        else:
            checks.append("✗ Model: API key should start with 'sk-or-v1-'")
    else:
        checks.append("⚠ Model: No API key configured (check env var OPENROUTER_API_KEY)")

    if or_config.get("model"):
        checks.append(f"✓ Model: Primary model set ({or_config['model']})")
    else:
        checks.append("✗ Model: No primary model configured")

    # Memory checks
    mem_config = config.get("memory", {})
    if mem_config.get("type"):
        checks.append(f"✓ Memory: Type set ({mem_config['type']})")
    else:
        checks.append("⚠ Memory: No memory type configured (defaults to file)")

    if mem_config.get("path"):
        checks.append(f"✓ Memory: Path set ({mem_config['path']})")
    else:
        checks.append("⚠ Memory: No memory path set (defaults to /var/lib/openclaw/memory)")

    # Security checks
    sec_config = config.get("security", {})
    rl = sec_config.get("rateLimit", {})
    if rl.get("enabled"):
        checks.append("✓ Security: Rate limiting enabled")
    else:
        checks.append("✗ Security: Rate limiting NOT enabled (recommended)")

    iv = sec_config.get("inputValidation", {})
    if iv.get("maxMessageLength"):
        checks.append(f"✓ Security: Max message length set ({iv['maxMessageLength']})")
    else:
        checks.append("⚠ Security: No max message length (default: unlimited)")

    # Channel checks
    ch_config = config.get("channels", {})
    enabled_channels = [k for k, v in ch_config.items() if isinstance(v, dict) and v.get("enabled")]
    if enabled_channels:
        checks.append(f"✓ Channels: {len(enabled_channels)} enabled ({', '.join(enabled_channels)})")
        for ch_name in enabled_channels:
            ch = ch_config[ch_name]
            has_allowlist = any(
                ch.get(k) for k in ["allowedUsers", "allowedGuilds", "allowedChannels", "allowedNumbers", "allowedRooms"]
            )
            if has_allowlist:
                checks.append(f"  ✓ {ch_name}: Allowlist configured")
            else:
                checks.append(f"  ✗ {ch_name}: No allowlist (security risk!)")
    else:
        checks.append("⚠ Channels: No channels enabled")

    # Logging checks
    log_config = config.get("logging", {})
    if log_config.get("level"):
        checks.append(f"✓ Logging: Level set ({log_config['level']})")
    else:
        checks.append("⚠ Logging: No log level set (defaults to info)")

    # Performance checks
    server_config = config.get("server", {})
    if server_config.get("port"):
        checks.append(f"✓ Server: Port configured ({server_config['port']})")

    gw_config = config.get("gateway", {})
    if gw_config.get("port"):
        checks.append(f"✓ Gateway: Port configured ({gw_config['port']})")

    if not config:
        checks = [
            "## OpenClaw Configuration Checklist\n",
            "No config provided. Here's the complete checklist:\n",
            "### Model",
            "- [ ] OpenRouter API key set (env var or config)",
            "- [ ] Primary model selected",
            "- [ ] Sub-agent model selected (for cost optimization)",
            "",
            "### Memory",
            "- [ ] Memory type chosen (file/vector/hybrid)",
            "- [ ] Memory path configured",
            "- [ ] SOUL.md created",
            "- [ ] USER.md created",
            "",
            "### Security",
            "- [ ] Rate limiting enabled",
            "- [ ] Input validation enabled",
            "- [ ] Channel allowlists configured",
            "- [ ] API key in env var (not plain config)",
            "",
            "### Channels",
            "- [ ] At least one channel enabled",
            "- [ ] Channel tokens set",
            "- [ ] Webhook URLs configured (if applicable)",
            "",
            "### Performance",
            "- [ ] Server port configured",
            "- [ ] Gateway port configured",
            "- [ ] Max tokens set appropriately",
            "",
            "### Deployment",
            "- [ ] Systemd services created",
            "- [ ] Firewall rules set",
            "- [ ] TLS/HTTPS via reverse proxy",
            "- [ ] Log rotation configured",
        ]

    log_tool_usage(auth, "validate_config", int((time.time() - start) * 1000))
    return "\n".join(checks)


# ===================================================================
# TOOL 5: search_web
# ===================================================================

@mcp.tool()
def search_web(query: str, num_results: int = 10) -> str:
    """Search the web using a private SearXNG instance.
    Returns results from Google and DuckDuckGo with titles, URLs, and snippets.
    """
    start = time.time()
    auth = _require_auth("search_web", "starter")

    try:
        with httpx.Client(timeout=30) as client:
            resp = client.get(
                f"{SEARXNG_URL}/search",
                params={
                    "q": query,
                    "format": "json",
                    "engines": "google,duckduckgo",
                    "num_results": min(num_results, 20),
                },
            )
            data = resp.json()
    except Exception as e:
        log_tool_usage(auth, "search_web", int((time.time() - start) * 1000), False)
        return f"Search error: {e}"

    results = data.get("results", [])[:num_results]
    if not results:
        log_tool_usage(auth, "search_web", int((time.time() - start) * 1000))
        return f"No results found for '{query}'."

    output_parts = [f"### Web Search Results for: {query}\n"]
    for i, r in enumerate(results, 1):
        title = r.get("title", "No title")
        url = r.get("url", "")
        snippet = r.get("content", "")[:200]
        output_parts.append(f"**{i}. {title}**\n{url}\n{snippet}\n")

    log_tool_usage(auth, "search_web", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# TOOL 6: recommend_skills
# ===================================================================

@mcp.tool()
def recommend_skills(category: str = "all") -> str:
    """Recommend ClawHub skill packs by category.

    Categories: communication, developer, productivity, data, media, all
    """
    start = time.time()
    auth = _require_auth("recommend_skills", "starter")

    skill_catalog = {
        "communication": {
            "title": "Communication & Social",
            "skills": [
                {"name": "gmail", "desc": "Full Gmail integration — read, compose, search, label"},
                {"name": "calendar", "desc": "Google Calendar management"},
                {"name": "slack-tools", "desc": "Advanced Slack channel and message operations"},
                {"name": "twitter", "desc": "Tweet, search, engage on Twitter/X"},
                {"name": "linkedin", "desc": "Profile management and post creation"},
            ],
        },
        "developer": {
            "title": "Developer Tools",
            "skills": [
                {"name": "github", "desc": "Repo management, issues, PRs, Actions"},
                {"name": "docker-manager", "desc": "Container lifecycle management"},
                {"name": "db-query", "desc": "PostgreSQL, MySQL, SQLite query execution"},
                {"name": "api-tester", "desc": "HTTP endpoint testing with assertions"},
                {"name": "code-review", "desc": "Automated AI-powered code review"},
            ],
        },
        "productivity": {
            "title": "Productivity",
            "skills": [
                {"name": "notion", "desc": "Notion page and database management"},
                {"name": "google-docs", "desc": "Document creation and editing"},
                {"name": "trello", "desc": "Board and card management"},
                {"name": "task-tracker", "desc": "Task and project tracking"},
                {"name": "file-manager", "desc": "Advanced local file operations"},
            ],
        },
        "data": {
            "title": "Data & Analytics",
            "skills": [
                {"name": "web-scraper", "desc": "Intelligent web scraping with selectors"},
                {"name": "data-viz", "desc": "Chart and graph generation"},
                {"name": "spreadsheet", "desc": "Google Sheets / Excel operations"},
                {"name": "pdf-tools", "desc": "PDF generation, parsing, merging"},
            ],
        },
        "media": {
            "title": "Media",
            "skills": [
                {"name": "tts", "desc": "Text-to-speech (OpenAI, ElevenLabs, Piper)"},
                {"name": "image-gen", "desc": "AI image generation (DALL-E, Midjourney)"},
                {"name": "transcribe", "desc": "Audio/video transcription (Whisper)"},
                {"name": "screenshot", "desc": "Web page screenshots"},
                {"name": "youtube-comment-bot", "desc": "YouTube comment management"},
            ],
        },
    }

    cat = category.lower().strip()
    if cat == "all" or cat not in skill_catalog:
        # Show all categories
        parts = ["## ClawHub Skill Pack Recommendations\n"]
        for cat_key, cat_data in skill_catalog.items():
            parts.append(f"### {cat_data['title']}")
            for s in cat_data["skills"]:
                parts.append(f"- **{s['name']}** — {s['desc']}")
            parts.append("")
        parts.append("Install: `clawhub install <skill-name>`")
    else:
        cat_data = skill_catalog[cat]
        parts = [f"## {cat_data['title']} Skills\n"]
        for s in cat_data["skills"]:
            parts.append(f"- **{s['name']}** — {s['desc']}")
        parts.append(f"\nInstall: `clawhub install <skill-name>`")

    log_tool_usage(auth, "recommend_skills", int((time.time() - start) * 1000))
    return "\n".join(parts)


# ===================================================================
# TOOL 7: generate_config
# ===================================================================

@mcp.tool()
def generate_config(profile: str = "developer") -> str:
    """Generate a complete openclaw.json configuration template.

    Profiles: developer, business, creator, production, budget, minimal, security
    """
    start = time.time()
    auth = _require_auth("generate_config", "starter")

    profiles = {
        "developer": {
            "openrouter": {
                "model": "deepseek/deepseek-v3-0324",
                "subAgentModel": "minimax/minimax-m2.1",
                "thinkingModel": "moonshotai/kimi-k2.5",
                "maxTokens": 8192,
                "temperature": 0.7
            },
            "memory": {"type": "file", "path": "/var/lib/openclaw/memory", "autoSummarize": True},
            "channels": {"telegram": {"enabled": True, "token": "YOUR_BOT_TOKEN", "allowedUsers": []}},
            "security": {"rateLimit": {"enabled": True, "maxRequestsPerMinute": 60}},
            "logging": {"level": "debug", "requests": True},
        },
        "business": {
            "openrouter": {
                "model": "anthropic/claude-sonnet-4",
                "subAgentModel": "anthropic/claude-3.5-haiku",
                "thinkingModel": "google/gemini-2.5-pro",
                "maxTokens": 8192,
                "temperature": 0.5
            },
            "memory": {"type": "hybrid", "path": "/var/lib/openclaw/memory"},
            "channels": {
                "slack": {"enabled": True, "botToken": "xoxb-YOUR-TOKEN", "signingSecret": "YOUR_SECRET"},
                "telegram": {"enabled": True, "token": "YOUR_BOT_TOKEN"},
            },
            "security": {
                "rateLimit": {"enabled": True, "maxRequestsPerMinute": 30, "maxTokensPerDay": 2000000},
                "inputValidation": {"maxMessageLength": 4000, "contentFilter": "strict"},
            },
            "logging": {"level": "info", "requests": True, "sensitiveDataMasking": True},
        },
        "creator": {
            "openrouter": {
                "model": "moonshotai/kimi-k2.5",
                "subAgentModel": "minimax/minimax-m2.1",
                "thinkingModel": "openai/gpt-4.1",
                "maxTokens": 8192,
                "temperature": 0.9
            },
            "memory": {"type": "file", "path": "/var/lib/openclaw/memory"},
            "channels": {"telegram": {"enabled": True, "token": "YOUR_BOT_TOKEN"}},
            "security": {"rateLimit": {"enabled": True, "maxRequestsPerMinute": 30}},
            "logging": {"level": "info"},
        },
        "production": {
            "openrouter": {
                "model": "anthropic/claude-sonnet-4",
                "subAgentModel": "anthropic/claude-3.5-haiku",
                "thinkingModel": "anthropic/claude-sonnet-4",
                "maxTokens": 8192,
                "temperature": 0.3
            },
            "server": {"port": 3100, "host": "127.0.0.1"},
            "gateway": {"port": 3200, "host": "127.0.0.1", "timeout": 60000},
            "memory": {"type": "hybrid", "path": "/var/lib/openclaw/memory"},
            "security": {
                "rateLimit": {"enabled": True, "maxRequestsPerMinute": 30, "maxCostPerDay": 20.0},
                "inputValidation": {"maxMessageLength": 4000, "stripHtml": True, "contentFilter": "strict"},
            },
            "logging": {"level": "warn", "requests": True, "sensitiveDataMasking": True, "rotate": True},
        },
        "budget": {
            "openrouter": {
                "model": "google/gemini-2.5-flash-lite",
                "subAgentModel": "google/gemini-2.5-flash-lite",
                "maxTokens": 4096,
                "temperature": 0.7
            },
            "memory": {"type": "file", "path": "/var/lib/openclaw/memory"},
            "channels": {"telegram": {"enabled": True, "token": "YOUR_BOT_TOKEN"}},
            "security": {"rateLimit": {"enabled": True, "maxTokensPerDay": 500000, "maxCostPerDay": 2.0}},
            "logging": {"level": "warn"},
        },
        "minimal": {
            "openrouter": {"model": "deepseek/deepseek-v3-0324", "maxTokens": 4096},
            "memory": {"type": "file", "path": "/var/lib/openclaw/memory"},
        },
        "security": {
            "openrouter": {
                "model": "anthropic/claude-sonnet-4",
                "subAgentModel": "anthropic/claude-3.5-haiku",
                "maxTokens": 4096,
                "temperature": 0.3
            },
            "memory": {
                "type": "file",
                "path": "/var/lib/openclaw/memory",
                "isolation": True,
                "maxFileSize": "50KB",
                "blockSystemFiles": True,
            },
            "security": {
                "rateLimit": {"enabled": True, "maxRequestsPerMinute": 10, "maxTokensPerDay": 500000, "maxCostPerDay": 5.0},
                "inputValidation": {"maxMessageLength": 2000, "stripHtml": True, "blockCodeExecution": True, "contentFilter": "strict"},
                "apiKeyRotation": {"enabled": True, "intervalDays": 30},
            },
            "logging": {"level": "info", "requests": True, "sensitiveDataMasking": True, "rotate": True},
        },
    }

    prof = profile.lower().strip()
    if prof not in profiles:
        log_tool_usage(auth, "generate_config", int((time.time() - start) * 1000), False)
        return f"Unknown profile '{profile}'. Available: {', '.join(profiles.keys())}"

    config = profiles[prof]
    config_json = json.dumps(config, indent=2)

    output = (
        f"## OpenClaw Configuration — {prof.title()} Profile\n\n"
        f"Save this to `/etc/openclaw/openclaw.json`:\n\n"
        f"```json\n{config_json}\n```\n\n"
        f"**Remember**: Replace `YOUR_BOT_TOKEN` and API keys with real values.\n"
        f"Set your OpenRouter API key via environment variable:\n"
        f"```bash\nexport OPENROUTER_API_KEY=sk-or-v1-your-key-here\n```"
    )

    log_tool_usage(auth, "generate_config", int((time.time() - start) * 1000))
    return output


# ===================================================================
# TOOL 8: security_audit
# ===================================================================

@mcp.tool()
def security_audit(config_json: str = "") -> str:
    """Run an 11-item security audit on an OpenClaw configuration.
    Returns severity levels (CRITICAL/HIGH/RECOMMENDED) with fix commands.
    """
    start = time.time()
    auth = _require_auth("security_audit", "starter")

    findings = []

    if config_json:
        try:
            config = json.loads(config_json)
        except json.JSONDecodeError:
            config = {}
    else:
        config = {}

    or_config = config.get("openrouter", {})
    sec_config = config.get("security", {})
    ch_config = config.get("channels", {})
    log_config = config.get("logging", {})
    mem_config = config.get("memory", {})

    # 1. API key exposure
    if or_config.get("apiKey") and not or_config.get("apiKeyFile"):
        findings.append({
            "severity": "CRITICAL",
            "item": "API key stored in plain config file",
            "fix": 'Move to env var: export OPENROUTER_API_KEY=sk-or-v1-...\nOr file: openclaw config set openrouter.apiKeyFile "/etc/openclaw/.api-key"',
        })
    else:
        findings.append({"severity": "OK", "item": "API key not in plain config"})

    # 2. Rate limiting
    rl = sec_config.get("rateLimit", {})
    if not rl.get("enabled"):
        findings.append({
            "severity": "HIGH",
            "item": "Rate limiting not enabled",
            "fix": "openclaw config set security.rateLimit.enabled true",
        })
    else:
        findings.append({"severity": "OK", "item": "Rate limiting enabled"})

    # 3. Input validation
    iv = sec_config.get("inputValidation", {})
    if not iv.get("maxMessageLength"):
        findings.append({
            "severity": "HIGH",
            "item": "No max message length set",
            "fix": "openclaw config set security.inputValidation.maxMessageLength 4000",
        })
    else:
        findings.append({"severity": "OK", "item": f"Max message length: {iv['maxMessageLength']}"})

    # 4. Channel allowlists
    for ch_name, ch_data in ch_config.items():
        if isinstance(ch_data, dict) and ch_data.get("enabled"):
            allowlist_keys = ["allowedUsers", "allowedGuilds", "allowedChannels", "allowedNumbers", "allowedRooms"]
            has_allowlist = any(ch_data.get(k) for k in allowlist_keys)
            if not has_allowlist:
                findings.append({
                    "severity": "CRITICAL",
                    "item": f"{ch_name}: No allowlist configured — anyone can message your bot",
                    "fix": f'openclaw config set channels.{ch_name}.allowedUsers \'[YOUR_USER_ID]\'',
                })
            else:
                findings.append({"severity": "OK", "item": f"{ch_name}: Allowlist configured"})

    # 5. Sensitive data masking
    if not log_config.get("sensitiveDataMasking"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "Sensitive data masking not enabled in logs",
            "fix": "openclaw config set logging.sensitiveDataMasking true",
        })
    else:
        findings.append({"severity": "OK", "item": "Log data masking enabled"})

    # 6. Memory isolation
    if not mem_config.get("isolation"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "Memory isolation not enabled",
            "fix": "openclaw config set memory.isolation true",
        })
    else:
        findings.append({"severity": "OK", "item": "Memory isolation enabled"})

    # 7. Content filter
    cf = iv.get("contentFilter", "")
    if cf not in ("moderate", "strict"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "Content filter not set or too permissive",
            "fix": 'openclaw config set security.inputValidation.contentFilter "moderate"',
        })
    else:
        findings.append({"severity": "OK", "item": f"Content filter: {cf}"})

    # 8. Cost limits
    if not rl.get("maxCostPerDay"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "No daily cost limit set",
            "fix": "openclaw config set security.rateLimit.maxCostPerDay 10.00",
        })
    else:
        findings.append({"severity": "OK", "item": f"Daily cost limit: ${rl['maxCostPerDay']}"})

    # 9. API key rotation
    akr = sec_config.get("apiKeyRotation", {})
    if not akr.get("enabled"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "API key rotation not enabled",
            "fix": "openclaw config set security.apiKeyRotation.enabled true",
        })
    else:
        findings.append({"severity": "OK", "item": f"Key rotation: every {akr.get('intervalDays', 90)} days"})

    # 10. HTML stripping
    if not iv.get("stripHtml"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "HTML stripping not enabled on input",
            "fix": "openclaw config set security.inputValidation.stripHtml true",
        })
    else:
        findings.append({"severity": "OK", "item": "HTML stripping enabled"})

    # 11. Request logging
    if not log_config.get("requests"):
        findings.append({
            "severity": "RECOMMENDED",
            "item": "Request logging not enabled",
            "fix": "openclaw config set logging.requests true",
        })
    else:
        findings.append({"severity": "OK", "item": "Request logging enabled"})

    # Format output
    output_parts = ["## Security Audit Results\n"]

    for sev in ["CRITICAL", "HIGH", "RECOMMENDED", "OK"]:
        items = [f for f in findings if f["severity"] == sev]
        if items:
            if sev == "OK":
                output_parts.append(f"### ✓ Passing ({len(items)})")
            else:
                output_parts.append(f"### {sev} ({len(items)})")
            for item in items:
                if sev == "OK":
                    output_parts.append(f"  ✓ {item['item']}")
                else:
                    symbol = "✗" if sev in ("CRITICAL", "HIGH") else "○"
                    output_parts.append(f"  {symbol} {item['item']}")
                    output_parts.append(f"    Fix: {item['fix']}")
            output_parts.append("")

    critical_count = len([f for f in findings if f["severity"] == "CRITICAL"])
    high_count = len([f for f in findings if f["severity"] == "HIGH"])
    if critical_count or high_count:
        output_parts.append(f"**⚠ {critical_count} critical, {high_count} high severity issues found.**")
    else:
        output_parts.append("**✓ No critical or high severity issues.**")

    log_tool_usage(auth, "security_audit", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# TOOL 9: estimate_cost
# ===================================================================

@mcp.tool()
def estimate_cost(model: str = "moonshotai/kimi-k2.5",
                  daily_messages: int = 100,
                  avg_tokens_per_message: int = 500) -> str:
    """Estimate monthly AI costs for OpenClaw based on model and usage.
    Calculates costs across multiple models for comparison.
    """
    start = time.time()
    auth = _require_auth("estimate_cost", "starter")

    # Pricing per million tokens (approximate, as of March 2026)
    model_pricing = {
        "google/gemini-2.5-flash-lite": {"input": 0.02, "output": 0.10, "name": "Gemini Flash Lite"},
        "minimax/minimax-m2.1": {"input": 0.10, "output": 0.30, "name": "MiniMax M2.1"},
        "deepseek/deepseek-v3-0324": {"input": 0.14, "output": 0.28, "name": "DeepSeek V3.2"},
        "qwen/qwen3-235b": {"input": 0.20, "output": 0.60, "name": "Qwen 3 235B"},
        "google/gemini-2.5-flash": {"input": 0.15, "output": 0.60, "name": "Gemini 2.5 Flash"},
        "openai/gpt-4.1-mini": {"input": 0.40, "output": 1.60, "name": "GPT-4.1 Mini"},
        "moonshotai/kimi-k2.5": {"input": 0.60, "output": 2.00, "name": "Kimi K2.5"},
        "anthropic/claude-3.5-haiku": {"input": 0.80, "output": 4.00, "name": "Claude 3.5 Haiku"},
        "google/gemini-2.5-pro": {"input": 1.25, "output": 10.00, "name": "Gemini 2.5 Pro"},
        "openai/gpt-4.1": {"input": 2.00, "output": 8.00, "name": "GPT-4.1"},
        "anthropic/claude-sonnet-4": {"input": 3.00, "output": 15.00, "name": "Claude Sonnet 4"},
        "anthropic/claude-opus-4": {"input": 15.00, "output": 75.00, "name": "Claude Opus 4"},
    }

    # Assume 60% input, 40% output ratio
    daily_tokens = daily_messages * avg_tokens_per_message
    monthly_tokens = daily_tokens * 30
    input_tokens = monthly_tokens * 0.6
    output_tokens = monthly_tokens * 0.4

    output_parts = [
        f"## Monthly Cost Estimate\n",
        f"**Usage**: {daily_messages} messages/day × {avg_tokens_per_message} avg tokens\n",
        f"**Monthly tokens**: {monthly_tokens:,} ({input_tokens:,.0f} in / {output_tokens:,.0f} out)\n",
        f"| Model | Monthly Cost | Daily Cost |",
        f"|-------|-------------|-----------|",
    ]

    for model_id, pricing in model_pricing.items():
        input_cost = (input_tokens / 1_000_000) * pricing["input"]
        output_cost = (output_tokens / 1_000_000) * pricing["output"]
        monthly_cost = input_cost + output_cost
        daily_cost = monthly_cost / 30
        marker = " ←" if model_id == model else ""
        output_parts.append(
            f"| {pricing['name']} | ${monthly_cost:.2f} | ${daily_cost:.2f} |{marker}"
        )

    output_parts.append(
        f"\n*Prices are approximate. Check https://openrouter.ai/models for current pricing.*"
    )

    log_tool_usage(auth, "estimate_cost", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# TOOL 10: diagnose_issue (subscriber tier)
# ===================================================================

@mcp.tool()
def diagnose_issue(error_text: str, context: str = "general") -> str:
    """Diagnose OpenClaw errors by matching against 20+ known issue patterns.
    Requires subscriber tier.

    context: general, startup, gateway, channels, memory, mcp
    """
    start = time.time()
    auth = _require_auth("diagnose_issue", "subscriber")

    patterns = [
        {"pattern": r"EACCES.*permission denied", "diagnosis": "Permission denied error",
         "category": "filesystem",
         "fix": "Run as root or fix permissions:\nsudo chown -R root:root /etc/openclaw /var/lib/openclaw\nsudo chmod 755 /etc/openclaw /var/lib/openclaw"},
        {"pattern": r"EADDRINUSE|port.*already in use", "diagnosis": "Port conflict — another process is using the same port",
         "category": "startup",
         "fix": "Find and kill the conflicting process:\nlsof -i :3100  # or :3200\nkill -9 <PID>\nOr change the port: openclaw config set server.port 3101"},
        {"pattern": r"ECONNREFUSED|connection refused", "diagnosis": "Target service not running or not accepting connections",
         "category": "general",
         "fix": "Check if the service is running:\nsystemctl status openclaw\nsystemctl status openclaw-gateway\nRestart: systemctl restart openclaw openclaw-gateway"},
        {"pattern": r"ENOTFOUND|getaddrinfo|DNS", "diagnosis": "DNS resolution failure — hostname not found",
         "category": "general",
         "fix": "Check DNS resolution:\ndig mcp.easyclaw.help\nCheck /etc/resolv.conf\nTry: echo 'nameserver 8.8.8.8' >> /etc/resolv.conf"},
        {"pattern": r"401|unauthorized|invalid.*api.*key", "diagnosis": "Authentication failure — invalid or expired API key",
         "category": "general",
         "fix": "Verify your API key:\n1. Check OpenRouter dashboard: https://openrouter.ai/keys\n2. Regenerate if needed\n3. Update config: openclaw config set openrouter.apiKey \"new-key\""},
        {"pattern": r"429|rate.?limit|too many requests", "diagnosis": "Rate limit exceeded — too many API calls",
         "category": "general",
         "fix": "Wait and retry, or:\n1. Use a cheaper/faster model for sub-agents\n2. Increase rate limits: openclaw config set security.rateLimit.maxRequestsPerMinute 60\n3. Add caching: openclaw config set cache.enabled true"},
        {"pattern": r"ENOMEM|out of memory|OOM|Cannot allocate", "diagnosis": "System out of memory",
         "category": "startup",
         "fix": "Free memory or add swap:\nfree -h\nfallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile\nOr use a smaller model to reduce memory usage"},
        {"pattern": r"ENOSPC|no space left|disk full", "diagnosis": "Disk full",
         "category": "startup",
         "fix": "Free disk space:\ndf -h\ndu -sh /var/log/* | sort -h\njournalctl --vacuum-size=100M\napt clean"},
        {"pattern": r"SyntaxError|unexpected token|invalid.*json", "diagnosis": "JSON syntax error in configuration file",
         "category": "startup",
         "fix": "Validate JSON:\ncat /etc/openclaw/openclaw.json | python3 -m json.tool\nCommon issues: trailing commas, missing quotes, unescaped characters\nReset: openclaw config init --force"},
        {"pattern": r"MODULE_NOT_FOUND|Cannot find module", "diagnosis": "Missing Node.js module or corrupted installation",
         "category": "startup",
         "fix": "Reinstall:\nnpm uninstall -g @openclaw/cli\nnpm install -g @openclaw/cli\nOr check: npm ls -g @openclaw/cli"},
        {"pattern": r"ETIMEOUT|timeout|timed? ?out", "diagnosis": "Request or connection timeout",
         "category": "general",
         "fix": "1. Check network: curl -s https://openrouter.ai/api/v1/models\n2. Increase timeout: openclaw config set gateway.timeout 120000\n3. Check firewall: ufw status"},
        {"pattern": r"fts5.*syntax|FTS.*error", "diagnosis": "FTS5 full-text search syntax error — query contains special characters",
         "category": "mcp",
         "fix": "This is handled automatically by the MCP server's LIKE fallback.\nIf you see this in your own code, wrap FTS5 MATCH queries in try/except."},
        {"pattern": r"database is locked|SQLITE_BUSY", "diagnosis": "SQLite database lock — concurrent write conflict",
         "category": "general",
         "fix": "Ensure only one uvicorn worker:\nCMD [\"uvicorn\", \"server:app\", \"--workers\", \"1\"]\nCheck for zombie processes: ps aux | grep uvicorn"},
        {"pattern": r"certificate.*expired|SSL.*error|CERT", "diagnosis": "TLS/SSL certificate error",
         "category": "general",
         "fix": "Check certificate:\nopenssl s_client -connect mcp.easyclaw.help:443\nRenew: docker restart traefik\nOr check Let's Encrypt logs in Traefik"},
        {"pattern": r"ExecStart.*not found|exec format error", "diagnosis": "Systemd service ExecStart binary not found",
         "category": "startup",
         "fix": "Check binary path:\nwhich node\nwhich openclaw\nUpdate service file:\nsystemctl edit openclaw --full\nReload: systemctl daemon-reload"},
        {"pattern": r"signal.*SIGKILL|killed|OOMKill", "diagnosis": "Process killed by OOM killer or signal",
         "category": "startup",
         "fix": "Check dmesg for OOM:\ndmesg | grep -i oom\nAdd swap or reduce memory usage:\nUse smaller model, reduce max tokens, limit concurrent requests"},
        {"pattern": r"webhook.*fail|webhook.*error|webhook.*timeout", "diagnosis": "Webhook delivery failed — URL unreachable or rejected",
         "category": "channels",
         "fix": "1. Verify webhook URL is publicly accessible\n2. Check firewall allows inbound on webhook port\n3. Test: curl -X POST https://your-server.com/webhook/test\n4. Check channel platform webhook settings"},
        {"pattern": r"bot.*blocked|bot.*banned|forbidden", "diagnosis": "Bot account blocked or banned by the platform",
         "category": "channels",
         "fix": "1. Check bot status on the platform dashboard\n2. For Telegram: message @BotFather to check\n3. Create a new bot if banned\n4. Review platform TOS for violations"},
        {"pattern": r"token.*invalid|token.*expired|token.*revoked", "diagnosis": "Authentication token invalid, expired, or revoked",
         "category": "channels",
         "fix": "1. Generate new token from the platform\n2. Update config: openclaw config set channels.<name>.token \"new-token\"\n3. Restart gateway: systemctl restart openclaw-gateway"},
        {"pattern": r"mcp.*connection|mcp.*refused|mcp.*timeout", "diagnosis": "MCP knowledge base connection failure",
         "category": "mcp",
         "fix": "1. Check MCP server: curl https://mcp.easyclaw.help/health\n2. Run without MCP: python -m easyclaw_installer --no-mcp\n3. Check DNS: dig mcp.easyclaw.help\n4. Verify API key in config"},
    ]

    error_lower = error_text.lower()
    matched = []
    for p in patterns:
        if re.search(p["pattern"], error_text, re.IGNORECASE):
            matched.append(p)

    if not matched:
        # No pattern match — provide generic advice
        log_tool_usage(auth, "diagnose_issue", int((time.time() - start) * 1000))
        return (
            f"## Diagnosis: Unknown Error\n\n"
            f"No known pattern matched for this error. Try these steps:\n\n"
            f"1. Check logs: `journalctl -u openclaw -n 50`\n"
            f"2. Check gateway logs: `journalctl -u openclaw-gateway -n 50`\n"
            f"3. Run health check: `openclaw health --full`\n"
            f"4. Search knowledge base: use `search_knowledge` with keywords from the error\n"
            f"5. Search the web: use `search_web` with the error message\n\n"
            f"**Error text**: {error_text[:500]}"
        )

    output_parts = [f"## Diagnosis Results\n\nFound {len(matched)} matching pattern(s):\n"]
    for i, m in enumerate(matched, 1):
        output_parts.append(
            f"### {i}. {m['diagnosis']}\n"
            f"**Category**: {m['category']}\n\n"
            f"**Fix**:\n```bash\n{m['fix']}\n```\n"
        )

    log_tool_usage(auth, "diagnose_issue", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# TOOL 11: validate_deployment (subscriber tier)
# ===================================================================

@mcp.tool()
def validate_deployment(server_url: str = "http://localhost:3100") -> str:
    """Run a 9-step deployment health check with verification commands.
    Requires subscriber tier.
    """
    start = time.time()
    auth = _require_auth("validate_deployment", "subscriber")

    checks = [
        {
            "name": "Node.js Version",
            "command": "node --version",
            "expected": "v18+ or v20+",
            "description": "Node.js 18+ is required for OpenClaw",
        },
        {
            "name": "OpenClaw CLI",
            "command": "openclaw --version",
            "expected": "Version string output",
            "description": "The @openclaw/cli package must be globally installed",
        },
        {
            "name": "Configuration File",
            "command": "cat /etc/openclaw/openclaw.json | python3 -m json.tool > /dev/null && echo 'Valid JSON'",
            "expected": "Valid JSON",
            "description": "Config file must exist and contain valid JSON",
        },
        {
            "name": "Config Validation",
            "command": "openclaw config validate",
            "expected": "All checks pass",
            "description": "OpenClaw config validation against required fields",
        },
        {
            "name": "Workspace Directory",
            "command": "ls -la /var/lib/openclaw/",
            "expected": "Directory listing with SOUL.md, USER.md, memory/",
            "description": "Workspace must be initialized with required files",
        },
        {
            "name": "Server Service",
            "command": "systemctl is-active openclaw",
            "expected": "active",
            "description": "The openclaw systemd service must be running",
        },
        {
            "name": "Gateway Service",
            "command": "systemctl is-active openclaw-gateway",
            "expected": "active",
            "description": "The openclaw-gateway systemd service must be running",
        },
        {
            "name": "Server Health",
            "command": f"curl -s {server_url}/health",
            "expected": "JSON response with status: ok",
            "description": "Server HTTP health endpoint must respond",
        },
        {
            "name": "Full Health Check",
            "command": "openclaw health --full",
            "expected": "All checks OK",
            "description": "Comprehensive health check covering all subsystems",
        },
    ]

    output_parts = ["## Deployment Validation Checklist\n"]
    output_parts.append("Run each command to verify your deployment:\n")

    for i, check in enumerate(checks, 1):
        output_parts.append(
            f"### {i}. {check['name']}\n"
            f"{check['description']}\n\n"
            f"```bash\n{check['command']}\n```\n"
            f"**Expected**: {check['expected']}\n"
        )

    output_parts.append(
        "---\n"
        "If any check fails, use `diagnose_issue` with the error output "
        "for step-by-step troubleshooting."
    )

    log_tool_usage(auth, "validate_deployment", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# TOOL 12: get_foreman_config (subscriber tier)
# ===================================================================

@mcp.tool()
def get_foreman_config(hostname: str = "my-server",
                       model: str = "moonshotai/kimi-k2.5",
                       channels: str = "telegram") -> str:
    """Generate a complete, validated OpenClaw configuration including
    systemd service files. Requires subscriber tier.

    channels: comma-separated list (telegram, discord, slack, whatsapp, matrix)
    """
    start = time.time()
    auth = _require_auth("get_foreman_config", "subscriber")

    channel_list = [c.strip().lower() for c in channels.split(",")]

    # Build channel config
    channel_configs = {}
    for ch in channel_list:
        if ch == "telegram":
            channel_configs["telegram"] = {
                "enabled": True,
                "token": "YOUR_TELEGRAM_BOT_TOKEN",
                "allowedUsers": [],
                "webhook": True,
                "webhookUrl": f"https://{hostname}/webhook/telegram"
            }
        elif ch == "discord":
            channel_configs["discord"] = {
                "enabled": True,
                "token": "YOUR_DISCORD_BOT_TOKEN",
                "allowedGuilds": [],
                "allowedChannels": [],
            }
        elif ch == "slack":
            channel_configs["slack"] = {
                "enabled": True,
                "botToken": "xoxb-YOUR-SLACK-TOKEN",
                "signingSecret": "YOUR_SIGNING_SECRET",
                "allowedChannels": [],
            }
        elif ch == "whatsapp":
            channel_configs["whatsapp"] = {
                "enabled": True,
                "provider": "evolution-api",
                "apiUrl": "http://localhost:8080",
                "instance": "openclaw",
                "allowedNumbers": [],
            }
        elif ch == "matrix":
            channel_configs["matrix"] = {
                "enabled": True,
                "homeserver": "https://matrix.org",
                "userId": "@bot:matrix.org",
                "accessToken": "YOUR_MATRIX_TOKEN",
                "allowedRooms": [],
            }

    full_config = {
        "server": {"port": 3100, "host": "127.0.0.1"},
        "gateway": {"port": 3200, "host": "127.0.0.1", "serverUrl": "http://127.0.0.1:3100", "timeout": 60000},
        "openrouter": {
            "model": model,
            "subAgentModel": "minimax/minimax-m2.1",
            "maxTokens": 8192,
            "temperature": 0.7,
        },
        "memory": {"type": "file", "path": "/var/lib/openclaw/memory", "autoSummarize": True},
        "channels": channel_configs,
        "security": {
            "rateLimit": {"enabled": True, "maxRequestsPerMinute": 30, "maxCostPerDay": 10.00},
            "inputValidation": {"maxMessageLength": 4000, "stripHtml": True, "contentFilter": "moderate"},
        },
        "logging": {"level": "info", "file": "/var/log/openclaw/openclaw.log", "requests": True, "sensitiveDataMasking": True, "rotate": True},
    }

    config_json = json.dumps(full_config, indent=2)

    # Systemd service files
    node_path = "/usr/bin/node"
    server_service = f"""[Unit]
Description=OpenClaw Server
After=network.target

[Service]
Type=simple
ExecStart={node_path} /usr/lib/node_modules/@openclaw/cli/lib/server.js
WorkingDirectory=/var/lib/openclaw
Environment=NODE_ENV=production
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target"""

    gateway_service = f"""[Unit]
Description=OpenClaw Gateway
After=network.target openclaw.service
Requires=openclaw.service

[Service]
Type=simple
ExecStart={node_path} /usr/lib/node_modules/@openclaw/cli/lib/gateway.js
WorkingDirectory=/var/lib/openclaw
Environment=NODE_ENV=production
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target"""

    output = (
        f"## Complete OpenClaw Configuration for {hostname}\n\n"
        f"### 1. Configuration File\n"
        f"Save to `/etc/openclaw/openclaw.json`:\n\n"
        f"```json\n{config_json}\n```\n\n"
        f"### 2. Server Service\n"
        f"Save to `/etc/systemd/system/openclaw.service`:\n\n"
        f"```ini\n{server_service}\n```\n\n"
        f"### 3. Gateway Service\n"
        f"Save to `/etc/systemd/system/openclaw-gateway.service`:\n\n"
        f"```ini\n{gateway_service}\n```\n\n"
        f"### 4. Deploy Commands\n"
        f"```bash\n"
        f"# Set API key\n"
        f"export OPENROUTER_API_KEY=sk-or-v1-your-key-here\n\n"
        f"# Enable and start services\n"
        f"systemctl daemon-reload\n"
        f"systemctl enable openclaw openclaw-gateway\n"
        f"systemctl start openclaw openclaw-gateway\n\n"
        f"# Verify\n"
        f"openclaw health --full\n"
        f"```\n\n"
        f"**Remember**: Replace all `YOUR_*` placeholders with real tokens."
    )

    log_tool_usage(auth, "get_foreman_config", int((time.time() - start) * 1000))
    return output


# ===================================================================
# TOOL 13: troubleshoot_gateway (subscriber tier)
# ===================================================================

@mcp.tool()
def troubleshoot_gateway(issue_type: str = "not_starting") -> str:
    """Guided diagnostic flowcharts for 7 gateway failure categories.
    Requires subscriber tier.

    issue_type: not_starting, crashing, no_response, auth_failed,
                channel_disconnected, mcp_timeout, config_rejected
    """
    start = time.time()
    auth = _require_auth("troubleshoot_gateway", "subscriber")

    flowcharts = {
        "not_starting": {
            "title": "Gateway Not Starting",
            "steps": [
                {"check": "Is the service enabled?", "command": "systemctl is-enabled openclaw-gateway", "if_fail": "Run: systemctl enable openclaw-gateway"},
                {"check": "Can systemd find the binary?", "command": "cat /etc/systemd/system/openclaw-gateway.service | grep ExecStart", "if_fail": "Verify the ExecStart path exists: which node; ls /usr/lib/node_modules/@openclaw/cli/lib/gateway.js"},
                {"check": "Is the port available?", "command": "lsof -i :3200", "if_fail": "Kill conflicting process or change port"},
                {"check": "Is the server running first?", "command": "systemctl is-active openclaw", "if_fail": "Start server first: systemctl start openclaw"},
                {"check": "Any errors in journal?", "command": "journalctl -u openclaw-gateway -n 30 --no-pager", "if_fail": "Read the error messages and use diagnose_issue"},
            ],
        },
        "crashing": {
            "title": "Gateway Crashing / Restarting",
            "steps": [
                {"check": "Check crash logs", "command": "journalctl -u openclaw-gateway --since '1 hour ago' | grep -i 'error\\|fatal\\|crash'", "if_fail": "Use diagnose_issue with the error text"},
                {"check": "Check memory usage", "command": "free -h && ps aux --sort=-%mem | head -5", "if_fail": "Add swap or reduce memory: use smaller model"},
                {"check": "Check config validity", "command": "openclaw config validate", "if_fail": "Fix config errors, then restart"},
                {"check": "Check Node.js version", "command": "node --version", "if_fail": "Install Node.js 20 LTS: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt install -y nodejs"},
                {"check": "Check for core dump", "command": "coredumpctl list 2>/dev/null | tail -5", "if_fail": "Report to OpenClaw with the core dump details"},
            ],
        },
        "no_response": {
            "title": "Gateway Running But No Response",
            "steps": [
                {"check": "Is the process actually running?", "command": "ps aux | grep gateway", "if_fail": "Restart: systemctl restart openclaw-gateway"},
                {"check": "Is it listening on the right port?", "command": "ss -tlnp | grep 3200", "if_fail": "Check gateway.port in config: openclaw config validate"},
                {"check": "Can you reach it locally?", "command": "curl -s http://127.0.0.1:3200/health", "if_fail": "Check bind address — should be 127.0.0.1 not a remote IP"},
                {"check": "Is the firewall blocking?", "command": "ufw status && iptables -L -n | head -20", "if_fail": "Allow the port: ufw allow 3200/tcp (if direct access needed)"},
                {"check": "Is the reverse proxy routing correctly?", "command": "curl -s https://your-domain.com/health", "if_fail": "Check Traefik/nginx config — ensure routing to port 3200"},
            ],
        },
        "auth_failed": {
            "title": "Authentication Failures",
            "steps": [
                {"check": "Is the API key set?", "command": "echo $OPENROUTER_API_KEY | head -c 20", "if_fail": "Set it: export OPENROUTER_API_KEY=sk-or-v1-..."},
                {"check": "Is the key valid?", "command": "curl -s -H 'Authorization: Bearer $OPENROUTER_API_KEY' https://openrouter.ai/api/v1/models | head -c 100", "if_fail": "Regenerate at https://openrouter.ai/keys"},
                {"check": "Does the config reference it correctly?", "command": "openclaw config validate", "if_fail": "Fix the apiKey field or use env var"},
                {"check": "Check for whitespace issues", "command": "cat /etc/openclaw/openclaw.json | python3 -c 'import json,sys; c=json.load(sys.stdin); print(repr(c.get(\"openrouter\",{}).get(\"apiKey\",\"NOT SET\")))'", "if_fail": "Remove trailing spaces/newlines from the API key"},
            ],
        },
        "channel_disconnected": {
            "title": "Channel Disconnected",
            "steps": [
                {"check": "Which channels are configured?", "command": "openclaw channels list", "if_fail": "No channels configured — run: openclaw channels init <channel>"},
                {"check": "Is the bot token valid?", "command": "Check token on the platform's bot management page", "if_fail": "Generate a new token from the platform"},
                {"check": "Is the webhook URL reachable?", "command": "curl -s -o /dev/null -w '%{http_code}' https://your-server.com/webhook/telegram", "if_fail": "Ensure the URL is publicly accessible and returns 200"},
                {"check": "Any channel-specific errors?", "command": "journalctl -u openclaw-gateway --since '1 hour ago' | grep -i 'telegram\\|discord\\|slack\\|whatsapp'", "if_fail": "Use diagnose_issue with the channel error"},
            ],
        },
        "mcp_timeout": {
            "title": "MCP Connection Timeout",
            "steps": [
                {"check": "Is the MCP server reachable?", "command": "curl -s https://mcp.easyclaw.help/health", "if_fail": "MCP server may be down — use --no-mcp to run without it"},
                {"check": "DNS resolution working?", "command": "dig +short mcp.easyclaw.help", "if_fail": "DNS issue — check /etc/resolv.conf or try: echo 'nameserver 8.8.8.8' >> /etc/resolv.conf"},
                {"check": "Is there a firewall blocking outbound?", "command": "curl -s https://httpbin.org/ip", "if_fail": "Firewall blocking outbound HTTPS — contact your hosting provider"},
                {"check": "Is the API key valid for MCP?", "command": "Check your EasyClaw subscription at easyclaw.help", "if_fail": "Subscribe or renew at https://easyclaw.help"},
            ],
        },
        "config_rejected": {
            "title": "Configuration Rejected",
            "steps": [
                {"check": "Is the JSON valid?", "command": "cat /etc/openclaw/openclaw.json | python3 -m json.tool", "if_fail": "Fix JSON syntax — look for trailing commas, missing quotes"},
                {"check": "Any unknown keys?", "command": "openclaw config validate 2>&1 | grep -i 'unknown\\|invalid'", "if_fail": "Remove unknown config keys — check configuration_reference guide"},
                {"check": "Are required fields present?", "command": "openclaw config validate", "if_fail": "Add missing required fields"},
                {"check": "Reset to defaults?", "command": "openclaw config init --force", "if_fail": "If this also fails, check file permissions on /etc/openclaw/"},
            ],
        },
    }

    issue = issue_type.lower().strip()
    if issue not in flowcharts:
        log_tool_usage(auth, "troubleshoot_gateway", int((time.time() - start) * 1000), False)
        return f"Unknown issue type '{issue_type}'. Available: {', '.join(flowcharts.keys())}"

    flow = flowcharts[issue]
    output_parts = [f"## Troubleshooting: {flow['title']}\n"]
    output_parts.append("Follow these steps in order. Stop at the first failure and apply the fix.\n")

    for i, step in enumerate(flow["steps"], 1):
        output_parts.append(
            f"### Step {i}: {step['check']}\n"
            f"```bash\n{step['command']}\n```\n"
            f"**If it fails**: {step['if_fail']}\n"
        )

    output_parts.append(
        "---\n"
        "If all steps pass but the issue persists, use `search_web` "
        "with the specific error message for additional solutions."
    )

    log_tool_usage(auth, "troubleshoot_gateway", int((time.time() - start) * 1000))
    return "\n".join(output_parts)


# ===================================================================
# Admin REST API (Starlette)
# ===================================================================

def _admin_auth(request: Request) -> bool:
    """Check X-Admin-Secret header."""
    secret = request.headers.get("X-Admin-Secret", "")
    return verify_admin_secret(secret)


async def health_endpoint(request: Request) -> JSONResponse:
    """Health check — no auth required."""
    guide_count = len(KNOWLEDGE_BASE)
    # Add dynamic entries count
    dynamic = list_knowledge_entries()
    total_guides = guide_count + len([d for d in dynamic if d["topic"] not in KNOWLEDGE_BASE])

    return JSONResponse({
        "status": "ok",
        "service": "easyclaw-mcp",
        "version": VERSION,
        "tools": 13,
        "guides": total_guides,
        "timestamp": datetime.now(timezone.utc).isoformat(),
    })


async def admin_create_user(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)
    email = body.get("email")
    if not email:
        return JSONResponse({"error": "email required"}, status_code=400)
    user = create_user(email)
    return JSONResponse(user)


async def admin_create_subscription(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)

    user_id = body.get("user_id")
    tier = body.get("tier", "subscriber")
    api_key = body.get("api_key", f"ec_live_{uuid.uuid4().hex[:48]}")

    if not user_id:
        return JSONResponse({"error": "user_id required"}, status_code=400)

    sub = create_subscription(
        user_id=user_id,
        tier=tier,
        api_key=api_key,
        stripe_customer_id=body.get("stripe_customer_id"),
        stripe_subscription_id=body.get("stripe_subscription_id"),
        current_period_end=body.get("current_period_end"),
    )
    return JSONResponse(sub)


async def admin_update_subscription_status(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)

    status = body.get("status")
    if not status:
        return JSONResponse({"error": "status required"}, status_code=400)

    success = update_subscription_status(
        api_key_hash=body.get("api_key_hash"),
        stripe_subscription_id=body.get("stripe_subscription_id"),
        status=status,
    )
    if success:
        return JSONResponse({"ok": True})
    return JSONResponse({"error": "subscription not found"}, status_code=404)


async def admin_get_usage(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    api_key_hash = request.query_params.get("api_key_hash")
    if not api_key_hash:
        return JSONResponse({"error": "api_key_hash required"}, status_code=400)
    days = int(request.query_params.get("days", "30"))
    stats = get_usage_stats(api_key_hash, days)
    return JSONResponse(stats)


async def admin_lookup(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    email = request.query_params.get("email")
    if not email:
        return JSONResponse({"error": "email required"}, status_code=400)
    result = lookup_by_email(email)
    return JSONResponse(result)


async def admin_upsert_knowledge(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)

    topic = body.get("topic")
    title = body.get("title")
    content = body.get("content")
    source = body.get("source", "manual")

    if not all([topic, title, content]):
        return JSONResponse({"error": "topic, title, content required"}, status_code=400)

    entry = upsert_knowledge_entry(topic, title, content, source)
    return JSONResponse(entry)


async def admin_list_knowledge(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    entries = list_knowledge_entries()
    return JSONResponse(entries)


async def admin_reload_knowledge(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    _rebuild_knowledge_fts()
    topics = list_guide_topics()
    return JSONResponse({"ok": True, "total_guides": len(topics)})


async def admin_delete_knowledge(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    topic = request.path_params.get("topic")
    if not topic:
        return JSONResponse({"error": "topic required"}, status_code=400)
    deleted = delete_knowledge_entry(topic)
    if deleted:
        return JSONResponse({"ok": True})
    return JSONResponse({"error": "topic not found"}, status_code=404)


async def admin_generate_otp(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)
    email = body.get("email")
    if not email:
        return JSONResponse({"error": "email required"}, status_code=400)
    ttl = body.get("ttl_seconds", 600)
    code = generate_otp(email, ttl)
    return JSONResponse({"email": email, "code": code, "ttl_seconds": ttl})


async def admin_verify_otp(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)
    email = body.get("email")
    code = body.get("code")
    if not email or not code:
        return JSONResponse({"error": "email and code required"}, status_code=400)
    valid = verify_otp(email, code)
    return JSONResponse({"valid": valid})


async def admin_create_guide_purchase(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    try:
        body = await request.json()
    except Exception:
        return JSONResponse({"error": "invalid JSON body"}, status_code=400)
    email = body.get("email")
    guide_type = body.get("guide_type")
    if not email or not guide_type:
        return JSONResponse({"error": "email and guide_type required"}, status_code=400)
    purchase = create_guide_purchase(
        email, guide_type, body.get("stripe_session_id")
    )
    return JSONResponse(purchase)


async def admin_validate_download(request: Request) -> JSONResponse:
    if not _admin_auth(request):
        return JSONResponse({"error": "unauthorized"}, status_code=401)
    token = request.query_params.get("token")
    if not token:
        return JSONResponse({"error": "token required"}, status_code=400)
    result = validate_download_token(token)
    if result:
        return JSONResponse(result)
    return JSONResponse({"error": "invalid token"}, status_code=404)


# ===================================================================
# Build Starlette admin app
# ===================================================================

admin_routes = [
    Route("/health", health_endpoint, methods=["GET"]),
    Route("/admin/users", admin_create_user, methods=["POST"]),
    Route("/admin/subscriptions", admin_create_subscription, methods=["POST"]),
    Route("/admin/subscriptions/status", admin_update_subscription_status, methods=["PUT"]),
    Route("/admin/usage", admin_get_usage, methods=["GET"]),
    Route("/admin/lookup", admin_lookup, methods=["GET"]),
    Route("/admin/knowledge", admin_upsert_knowledge, methods=["POST"]),
    Route("/admin/knowledge", admin_list_knowledge, methods=["GET"]),
    Route("/admin/knowledge/reload", admin_reload_knowledge, methods=["POST"]),
    Route("/admin/knowledge/{topic}", admin_delete_knowledge, methods=["DELETE"]),
    Route("/admin/otp/generate", admin_generate_otp, methods=["POST"]),
    Route("/admin/otp/verify", admin_verify_otp, methods=["POST"]),
    Route("/admin/guide-purchases", admin_create_guide_purchase, methods=["POST"]),
    Route("/admin/validate-download", admin_validate_download, methods=["GET"]),
]


def _build_admin_app() -> Starlette:
    """Build the Starlette admin application with CORS."""
    admin = Starlette(routes=admin_routes)
    admin.add_middleware(
        CORSMiddleware,
        allow_origins=[
            "https://easyclaw.help",
            "https://n8n.srv1071455.hstgr.cloud",
        ],
        allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        allow_headers=["*"],
    )
    return admin


# ===================================================================
# Raw ASGI Dispatcher — avoids Starlette Mount redirect loops
# ===================================================================

def create_app():
    """Application factory. Called once at import time."""
    # Initialize database and FTS5 index
    init_db()

    # Build sub-applications
    admin_app = _build_admin_app()
    mcp_app = mcp.http_app(stateless_http=True)

    async def combined_app(scope, receive, send):
        """Route requests to admin or MCP based on path."""
        if scope["type"] == "lifespan":
            # FastMCP MUST receive lifespan to init its session manager
            await mcp_app(scope, receive, send)
            return

        path = scope.get("path", "")

        # Admin routes and health check → Starlette
        if path.startswith("/admin") or path == "/health":
            await admin_app(scope, receive, send)
        else:
            # MCP routes → FastMCP with auth context injection
            auth_header = extract_auth_header(scope)
            auth_result = authenticate_request(auth_header)
            token = _auth_context.set(auth_result)
            try:
                await mcp_app(scope, receive, send)
            finally:
                _auth_context.reset(token)

    return combined_app


# Module-level app for uvicorn: `uvicorn server:app`
app = create_app()
