# 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

"""
Triggers Skill - The Fates (Moirai)

Event-driven automation for Familiar.

The three Fates:
- Clotho (spinner) - Creates triggers, spins the thread of events
- Lachesis (allotter) - Matches events to actions, measures the thread
- Atropos (unturnable) - Executes actions, cuts the thread

Features:
- Webhook server - receive HTTP callbacks from external services
- Polling triggers - check conditions periodically
- Event matching - pattern-based routing
- Action chains - multi-step workflows
- Conditional logic - if/then/else flows
- Rate limiting - prevent runaway triggers
- Audit logging - track all executions

Use cases:
- New email → triage → notify if urgent
- GitHub push → summarize changes → post to Discord
- Calendar event starting → send reminder
- Stripe payment → log donor → send thank you
- RSS new item → summarize → add to knowledge base
- Cron schedule → daily briefing → send to Telegram
"""

import hashlib
import hmac
import json
import logging
import os
import queue
import re
import threading
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# Storage paths - use centralized paths


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.paths import (
        EXECUTIONS_LOG,
        TRIGGERS_DIR,
        TRIGGERS_FILE,
        WEBHOOKS_FILE,
        ensure_dir,
    )
except ImportError:
    DATA_DIR = _get_data_dir()
    TRIGGERS_DIR = DATA_DIR / "triggers"
    TRIGGERS_FILE = TRIGGERS_DIR / "triggers.json"
    WEBHOOKS_FILE = TRIGGERS_DIR / "webhooks.json"
    EXECUTIONS_LOG = TRIGGERS_DIR / "executions.log"

    def ensure_dir(path):
        path.mkdir(parents=True, exist_ok=True)
        return path


ensure_dir(TRIGGERS_DIR)


class TriggerSystem:
    """
    Singleton manager for the trigger system.

    Usage:
        # Get or create the trigger system
        system = TriggerSystem.get_instance(agent)

        # Access components
        engine = system.engine
        webhook_server = system.webhook_server

        # Start everything
        system.start()
    """

    _instance = None
    _lock = threading.Lock()

    def __init__(self, agent=None):
        self.agent = agent
        self.engine: Optional["TriggerEngine"] = None
        self.webhook_server: Optional["WebhookServer"] = None
        # Limit queue size to prevent memory exhaustion from flooding
        self._event_queue = queue.Queue(maxsize=1000)
        self._started = False

    @classmethod
    def get_instance(cls, agent=None) -> "TriggerSystem":
        """Get or create the singleton instance."""
        with cls._lock:
            if cls._instance is None:
                cls._instance = cls(agent)
            elif agent is not None and cls._instance.agent is None:
                cls._instance.agent = agent
            return cls._instance

    @classmethod
    def reset_instance(cls):
        """Reset the singleton (for testing)."""
        with cls._lock:
            if cls._instance is not None:
                cls._instance.stop()
            cls._instance = None

    def start(self, webhook_port: int = 8765):
        """Start the trigger engine and webhook server."""
        if self._started:
            return

        self.engine = TriggerEngine(self.agent, self._event_queue)
        self.engine.start()

        self.webhook_server = WebhookServer(self.engine, port=webhook_port)
        self.webhook_server.start()

        self._started = True
        logger.info("Trigger system started")

    def stop(self):
        """Stop all components."""
        if self.webhook_server:
            self.webhook_server.stop()
        if self.engine:
            self.engine.stop()
        self._started = False

    def fire_event(self, event_type: str, data: Dict[str, Any], source: str = "internal"):
        """Fire an event to be processed."""
        if self.engine:
            self.engine.fire_event(event_type, data, source)


# Backwards compatibility - module-level accessors
def _get_trigger_engine() -> Optional["TriggerEngine"]:
    """Get the trigger engine from the singleton."""
    system = TriggerSystem._instance
    return system.engine if system else None


def _get_event_queue() -> queue.Queue:
    """Get the event queue from the singleton."""
    system = TriggerSystem._instance
    return system._event_queue if system else queue.Queue()


def get_webhook_secret(trigger_id: str, config_secret: str = "") -> str:
    """
    Get webhook secret, preferring environment variable.

    Checks for: WEBHOOK_SECRET_{TRIGGER_ID} env var first,
    then falls back to config-stored secret.
    """
    # Sanitize trigger_id for env var name
    safe_id = re.sub(r"[^A-Za-z0-9_]", "_", trigger_id).upper()
    env_secret = os.environ.get(f"WEBHOOK_SECRET_{safe_id}", "")

    if env_secret:
        return env_secret

    # Also check generic webhook secret
    generic_secret = os.environ.get("FAMILIAR_WEBHOOK_SECRET", "")
    if generic_secret:
        return generic_secret

    return config_secret


class TriggerType(str, Enum):
    WEBHOOK = "webhook"  # External HTTP callback
    POLL = "poll"  # Periodic check
    SCHEDULE = "schedule"  # Cron-like schedule
    EVENT = "event"  # Internal event bus
    WATCH = "watch"  # File/folder watcher


class ActionType(str, Enum):
    CHAT = "chat"  # Send message to LLM
    TOOL = "tool"  # Call a specific tool
    NOTIFY = "notify"  # Send notification
    WEBHOOK = "webhook"  # Call external webhook
    CHAIN = "chain"  # Run another trigger
    SCRIPT = "script"  # Run shell command


@dataclass
class Condition:
    """A condition that must be met for trigger to fire."""

    field: str  # JSONPath-like field selector
    operator: str  # eq, ne, gt, lt, contains, matches, exists
    value: Any  # Value to compare against

    def evaluate(self, data: dict) -> bool:
        """Evaluate condition against data."""
        # Get field value using dot notation
        field_value = self._get_field(data, self.field)

        if self.operator == "exists":
            return field_value is not None

        if self.operator == "not_exists":
            return field_value is None

        if field_value is None:
            return False

        if self.operator == "eq":
            return field_value == self.value
        elif self.operator == "ne":
            return field_value != self.value
        elif self.operator == "gt":
            return field_value > self.value
        elif self.operator == "lt":
            return field_value < self.value
        elif self.operator == "gte":
            return field_value >= self.value
        elif self.operator == "lte":
            return field_value <= self.value
        elif self.operator == "contains":
            return self.value in str(field_value).lower()
        elif self.operator == "not_contains":
            return self.value not in str(field_value).lower()
        elif self.operator == "matches":
            return bool(re.search(self.value, str(field_value)))
        elif self.operator == "in":
            return field_value in self.value
        elif self.operator == "not_in":
            return field_value not in self.value

        return False

    def _get_field(self, data: dict, path: str) -> Any:
        """Get nested field value using dot notation."""
        parts = path.split(".")
        value = data

        for part in parts:
            if isinstance(value, dict):
                value = value.get(part)
            elif isinstance(value, list) and part.isdigit():
                idx = int(part)
                value = value[idx] if idx < len(value) else None
            else:
                return None

        return value


@dataclass
class Action:
    """An action to execute when trigger fires."""

    type: ActionType
    config: Dict[str, Any]

    # For CHAT: {"prompt": "...", "channel": "telegram", "chat_id": "..."}
    # For TOOL: {"name": "tool_name", "input": {...}}
    # For NOTIFY: {"channel": "telegram", "chat_id": "...", "message": "..."}
    # For WEBHOOK: {"url": "...", "method": "POST", "headers": {...}, "body": {...}}
    # For CHAIN: {"trigger_id": "..."}
    # For SCRIPT: {"command": "...", "timeout": 30}


@dataclass
class Trigger:
    """A complete trigger definition."""

    id: str
    name: str
    description: str
    enabled: bool
    type: TriggerType

    # Type-specific config
    config: Dict[str, Any]
    # For WEBHOOK: {"path": "/hooks/github", "secret": "...", "methods": ["POST"]}
    # For POLL: {"interval_seconds": 300, "source": "tool_name", "input": {...}}
    # For SCHEDULE: {"cron": "0 8 * * *"} or {"interval": "1h"}
    # For EVENT: {"event_type": "email.received", "source": "cerberus"}
    # For WATCH: {"path": "/path/to/watch", "events": ["created", "modified"]}

    # Conditions (all must be true - AND logic)
    conditions: List[Condition] = field(default_factory=list)

    # Actions to execute (in order)
    actions: List[Action] = field(default_factory=list)

    # Rate limiting
    rate_limit: Optional[Dict[str, int]] = None  # {"max": 10, "period_seconds": 3600}

    # Metadata
    created_at: str = ""
    updated_at: str = ""
    last_fired: Optional[str] = None
    fire_count: int = 0

    # Template variables available in actions
    # {{event.field}} - from trigger event
    # {{result.field}} - from previous action
    # {{env.VAR}} - environment variable
    # {{now}} - current timestamp


@dataclass
class Execution:
    """Record of a trigger execution."""

    trigger_id: str
    trigger_name: str
    started_at: str
    completed_at: Optional[str]
    success: bool
    event_data: Dict[str, Any]
    action_results: List[Dict[str, Any]]
    error: Optional[str] = None


# ============================================================
# TRIGGER STORAGE
# ============================================================


def _load_triggers() -> Dict[str, Trigger]:
    """Load all triggers from disk."""
    if not TRIGGERS_FILE.exists():
        return {}

    try:
        data = json.loads(TRIGGERS_FILE.read_text())
        triggers = {}

        for tid, tdata in data.items():
            # Reconstruct conditions
            conditions = [Condition(**c) for c in tdata.get("conditions", [])]

            # Reconstruct actions
            actions = [
                Action(type=ActionType(a["type"]), config=a["config"])
                for a in tdata.get("actions", [])
            ]

            triggers[tid] = Trigger(
                id=tdata["id"],
                name=tdata["name"],
                description=tdata.get("description", ""),
                enabled=tdata.get("enabled", True),
                type=TriggerType(tdata["type"]),
                config=tdata.get("config", {}),
                conditions=conditions,
                actions=actions,
                rate_limit=tdata.get("rate_limit"),
                created_at=tdata.get("created_at", ""),
                updated_at=tdata.get("updated_at", ""),
                last_fired=tdata.get("last_fired"),
                fire_count=tdata.get("fire_count", 0),
            )

        return triggers

    except Exception as e:
        logger.error(f"Error loading triggers: {e}")
        return {}


def _save_triggers(triggers: Dict[str, Trigger]):
    """Save all triggers to disk."""
    data = {}

    for tid, trigger in triggers.items():
        data[tid] = {
            "id": trigger.id,
            "name": trigger.name,
            "description": trigger.description,
            "enabled": trigger.enabled,
            "type": trigger.type.value,
            "config": trigger.config,
            "conditions": [asdict(c) for c in trigger.conditions],
            "actions": [{"type": a.type.value, "config": a.config} for a in trigger.actions],
            "rate_limit": trigger.rate_limit,
            "created_at": trigger.created_at,
            "updated_at": trigger.updated_at,
            "last_fired": trigger.last_fired,
            "fire_count": trigger.fire_count,
        }

    TRIGGERS_FILE.write_text(json.dumps(data, indent=2))


def _log_execution(execution: Execution):
    """Append execution to log file."""
    with open(EXECUTIONS_LOG, "a") as f:
        f.write(json.dumps(asdict(execution)) + "\n")


# ============================================================
# TEMPLATE RENDERING
# ============================================================

# Safe environment variables that can be exposed in templates
# These are non-sensitive system variables only
SAFE_ENV_VARS = frozenset(
    {
        "TZ",
        "LANG",
        "LC_ALL",
        "LC_CTYPE",
        "HOME",
        "USER",
        "HOSTNAME",
        "SHELL",
        "TERM",
        "PATH",
        "PWD",
        "OLDPWD",
        "FAMILIAR_ENV",
        "FAMILIAR_VERSION",
        "FAMILIAR_DEBUG",
    }
)


def _render_template(template: str, context: Dict[str, Any]) -> str:
    """Render template string with context variables."""
    if not isinstance(template, str):
        return template

    result = template

    # Find all {{...}} patterns
    pattern = r"\{\{([^}]+)\}\}"

    def replace(match):
        path = match.group(1).strip()

        # Special variables
        if path == "now":
            return datetime.now().isoformat()

        if path.startswith("env."):
            var_name = path[4:]
            # Security: Only allow safe environment variables
            if var_name.upper() not in SAFE_ENV_VARS:
                logger.warning(f"Blocked template access to sensitive env var: {var_name}")
                return "(blocked)"
            return os.environ.get(var_name, "")

        # Navigate context
        parts = path.split(".")
        value = context

        for part in parts:
            if isinstance(value, dict):
                value = value.get(part, "")
            else:
                return ""

        return str(value) if value else ""

    result = re.sub(pattern, replace, result)
    return result


def _render_dict(data: Dict, context: Dict[str, Any]) -> Dict:
    """Recursively render all string values in a dict."""
    result = {}

    for key, value in data.items():
        if isinstance(value, str):
            result[key] = _render_template(value, context)
        elif isinstance(value, dict):
            result[key] = _render_dict(value, context)
        elif isinstance(value, list):
            result[key] = [
                _render_template(v, context)
                if isinstance(v, str)
                else _render_dict(v, context)
                if isinstance(v, dict)
                else v
                for v in value
            ]
        else:
            result[key] = value

    return result


# ============================================================
# ACTION EXECUTORS
# ============================================================


def _execute_action(action: Action, context: Dict[str, Any], agent=None) -> Dict[str, Any]:
    """Execute a single action and return result."""

    config = _render_dict(action.config, context)

    if action.type == ActionType.CHAT:
        # Send to LLM
        prompt = config.get("prompt", "")

        if agent:
            response = agent.chat(prompt)
            return {"success": True, "response": response}
        else:
            return {"success": False, "error": "No agent available"}

    elif action.type == ActionType.TOOL:
        # Call a specific tool
        tool_name = config.get("name", "")
        tool_input = config.get("input", {})

        if agent and hasattr(agent, "tools"):
            result = agent.tools.execute(tool_name, tool_input)
            return {"success": True, "result": result}
        else:
            return {"success": False, "error": "No agent/tools available"}

    elif action.type == ActionType.NOTIFY:
        # Send notification
        channel = config.get("channel", "")
        message = config.get("message", "")
        chat_id = config.get("chat_id", "")

        # This would integrate with the messaging skill
        return {
            "success": True,
            "notification": {"channel": channel, "chat_id": chat_id, "message": message},
        }

    elif action.type == ActionType.WEBHOOK:
        # Call external webhook
        import urllib.request

        url = config.get("url", "")
        method = config.get("method", "POST")
        headers = config.get("headers", {})
        body = config.get("body", {})

        try:
            req_body = json.dumps(body).encode() if body else None
            headers["Content-Type"] = headers.get("Content-Type", "application/json")

            req = urllib.request.Request(url, data=req_body, headers=headers, method=method)

            with urllib.request.urlopen(req, timeout=30) as response:
                resp_body = response.read().decode()
                return {"success": True, "status": response.status, "body": resp_body}

        except Exception as e:
            return {"success": False, "error": str(e)}

    elif action.type == ActionType.CHAIN:
        # Trigger another trigger
        trigger_id = config.get("trigger_id", "")
        triggers = _load_triggers()

        if trigger_id in triggers:
            # Add to event queue for processing
            # Use the TriggerSystem's queue if available
            system = TriggerSystem._instance
            if system and system._event_queue:
                system._event_queue.put(
                    {"type": "chain", "trigger_id": trigger_id, "data": context.get("event", {})}
                )
            return {"success": True, "chained": trigger_id}
        else:
            return {"success": False, "error": f"Trigger not found: {trigger_id}"}

    elif action.type == ActionType.SCRIPT:
        # Run shell command - route through tool registry for security validation
        command = config.get("command", "")
        timeout = config.get("timeout", 30)

        try:
            # Import here to avoid circular imports
            from familiar.core.tools import get_tool_registry

            tools = get_tool_registry()
            result_str = tools.execute("run_shell", {"command": command, "timeout": timeout})

            # Parse result - tool returns error strings on failure
            if result_str.startswith("Error:"):
                return {
                    "success": False,
                    "error": result_str,
                    "stdout": "",
                    "stderr": result_str,
                    "returncode": 1,
                }
            else:
                return {"success": True, "stdout": result_str, "stderr": "", "returncode": 0}
        except Exception as e:
            return {"success": False, "error": str(e)}

    return {"success": False, "error": f"Unknown action type: {action.type}"}


# ============================================================
# TRIGGER ENGINE
# ============================================================


class TriggerEngine:
    """
    Main engine that processes triggers.

    Usage:
        engine = TriggerEngine(agent, event_queue)
        engine.start()
        engine.fire_event("email.received", {"from": "...", "subject": "..."})
    """

    def __init__(self, agent=None, event_queue: queue.Queue = None):
        self.agent = agent
        self._event_queue = event_queue or queue.Queue()
        self.triggers = _load_triggers()
        self._running = False
        self._threads = []
        self._rate_limit_tracker = {}  # trigger_id -> [timestamps]
        self._lock = threading.Lock()

    def start(self):
        """Start the trigger engine."""
        self._running = True

        # Start event processor thread
        t = threading.Thread(target=self._event_loop, daemon=True)
        t.start()
        self._threads.append(t)

        # Start polling triggers
        for trigger in self.triggers.values():
            if trigger.enabled and trigger.type == TriggerType.POLL:
                t = threading.Thread(target=self._poll_loop, args=(trigger.id,), daemon=True)
                t.start()
                self._threads.append(t)

        # Start scheduled triggers
        for trigger in self.triggers.values():
            if trigger.enabled and trigger.type == TriggerType.SCHEDULE:
                t = threading.Thread(target=self._schedule_loop, args=(trigger.id,), daemon=True)
                t.start()
                self._threads.append(t)

        logger.info(f"Trigger engine started with {len(self.triggers)} triggers")

    def stop(self):
        """Stop the trigger engine."""
        self._running = False
        for t in self._threads:
            t.join(timeout=2)

    def reload(self):
        """Reload triggers from disk."""
        self.triggers = _load_triggers()

    def fire_event(self, event_type: str, data: Dict[str, Any], source: str = "internal"):
        """Fire an event to be processed by matching triggers."""
        self._event_queue.put(
            {
                "type": event_type,
                "data": data,
                "source": source,
                "timestamp": datetime.now().isoformat(),
            }
        )

    def _event_loop(self):
        """Process events from the queue."""
        while self._running:
            try:
                event = self._event_queue.get(timeout=1)
                self._process_event(event)
            except queue.Empty:
                continue
            except Exception as e:
                logger.error(f"Event processing error: {e}")

    def _process_event(self, event: Dict[str, Any]):
        """Process a single event against all triggers."""
        event_type = event.get("type", "")
        event_data = event.get("data", {})

        for trigger in self.triggers.values():
            if not trigger.enabled:
                continue

            # Check if trigger matches this event type
            if trigger.type == TriggerType.EVENT:
                trigger_event_type = trigger.config.get("event_type", "")
                if trigger_event_type and trigger_event_type != event_type:
                    continue

            # Check conditions
            if not self._check_conditions(trigger, event_data):
                continue

            # Check rate limit
            if not self._check_rate_limit(trigger):
                logger.warning(f"Trigger {trigger.name} rate limited")
                continue

            # Execute trigger
            self._execute_trigger(trigger, event_data)

    def _check_conditions(self, trigger: Trigger, data: Dict) -> bool:
        """Check if all conditions are met."""
        for condition in trigger.conditions:
            if not condition.evaluate(data):
                return False
        return True

    def _check_rate_limit(self, trigger: Trigger) -> bool:
        """Check if trigger is within rate limit."""
        if not trigger.rate_limit:
            return True

        max_fires = trigger.rate_limit.get("max", 10)
        period = trigger.rate_limit.get("period_seconds", 3600)

        now = time.time()
        cutoff = now - period

        # Get recent fires
        recent = self._rate_limit_tracker.get(trigger.id, [])
        recent = [t for t in recent if t > cutoff]

        if len(recent) >= max_fires:
            return False

        # Record this fire
        recent.append(now)
        self._rate_limit_tracker[trigger.id] = recent

        return True

    def _execute_trigger(self, trigger: Trigger, event_data: Dict):
        """Execute a trigger's actions."""
        execution = Execution(
            trigger_id=trigger.id,
            trigger_name=trigger.name,
            started_at=datetime.now().isoformat(),
            completed_at=None,
            success=True,
            event_data=event_data,
            action_results=[],
        )

        context = {
            "event": event_data,
            "trigger": {"id": trigger.id, "name": trigger.name},
            "result": {},
        }

        try:
            for i, action in enumerate(trigger.actions):
                result = _execute_action(action, context, self.agent)
                execution.action_results.append(
                    {"action_index": i, "type": action.type.value, "result": result}
                )

                # Update context with result for next action
                context["result"] = result

                if not result.get("success", False):
                    execution.success = False
                    execution.error = result.get("error", "Action failed")
                    break

        except Exception as e:
            execution.success = False
            execution.error = str(e)
            logger.error(f"Trigger execution error: {e}")

        execution.completed_at = datetime.now().isoformat()

        # Update trigger stats
        trigger.last_fired = execution.started_at
        trigger.fire_count += 1
        trigger.updated_at = datetime.now().isoformat()

        self.triggers[trigger.id] = trigger
        _save_triggers(self.triggers)

        # Log execution
        _log_execution(execution)

        logger.info(
            f"Trigger '{trigger.name}' executed: {'success' if execution.success else 'failed'}"
        )

    def _poll_loop(self, trigger_id: str):
        """Polling loop for a specific trigger."""
        while self._running:
            trigger = self.triggers.get(trigger_id)

            if not trigger or not trigger.enabled:
                time.sleep(60)
                continue

            interval = trigger.config.get("interval_seconds", 300)

            try:
                # Execute the poll source
                source_tool = trigger.config.get("source", "")
                source_input = trigger.config.get("input", {})

                if source_tool and self.agent and hasattr(self.agent, "tools"):
                    result = self.agent.tools.execute(source_tool, source_input)

                    # Fire event with result
                    self.fire_event(f"poll.{trigger_id}", {"result": result}, source="poll")

            except Exception as e:
                logger.error(f"Poll error for {trigger_id}: {e}")

            time.sleep(interval)

    def _schedule_loop(self, trigger_id: str):
        """Schedule loop for cron-like triggers."""
        while self._running:
            trigger = self.triggers.get(trigger_id)

            if not trigger or not trigger.enabled:
                time.sleep(60)
                continue

            # Simple interval-based scheduling
            interval_str = trigger.config.get("interval", "1h")
            interval_seconds = self._parse_interval(interval_str)

            # Check if it's time to fire
            last_fired = trigger.last_fired
            if last_fired:
                last_time = datetime.fromisoformat(last_fired)
                next_time = last_time + timedelta(seconds=interval_seconds)

                if datetime.now() < next_time:
                    time.sleep(min(60, (next_time - datetime.now()).seconds))
                    continue

            # Fire the trigger
            self.fire_event(
                f"schedule.{trigger_id}",
                {"scheduled_time": datetime.now().isoformat()},
                source="schedule",
            )

            time.sleep(60)  # Check every minute

    def _parse_interval(self, interval: str) -> int:
        """Parse interval string like '1h', '30m', '1d' to seconds."""
        match = re.match(r"^(\d+)([smhd])$", interval.lower())
        if not match:
            return 3600  # Default 1 hour

        value = int(match.group(1))
        unit = match.group(2)

        multipliers = {"s": 1, "m": 60, "h": 3600, "d": 86400}
        return value * multipliers.get(unit, 3600)


# ============================================================
# WEBHOOK SERVER
# ============================================================


class WebhookServer:
    """
    HTTP server to receive webhooks.

    Usage:
        server = WebhookServer(engine, port=8080)
        server.start()

    Security:
        By default, binds to localhost (127.0.0.1) only.
        To expose to network, explicitly set host="0.0.0.0"
    """

    def __init__(self, engine: TriggerEngine, host: str = "127.0.0.1", port: int = 8765):
        self.engine = engine
        self.host = host
        self.port = port
        self._server = None
        self._thread = None

    def start(self):
        """Start the webhook server."""
        try:
            import json
            from http.server import BaseHTTPRequestHandler, HTTPServer

            engine = self.engine

            class WebhookHandler(BaseHTTPRequestHandler):
                def log_message(self, format, *args):
                    logger.debug(f"Webhook: {format % args}")

                def do_POST(self):
                    self._handle_request("POST")

                def do_GET(self):
                    self._handle_request("GET")

                def _handle_request(self, method):
                    path = self.path.split("?")[0]

                    # Find matching webhook trigger
                    trigger = None
                    for t in engine.triggers.values():
                        if t.type == TriggerType.WEBHOOK and t.enabled:
                            webhook_path = t.config.get("path", "")
                            allowed_methods = t.config.get("methods", ["POST"])

                            if webhook_path == path and method in allowed_methods:
                                trigger = t
                                break

                    if not trigger:
                        self.send_error(404, "Webhook not found")
                        return

                    # Read body
                    content_length = int(self.headers.get("Content-Length", 0))
                    body = self.rfile.read(content_length).decode("utf-8") if content_length else ""

                    # Verify signature if configured
                    # Use env var secret if available, fall back to config
                    config_secret = trigger.config.get("secret", "")
                    secret = get_webhook_secret(trigger.id, config_secret)

                    if secret:
                        signature = self.headers.get("X-Hub-Signature-256", "")
                        if not self._verify_signature(body, secret, signature):
                            self.send_error(401, "Invalid signature")
                            return

                    # Parse body
                    try:
                        data = json.loads(body) if body else {}
                    except json.JSONDecodeError:
                        data = {"raw": body}

                    # Add headers to data
                    data["_headers"] = dict(self.headers)
                    data["_path"] = path
                    data["_method"] = method

                    # Fire event
                    engine.fire_event(f"webhook.{trigger.id}", data, source="webhook")

                    # Respond
                    self.send_response(200)
                    self.send_header("Content-Type", "application/json")
                    self.end_headers()
                    self.wfile.write(json.dumps({"status": "ok"}).encode())

                def _verify_signature(self, body: str, secret: str, signature: str) -> bool:
                    """Verify GitHub-style HMAC signature."""
                    if not signature:
                        return False

                    expected = (
                        "sha256="
                        + hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
                    )

                    # Use constant-time comparison to prevent timing attacks
                    return hmac.compare_digest(expected, signature)

            self._server = HTTPServer((self.host, self.port), WebhookHandler)

            self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
            self._thread.start()

            logger.info(f"Webhook server started on {self.host}:{self.port}")

        except Exception as e:
            logger.error(f"Failed to start webhook server: {e}")

    def stop(self):
        """Stop the webhook server."""
        if self._server:
            self._server.shutdown()


# ============================================================
# TOOL HANDLERS
# ============================================================


def create_trigger(data: dict) -> str:
    """Create a new trigger."""
    name = data.get("name", "").strip()
    description = data.get("description", "")
    trigger_type = data.get("type", "event")
    config = data.get("config", {})

    if not name:
        return "Please provide a trigger name"

    # Generate ID
    trigger_id = hashlib.md5(f"{name}:{datetime.now().isoformat()}".encode()).hexdigest()[:12]

    # Parse conditions
    conditions = []
    for cond in data.get("conditions", []):
        conditions.append(
            Condition(
                field=cond.get("field", ""),
                operator=cond.get("operator", "eq"),
                value=cond.get("value"),
            )
        )

    # Parse actions
    actions = []
    for act in data.get("actions", []):
        actions.append(
            Action(type=ActionType(act.get("type", "notify")), config=act.get("config", {}))
        )

    # Default action if none specified
    if not actions:
        actions.append(
            Action(
                type=ActionType.NOTIFY, config={"message": f"Trigger '{name}' fired: {{{{event}}}}"}
            )
        )

    trigger = Trigger(
        id=trigger_id,
        name=name,
        description=description,
        enabled=True,
        type=TriggerType(trigger_type),
        config=config,
        conditions=conditions,
        actions=actions,
        rate_limit=data.get("rate_limit"),
        created_at=datetime.now().isoformat(),
        updated_at=datetime.now().isoformat(),
    )

    triggers = _load_triggers()
    triggers[trigger_id] = trigger
    _save_triggers(triggers)

    # Reload engine if running
    # (read-only access to _trigger_engine - no global declaration needed)
    if _trigger_engine:
        _trigger_engine.reload()

    return (
        f"✓ Created trigger: {name}\n"
        f"  ID: {trigger_id}\n"
        f"  Type: {trigger_type}\n"
        f"  Conditions: {len(conditions)}\n"
        f"  Actions: {len(actions)}"
    )


def create_webhook(data: dict) -> str:
    """Create a webhook trigger (simplified)."""
    name = data.get("name", "").strip()
    path = data.get("path", "").strip()
    secret = data.get("secret", "")
    action_type = data.get("action", "notify")
    action_config = data.get("action_config", {})

    if not name:
        return "Please provide a webhook name"

    if not path:
        path = f"/hooks/{name.lower().replace(' ', '-')}"

    if not path.startswith("/"):
        path = "/" + path

    # Build trigger data
    trigger_data = {
        "name": name,
        "description": f"Webhook at {path}",
        "type": "webhook",
        "config": {"path": path, "secret": secret, "methods": ["POST"]},
        "actions": [
            {
                "type": action_type,
                "config": action_config or {"message": f"Webhook '{name}' received: {{{{event}}}}"},
            }
        ],
    }

    result = create_trigger(trigger_data)

    # Add URL info
    port = 8765  # Default webhook port
    result += f"\n  URL: http://YOUR_HOST:{port}{path}"

    if secret:
        result += "\n  Secret: configured (use X-Hub-Signature-256 header)"

    return result


def create_scheduled_trigger(data: dict) -> str:
    """Create a scheduled trigger (simplified)."""
    name = data.get("name", "").strip()
    interval = data.get("interval", "1h")
    action_type = data.get("action", "chat")
    action_config = data.get("action_config", {})

    if not name:
        return "Please provide a trigger name"

    trigger_data = {
        "name": name,
        "description": f"Runs every {interval}",
        "type": "schedule",
        "config": {"interval": interval},
        "actions": [{"type": action_type, "config": action_config}],
    }

    return create_trigger(trigger_data)


def list_triggers(data: dict) -> str:
    """List all triggers."""
    triggers = _load_triggers()

    if not triggers:
        return "No triggers configured. Create one with: create_trigger"

    lines = ["⚡ Triggers:\n"]

    for trigger in sorted(triggers.values(), key=lambda t: t.name):
        status = "✓" if trigger.enabled else "○"
        lines.append(f"{status} **{trigger.name}** ({trigger.type.value})")

        if trigger.description:
            lines.append(f"    {trigger.description}")

        lines.append(f"    ID: {trigger.id}")
        lines.append(f"    Fired: {trigger.fire_count} times")

        if trigger.last_fired:
            lines.append(f"    Last: {trigger.last_fired}")

        lines.append("")

    return "\n".join(lines)


def get_trigger(data: dict) -> str:
    """Get details about a specific trigger."""
    trigger_id = data.get("id", data.get("trigger_id", ""))
    name = data.get("name", "")

    triggers = _load_triggers()

    # Find by ID or name
    trigger = None
    if trigger_id and trigger_id in triggers:
        trigger = triggers[trigger_id]
    elif name:
        for t in triggers.values():
            if t.name.lower() == name.lower():
                trigger = t
                break

    if not trigger:
        return f"Trigger not found: {trigger_id or name}"

    lines = [f"⚡ Trigger: {trigger.name}\n"]
    lines.append(f"ID: {trigger.id}")
    lines.append(f"Type: {trigger.type.value}")
    lines.append(f"Enabled: {trigger.enabled}")
    lines.append(f"Description: {trigger.description or '(none)'}")

    lines.append("\nConfig:")
    for key, value in trigger.config.items():
        lines.append(f"  {key}: {value}")

    if trigger.conditions:
        lines.append(f"\nConditions ({len(trigger.conditions)}):")
        for cond in trigger.conditions:
            lines.append(f"  • {cond.field} {cond.operator} {cond.value}")

    lines.append(f"\nActions ({len(trigger.actions)}):")
    for i, action in enumerate(trigger.actions):
        lines.append(f"  {i + 1}. {action.type.value}")

    if trigger.rate_limit:
        lines.append(
            f"\nRate limit: {trigger.rate_limit['max']} per {trigger.rate_limit['period_seconds']}s"
        )

    lines.append("\nStats:")
    lines.append(f"  Fire count: {trigger.fire_count}")
    lines.append(f"  Last fired: {trigger.last_fired or 'never'}")
    lines.append(f"  Created: {trigger.created_at}")

    return "\n".join(lines)


def enable_trigger(data: dict) -> str:
    """Enable a trigger."""
    trigger_id = data.get("id", data.get("trigger_id", ""))

    triggers = _load_triggers()

    if trigger_id not in triggers:
        return f"Trigger not found: {trigger_id}"

    triggers[trigger_id].enabled = True
    triggers[trigger_id].updated_at = datetime.now().isoformat()
    _save_triggers(triggers)

    # (read-only access to _trigger_engine - no global declaration needed)
    if _trigger_engine:
        _trigger_engine.reload()

    return f"✓ Enabled trigger: {triggers[trigger_id].name}"


def disable_trigger(data: dict) -> str:
    """Disable a trigger."""
    trigger_id = data.get("id", data.get("trigger_id", ""))

    triggers = _load_triggers()

    if trigger_id not in triggers:
        return f"Trigger not found: {trigger_id}"

    triggers[trigger_id].enabled = False
    triggers[trigger_id].updated_at = datetime.now().isoformat()
    _save_triggers(triggers)

    # (read-only access to _trigger_engine - no global declaration needed)
    if _trigger_engine:
        _trigger_engine.reload()

    return f"✓ Disabled trigger: {triggers[trigger_id].name}"


def delete_trigger(data: dict) -> str:
    """Delete a trigger."""
    trigger_id = data.get("id", data.get("trigger_id", ""))

    triggers = _load_triggers()

    if trigger_id not in triggers:
        return f"Trigger not found: {trigger_id}"

    trigger = triggers[trigger_id]

    if not data.get("_confirmed"):
        from familiar.core.confirmations import needs_confirmation

        return needs_confirmation(
            tool_name="delete_trigger",
            tool_input=data,
            preview=f"Delete trigger '{trigger.name}'. This cannot be undone.",
            risk="high",
        )

    del triggers[trigger_id]
    _save_triggers(triggers)

    # (read-only access to _trigger_engine - no global declaration needed)
    if _trigger_engine:
        _trigger_engine.reload()

    return f"✓ Deleted trigger: {trigger.name}"


def fire_test_event(data: dict) -> str:
    """Fire a test event to check triggers."""
    event_type = data.get("event_type", "test")
    event_data = data.get("data", {"test": True})

    # (read-only access to _trigger_engine - no global declaration needed)

    if not _trigger_engine:
        return "Trigger engine not running. Start it first."

    _trigger_engine.fire_event(event_type, event_data, source="test")

    return f"✓ Fired test event: {event_type}\n  Data: {json.dumps(event_data)}"


def get_execution_log(data: dict) -> str:
    """Get recent trigger executions."""
    limit = data.get("limit", 10)
    trigger_id = data.get("trigger_id")

    if not EXECUTIONS_LOG.exists():
        return "No executions logged yet."

    executions = []
    with open(EXECUTIONS_LOG, "r") as f:
        for line in f:
            try:
                exec_data = json.loads(line.strip())
                if trigger_id and exec_data.get("trigger_id") != trigger_id:
                    continue
                executions.append(exec_data)
            except json.JSONDecodeError:
                continue

    # Get most recent
    executions = executions[-limit:]
    executions.reverse()

    if not executions:
        return "No matching executions found."

    lines = ["📋 Recent Executions:\n"]

    for ex in executions:
        status = "✓" if ex.get("success") else "✗"
        lines.append(f"{status} {ex.get('trigger_name', 'unknown')}")
        lines.append(f"    {ex.get('started_at', '')}")

        if ex.get("error"):
            lines.append(f"    Error: {ex['error']}")

        lines.append("")

    return "\n".join(lines)


def start_trigger_engine(data: dict) -> str:
    """Start the trigger engine and webhook server."""
    global _trigger_engine, _webhook_server

    agent = data.get("_agent")  # Injected by the tool system
    webhook_port = data.get("webhook_port", 8765)

    if _trigger_engine and _trigger_engine._running:
        return "Trigger engine already running."

    _trigger_engine = TriggerEngine(agent)
    _trigger_engine.start()

    # Start webhook server
    _webhook_server = WebhookServer(_trigger_engine, port=webhook_port)
    _webhook_server.start()

    trigger_count = len(_trigger_engine.triggers)
    webhook_count = sum(
        1 for t in _trigger_engine.triggers.values() if t.type == TriggerType.WEBHOOK and t.enabled
    )

    return (
        f"✓ Trigger engine started\n"
        f"  Triggers: {trigger_count}\n"
        f"  Webhooks: {webhook_count} (port {webhook_port})"
    )


def stop_trigger_engine(data: dict) -> str:
    """Stop the trigger engine."""
    global _trigger_engine, _webhook_server

    if _webhook_server:
        _webhook_server.stop()
        _webhook_server = None

    if _trigger_engine:
        _trigger_engine.stop()
        _trigger_engine = None

    return "✓ Trigger engine stopped"


# ============================================================
# TOOL DEFINITIONS
# ============================================================

TOOLS = [
    {
        "name": "create_trigger",
        "description": (
            "Create a new automation trigger that fires on events, webhooks, schedules, or polling. "
            "Triggers evaluate conditions against incoming data and execute one or more actions in sequence. "
            "Supports rate limiting to prevent runaway executions. "
            "Actions can send messages, call tools, fire webhooks, chain to other triggers, or run scripts."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Trigger name"},
                "description": {
                    "type": "string",
                    "description": "Human-readable description of what the trigger does",
                },
                "type": {
                    "type": "string",
                    "enum": ["webhook", "poll", "schedule", "event"],
                    "description": "Trigger type",
                },
                "config": {"type": "object", "description": "Type-specific configuration"},
                "conditions": {
                    "type": "array",
                    "description": "Conditions that must be met",
                    "items": {
                        "type": "object",
                        "properties": {
                            "field": {
                                "type": "string",
                                "description": "JSONPath-like field selector using dot notation",
                            },
                            "operator": {
                                "type": "string",
                                "enum": ["eq", "ne", "gt", "lt", "contains", "matches", "exists"],
                                "description": "Comparison operator for the condition",
                            },
                            "value": {"description": "Value to compare against"},
                        },
                    },
                },
                "actions": {
                    "type": "array",
                    "description": "Actions to execute",
                    "items": {
                        "type": "object",
                        "properties": {
                            "type": {
                                "type": "string",
                                "enum": ["chat", "tool", "notify", "webhook", "chain", "script"],
                                "description": "Action type to execute when trigger fires",
                            },
                            "config": {
                                "type": "object",
                                "description": "Action-specific configuration parameters",
                            },
                        },
                    },
                },
                "rate_limit": {
                    "type": "object",
                    "description": "Rate limiting configuration to prevent runaway triggers",
                    "properties": {
                        "max": {
                            "type": "integer",
                            "description": "Maximum number of fires allowed within the period",
                        },
                        "period_seconds": {
                            "type": "integer",
                            "description": "Time window in seconds for rate limiting",
                        },
                    },
                },
            },
            "required": ["name", "type"],
        },
        "input_examples": [
            {
                "name": "daily-briefing",
                "type": "scheduled",
                "schedule": "0 8 * * *",
                "action": "task_briefing",
                "action_params": {},
            },
            {
                "name": "new-email-alert",
                "type": "event",
                "event": "email.received",
                "conditions": {"from": "boss@example.com"},
                "action": "send_message",
                "action_params": {"to": "+1234567890", "message": "New email from boss"},
            },
        ],
        "handler": create_trigger,
        "category": "triggers",
    },
    {
        "name": "create_webhook",
        "description": (
            "Create a webhook trigger that receives HTTP callbacks from external services. "
            "Automatically generates a URL path and configures HMAC signature verification if a secret is provided. "
            "Supports GitHub-style X-Hub-Signature-256 headers for payload validation. "
            "This is a simplified wrapper around create_trigger for common webhook use cases."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Webhook name"},
                "path": {"type": "string", "description": "URL path (e.g., /hooks/github)"},
                "secret": {
                    "type": "string",
                    "description": "Shared secret for signature verification",
                },
                "action": {
                    "type": "string",
                    "enum": ["notify", "chat", "tool", "webhook"],
                    "default": "notify",
                    "description": "Action type to execute when webhook is received",
                },
                "action_config": {"type": "object", "description": "Action configuration"},
            },
            "required": ["name"],
        },
        "handler": create_webhook,
        "category": "triggers",
    },
    {
        "name": "create_scheduled_trigger",
        "description": (
            "Create a scheduled trigger that runs automatically at fixed time intervals. "
            "Intervals are specified as duration strings like '1h', '30m', or '1d'. "
            "Ideal for recurring tasks such as daily briefings, periodic health checks, or status reports. "
            "This is a simplified wrapper around create_trigger for scheduled automation."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Trigger name"},
                "interval": {
                    "type": "string",
                    "description": "Interval (e.g., '1h', '30m', '1d')",
                    "default": "1h",
                },
                "action": {
                    "type": "string",
                    "enum": ["chat", "tool", "notify"],
                    "default": "chat",
                    "description": "Action type to execute on each interval",
                },
                "action_config": {
                    "type": "object",
                    "description": "Action-specific configuration parameters",
                },
            },
            "required": ["name"],
        },
        "handler": create_scheduled_trigger,
        "category": "triggers",
    },
    {
        "name": "list_triggers",
        "description": (
            "List all configured triggers with their current status and fire counts. "
            "Shows enabled/disabled state, trigger type, description, and last-fired timestamp. "
            "Results are sorted alphabetically by trigger name. "
            "Use get_trigger for full details on a specific trigger."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": list_triggers,
        "category": "triggers",
    },
    {
        "name": "get_trigger",
        "description": (
            "Get full details about a specific trigger by ID or name. "
            "Returns configuration, conditions, actions, rate limits, and execution statistics. "
            "Includes fire count, last-fired timestamp, and creation date. "
            "Lookup is case-insensitive when searching by name."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "id": {"type": "string", "description": "Trigger ID to look up"},
                "name": {
                    "type": "string",
                    "description": "Trigger name to search for (case-insensitive)",
                },
            },
        },
        "handler": get_trigger,
        "category": "triggers",
    },
    {
        "name": "enable_trigger",
        "description": (
            "Enable a previously disabled trigger so it resumes processing events. "
            "The trigger engine is automatically reloaded after enabling. "
            "The trigger's updated_at timestamp is refreshed. "
            "Use list_triggers to see which triggers are currently disabled."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"id": {"type": "string", "description": "Trigger ID"}},
            "required": ["id"],
        },
        "handler": enable_trigger,
        "category": "triggers",
    },
    {
        "name": "disable_trigger",
        "description": (
            "Disable a trigger to temporarily stop it from processing events. "
            "The trigger definition is preserved and can be re-enabled later. "
            "The trigger engine is automatically reloaded after disabling. "
            "Useful for pausing triggers during maintenance or debugging."
        ),
        "input_schema": {
            "type": "object",
            "properties": {"id": {"type": "string", "description": "Trigger ID"}},
            "required": ["id"],
        },
        "handler": disable_trigger,
        "category": "triggers",
    },
    {
        "name": "delete_trigger",
        "description": (
            "Permanently delete a trigger and remove it from storage. "
            "This action cannot be undone and all trigger configuration will be lost. "
            "The trigger engine is automatically reloaded after deletion. "
            "Requires confirmation before proceeding with the destructive operation."
        ),
        "requires_confirmation": True,
        "input_schema": {
            "type": "object",
            "properties": {
                "id": {"type": "string", "description": "Trigger ID to delete"},
                "confirm": {
                    "type": "boolean",
                    "default": False,
                    "description": "Set to true to confirm deletion",
                },
            },
            "required": ["id"],
        },
        "handler": delete_trigger,
        "category": "triggers",
    },
    {
        "name": "fire_test_event",
        "description": (
            "Fire a test event into the trigger engine to verify trigger matching and execution. "
            "The event is processed against all enabled triggers just like a real event. "
            "Useful for debugging trigger conditions and validating action configurations. "
            "The event source is tagged as 'test' to distinguish it from production events."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "event_type": {
                    "type": "string",
                    "description": "Event type to fire",
                    "default": "test",
                },
                "data": {
                    "type": "object",
                    "description": "Event data payload to send with the test event",
                },
            },
        },
        "handler": fire_test_event,
        "category": "triggers",
    },
    {
        "name": "execution_log",
        "description": (
            "View the log of recent trigger executions with success/failure status and timestamps. "
            "Results can be filtered by trigger ID to see executions for a specific trigger. "
            "Returns the most recent executions first, up to the specified limit. "
            "Includes error messages for failed executions to aid in debugging."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "default": 10,
                    "description": "Maximum number of executions to return",
                },
                "trigger_id": {"type": "string", "description": "Filter by trigger ID"},
            },
        },
        "handler": get_execution_log,
        "category": "triggers",
    },
    {
        "name": "start_triggers",
        "description": (
            "Start the trigger engine and webhook server to begin processing automation events. "
            "Loads all trigger definitions from disk and begins polling, scheduling, and event matching. "
            "The webhook server listens on the specified port for incoming HTTP callbacks. "
            "Reports the number of loaded triggers and active webhooks on startup."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "webhook_port": {
                    "type": "integer",
                    "default": 8765,
                    "description": "Port for the webhook HTTP server",
                },
            },
        },
        "handler": start_trigger_engine,
        "category": "triggers",
    },
    {
        "name": "stop_triggers",
        "description": (
            "Stop the trigger engine and webhook server to halt all automation processing. "
            "Gracefully shuts down the webhook HTTP server and all polling/scheduling threads. "
            "Triggers remain saved on disk and can be resumed with start_triggers. "
            "No new events will be processed after stopping."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": stop_trigger_engine,
        "category": "triggers",
    },
]
