# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Action Router — intercepts common user messages and handles them
without involving the LLM.

The goal: a 0.5B model should never be asked to figure out "show my
calendar" means "navigate to the Calendar tab."  Pattern-match first,
fall through to the LLM only when no pattern matches.

Return format:
    match_action(message) → ActionResult | None

    ActionResult is a dict with:
        type: "navigate" | "api_call" | "quick_reply" | "search" | "provision"
        panel: tab name to navigate to (for "navigate")
        response: text to show the user
        data: optional structured payload
        refresh: whether to refresh the target panel after navigating

    None means no match — fall through to the LLM.
"""

import logging
import re
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Pattern registry
# ---------------------------------------------------------------------------

# Each entry: (compiled regex, handler function name)
# Patterns are tested in order; first match wins.
# Use (?i) for case-insensitive.  Keep patterns simple — this runs on
# every chat message, so no heavy computation.

_ActionResult = Optional[Dict[str, Any]]


def _nav(panel: str, response: str, refresh: bool = True) -> Dict[str, Any]:
    """Build a navigate action."""
    return {"type": "navigate", "panel": panel, "response": response, "refresh": refresh}


def _reply(response: str, data: Optional[dict] = None) -> Dict[str, Any]:
    """Build a quick reply action (no navigation)."""
    result: Dict[str, Any] = {"type": "quick_reply", "response": response}
    if data:
        result["data"] = data
    return result


def _api(endpoint: str, method: str, response: str, body: Optional[dict] = None) -> Dict[str, Any]:
    """Build an API call action."""
    return {
        "type": "api_call",
        "endpoint": endpoint,
        "method": method,
        "body": body or {},
        "response": response,
    }


def _provision(service_key: str) -> Dict[str, Any]:
    """Build a provision action."""
    return {
        "type": "provision",
        "service": service_key,
        "response": f"Deploying {service_key}...",
    }


def _search(query: str) -> Dict[str, Any]:
    """Build a search action."""
    return {
        "type": "search",
        "query": query,
        "response": f"Searching for: {query}",
    }


# ---------------------------------------------------------------------------
# Handlers — each returns ActionResult or None
# ---------------------------------------------------------------------------


def _handle_email(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to email panel."""
    return _nav("email", "Here's your email.")


def _handle_calendar(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to calendar panel."""
    return _nav("calendar", "Here's your calendar.")


def _handle_tasks(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to tasks panel."""
    return _nav("tasks", "Here are your tasks.")


def _handle_workspace(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to workspace / command center."""
    return _nav("workspace", "Here's your workspace.")


def _handle_server(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to server panel."""
    return _nav("server", "Here's your server status.")


def _handle_memory(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to memory panel."""
    return _nav("memory", "Here's your memory.")


def _handle_settings(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to settings panel."""
    return _nav("settings", "Here are your settings.")


def _handle_connect(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to connect/services panel."""
    return _nav("connect", "Here are your connected services.")


def _handle_models(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to models panel."""
    return _nav("models", "Here are your models.")


def _handle_skills(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to skills panel."""
    return _nav("skills", "Here are your available skills.")


def _handle_nodes(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to mesh nodes panel."""
    return _nav("nodes", "Here are your mesh nodes.")


def _handle_messages(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to messages panel."""
    return _nav("messages", "Here are your messages.")


def _handle_activity(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to activity/analytics panel."""
    return _nav("activity", "Here's your activity log.")


def _handle_briefing(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to briefing panel."""
    return _nav("briefing", "Here's your daily briefing.")


def _handle_media(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to media panel."""
    return _nav("media", "Here's your media browser.")


def _handle_gallery(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to gallery panel."""
    return _nav("gallery", "Here's your gallery.")


def _handle_social(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to social panel."""
    return _nav("social", "Here are your social feeds.")


def _handle_training(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to training panel."""
    return _nav("training", "Here's your training dashboard.")


def _handle_marketplace(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to marketplace panel."""
    return _nav("marketplace", "Here's the marketplace.")


def _handle_deploy_service(msg: str, match: re.Match) -> _ActionResult:
    """Deploy a specific service by name."""
    service_name = match.group(1).strip().lower()
    # Map common names to service keys
    _name_map = {
        "mealie": "mealie",
        "recipe": "mealie",
        "recipes": "mealie",
        "gitea": "gitea",
        "forgejo": "gitea",
        "git": "gitea",
        "nextcloud": "nextcloud",
        "cloud": "nextcloud",
        "files": "nextcloud",
        "jellyfin": "jellyfin",
        "media server": "jellyfin",
        "joplin": "joplin",
        "notes": "joplin",
        "pihole": "pihole",
        "pi-hole": "pihole",
        "dns": "pihole",
        "searxng": "searxng",
        "searx": "searxng",
        "search engine": "searxng",
        "wazuh": "wazuh_manager",
        "siem": "wazuh_manager",
        "crowdsec": "crowdsec",
        "spiderfoot": "spiderfoot",
        "osint": "spiderfoot",
        "netdata": "netdata",
        "monitoring": "netdata",
        "uptime kuma": "uptime_kuma",
        "uptime": "uptime_kuma",
        "cockpit": "cockpit",
        "gotosocial": "gotosocial",
        "mastodon": "gotosocial",
        "fediverse": "gotosocial",
        "bluesky": "bluesky_pds",
        "bluesky pds": "bluesky_pds",
    }
    key = _name_map.get(service_name)
    if not key:
        # Try direct match
        key = service_name.replace(" ", "_").replace("-", "_")
    return _provision(key)


def _handle_deploy_all(msg: str, match: re.Match) -> _ActionResult:
    """Full server setup — bootstrap runtime + deploy all services."""
    return {
        "type": "api_call",
        "endpoint": "/api/server/full-setup",
        "method": "POST",
        "body": {},
        "response": "Setting up your server — installing runtime if needed, then deploying all services...",
        "panel": "server",
    }


def _handle_setup(msg: str, match: re.Match) -> _ActionResult:
    """Navigate to workspace setup wizard tab."""
    return _nav("workspace", "Here's your setup wizard. Let's get everything configured.")


def _handle_web_search(msg: str, match: re.Match) -> _ActionResult:
    """Execute a web search directly."""
    query = match.group(1).strip()
    if not query:
        return None
    # Don't intercept searches that target specific sources (email, files, etc.)
    # Those need the LLM to pick the right tool.
    _source_words = {
        "my email",
        "my inbox",
        "my files",
        "my documents",
        "my drive",
        "my calendar",
        "my tasks",
        "my contacts",
        "my notes",
    }
    query_lower = query.lower()
    for sw in _source_words:
        if sw in query_lower:
            return None
    return _search(query)


def _handle_status(msg: str, match: re.Match) -> _ActionResult:
    """Show system status — navigate to workspace."""
    return _nav("workspace", "Here's your current status overview.")


def _handle_help(msg: str, match: re.Match) -> _ActionResult:
    """Show help text — no LLM needed."""
    return _reply(
        "Here's what I can help with:\n\n"
        '**Navigation** — "show my email", "open calendar", "check tasks"\n'
        '**Server** — "deploy mealie", "deploy all services", "server status"\n'
        '**Search** — "search for <query>"\n'
        '**Setup** — "set up my services", "connect stripe"\n'
        '**Info** — "what\'s my status", "show briefing"\n\n'
        "Or just ask me anything — I'll use my full capabilities to help."
    )


# ---------------------------------------------------------------------------
# Pattern table
# ---------------------------------------------------------------------------

# (regex_pattern, handler_function)
# Patterns are tested in order. First match wins.
# All patterns are case-insensitive via re.IGNORECASE flag.

_PATTERNS: List[Tuple[re.Pattern, Any]] = []


def _p(pattern: str, handler):
    """Register a pattern."""
    _PATTERNS.append((re.compile(pattern, re.IGNORECASE), handler))


# -- Navigation patterns --
# Common "show me the / show my / show me my" fragment
_ME = r"(?:me\s+)?(?:the\s+|my\s+)?"

# Email
_p(rf"^(?:show|check|open|view|read|get)\s+{_ME}(?:email|inbox|mail)s?$", _handle_email)
_p(r"^(?:any\s+)?(?:new\s+)?(?:email|mail)s?\??$", _handle_email)
_p(r"^what(?:'s| is)\s+in\s+my\s+(?:email|inbox|mail)\??$", _handle_email)
_p(r"^(?:email|inbox|mail)$", _handle_email)

# Calendar
_p(
    rf"^(?:show|check|open|view|get)\s+{_ME}(?:calendar|schedule|events|agenda)s?$",
    _handle_calendar,
)
_p(
    r"^what(?:'s| is)\s+(?:on\s+)?(?:my\s+)?(?:calendar|schedule|agenda)\s*(?:today|this week)?\??$",
    _handle_calendar,
)
_p(r"^(?:any\s+)?(?:meetings?|events?|appointments?)\s*(?:today|this week)?\??$", _handle_calendar)
_p(
    r"^(?:what(?:'s| is)\s+)?(?:coming up|next on|on)\s*(?:my\s+)?(?:calendar|schedule)?\??$",
    _handle_calendar,
)
_p(r"^calendar$", _handle_calendar)

# Tasks
_p(rf"^(?:show|check|open|view|get|list)\s+{_ME}tasks?$", _handle_tasks)
_p(r"^(?:what(?:'s| is)\s+)?(?:my\s+)?(?:to.?do|task)\s*(?:list)?\??$", _handle_tasks)
_p(r"^tasks?$", _handle_tasks)

# Workspace / Command Center / Dashboard
_p(
    rf"^(?:show|open|view)\s+{_ME}(?:workspace|command center|dashboard|hq|headquarters)$",
    _handle_workspace,
)
_p(r"^(?:workspace|command center|hq)$", _handle_workspace)

# Server
_p(rf"^(?:show|check|open|view)\s+{_ME}(?:server|services|containers?)s?$", _handle_server)
_p(r"^(?:server|service)\s*(?:status)?\??$", _handle_server)
_p(r"^what(?:'s|\s+is)\s+(?:running|on)\s+(?:my\s+)?server\??$", _handle_server)
_p(r"^what(?:'s|\s+is)\s+running\s+on\s+(?:my\s+)?server\??$", _handle_server)
_p(r"^(?:what(?:'s| is)\s+)?(?:my\s+)?server\s*(?:status|doing)?\??$", _handle_server)

# Memory
_p(rf"^(?:show|check|open|view)\s+{_ME}(?:memory|memories|remember)$", _handle_memory)
_p(r"^(?:memory|memories)$", _handle_memory)

# Settings
_p(rf"^(?:show|open|view|change)\s+{_ME}settings$", _handle_settings)
_p(r"^settings$", _handle_settings)

# Connect / Services
_p(
    rf"^(?:show|open|view)\s+{_ME}(?:connected?\s*services|connections?|integrations?)$",
    _handle_connect,
)
_p(r"^(?:connections?|integrations?)$", _handle_connect)

# Models
_p(rf"^(?:show|check|open|view)\s+{_ME}models?$", _handle_models)
_p(r"^(?:what\s+)?models?\s*(?:are\s+)?(?:available|loaded|running)\??$", _handle_models)
_p(r"^models?$", _handle_models)

# Skills
_p(rf"^(?:show|list|open|view)\s+{_ME}(?:available\s+)?skills?$", _handle_skills)
_p(r"^(?:what\s+)?(?:can\s+you\s+do|skills?)\??$", _handle_skills)
_p(r"^skills?$", _handle_skills)

# Nodes / Mesh
_p(rf"^(?:show|check|open|view)\s+{_ME}(?:mesh\s+)?nodes?$", _handle_nodes)
_p(r"^(?:mesh|nodes?)$", _handle_nodes)

# Messages
_p(rf"^(?:show|check|open|view|read)\s+{_ME}messages?$", _handle_messages)
_p(r"^messages?$", _handle_messages)

# Activity / Analytics
_p(rf"^(?:show|check|open|view)\s+{_ME}(?:activity|analytics|stats?)$", _handle_activity)
_p(r"^(?:activity|analytics)$", _handle_activity)

# Briefing
_p(rf"^(?:show|open|view|get)\s+{_ME}(?:daily\s+)?briefing$", _handle_briefing)
_p(r"^(?:give\s+me\s+)?(?:a\s+)?(?:daily\s+)?briefing\??$", _handle_briefing)
_p(r"^briefing$", _handle_briefing)

# Media
_p(r"^(?:show|open|view)\s+(?:my\s+)?(?:media|music|videos?)$", _handle_media)
_p(r"^media$", _handle_media)

# Gallery
_p(r"^(?:show|open|view)\s+(?:my\s+)?(?:gallery|photos?|images?)$", _handle_gallery)
_p(r"^gallery$", _handle_gallery)

# Social
_p(r"^(?:show|open|view)\s+(?:my\s+)?(?:social|feeds?)$", _handle_social)
_p(r"^social$", _handle_social)

# Training
_p(r"^(?:show|open|view)\s+(?:my\s+)?training$", _handle_training)
_p(r"^training$", _handle_training)

# Marketplace
_p(r"^(?:show|open|view)\s+(?:my\s+)?(?:marketplace|market|store)$", _handle_marketplace)
_p(r"^marketplace$", _handle_marketplace)

# "Go to X" / "take me to X" / "navigate to X" — generic panel lookup
_PANEL_ALIASES: dict[str, tuple[str, str]] = {
    "email": ("email", "Here's your email."),
    "inbox": ("email", "Here's your inbox."),
    "mail": ("email", "Here's your email."),
    "calendar": ("calendar", "Here's your calendar."),
    "schedule": ("calendar", "Here's your schedule."),
    "tasks": ("tasks", "Here are your tasks."),
    "workspace": ("workspace", "Here's your workspace."),
    "command center": ("workspace", "Here's your command center."),
    "dashboard": ("workspace", "Here's your dashboard."),
    "server": ("server", "Here's your server status."),
    "services": ("server", "Here's your services."),
    "memory": ("memory", "Here's your memory."),
    "settings": ("settings", "Here are your settings."),
    "connections": ("connect", "Here are your connections."),
    "integrations": ("connect", "Here are your integrations."),
    "models": ("models", "Here are your models."),
    "skills": ("skills", "Here are your skills."),
    "nodes": ("nodes", "Here are your mesh nodes."),
    "mesh": ("nodes", "Here's your mesh."),
    "messages": ("messages", "Here are your messages."),
    "activity": ("activity", "Here's your activity."),
    "analytics": ("activity", "Here's your analytics."),
    "briefing": ("briefing", "Here's your briefing."),
    "media": ("media", "Here's your media."),
    "gallery": ("gallery", "Here's your gallery."),
    "social": ("social", "Here's your social feed."),
    "training": ("training", "Here's your training."),
    "marketplace": ("marketplace", "Here's the marketplace."),
}


def _handle_goto(msg: str, match: re.Match) -> _ActionResult:
    """Handle 'go to X' navigation."""
    target = match.group(1).strip().lower()
    entry = _PANEL_ALIASES.get(target)
    if entry:
        return _nav(entry[0], entry[1])
    return None


_p(rf"^(?:go\s+to|take\s+me\s+to|navigate\s+to|switch\s+to|open)\s+{_ME}(.+?)$", _handle_goto)

# -- Action patterns --

# Setup wizard (must be before deploy to catch "set up my services" vs "set up mealie")
_p(
    r"^(?:set\s*up|setup|configure)\s+(?:my\s+)?(?:services?|stuff|everything|wizard|server)$",
    _handle_setup,
)
_p(r"^what(?:'s|\s+is)\s+not\s+(?:set\s*up|configured)\??$", _handle_setup)
_p(r"^what\s+(?:do\s+i\s+)?need\s+to\s+(?:set\s*up|configure)\??$", _handle_setup)

# Deploy all (must be before deploy-specific to avoid "all services" matching as a service name)
_p(
    r"^(?:deploy|install|set\s*up|provision|start|launch|spin\s*up)\s+(?:all|everything)(?:\s+(?:services?|containers?))?$",
    _handle_deploy_all,
)
_p(r"^auto.?(?:deploy|provision)$", _handle_deploy_all)

# Deploy specific service
_p(
    r"^(?:deploy|install|set\s*up|provision|start|launch|spin\s*up)\s+(.+?)(?:\s+server)?$",
    _handle_deploy_service,
)

# Web search
_p(r"^(?:search|look\s*up|google|find)\s+(?:for\s+)?(.+)$", _handle_web_search)
_p(r"^what\s+is\s+(.{10,})$", _handle_web_search)  # Only for longer queries

# System status
_p(r"^(?:what(?:'s| is)\s+)?(?:my\s+)?(?:status|system\s*status)\??$", _handle_status)
_p(
    r"^(?:how(?:'s| is)\s+)?(?:everything|the system|my system)\s*(?:doing|going|looking)?\??$",
    _handle_status,
)
_p(r"^(?:give\s+me\s+)?(?:a\s+)?status\s*(?:update|report)?\??$", _handle_status)

# Help
_p(r"^(?:what\s+can\s+you\s+do|help\s+me|how\s+do\s+i|help)\??$", _handle_help)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def match_action(message: str) -> _ActionResult:
    """Test a user message against all registered patterns.

    Returns an action dict if a pattern matches, or None to fall through
    to the LLM.
    """
    if not message:
        return None

    text = message.strip()

    # Don't intercept messages with file attachments context
    if text.startswith("[Attached files"):
        return None

    # Don't intercept /commands (handled by wizard system)
    if text.startswith("/"):
        return None

    for pattern, handler in _PATTERNS:
        m = pattern.match(text)
        if m:
            try:
                result = handler(text, m)
                if result is not None:
                    logger.info(
                        "Action router matched: %s → %s", pattern.pattern[:40], result.get("type")
                    )
                    return result
            except Exception as e:
                logger.warning("Action router handler error: %s", e)
                return None

    return None
