"""
Phase 39: Extensible Plugin System for Brynq.

Third-party developers can extend the platform with custom tools, event handlers,
and hook-based integrations. Plugins are self-contained registrations (dicts with
callable references) that the platform orchestrates at runtime.

Features:
  - REGISTER: Declare plugins with metadata, hooks, tools, and event handlers
  - LIFECYCLE: installed -> enabled -> disabled -> uninstalled
  - HOOKS: Intercept platform events (pre/post) to modify or react to context
  - TOOLS: MCP-style custom tools registered per-plugin
  - CONFIG: Per-plugin key/value settings persisted alongside metadata
  - STATS: Hook invocations, tool calls, errors, uptime tracking

Storage:
  state/broker/plugins/{name}.json       — plugin registry + config
  state/broker/plugins/stats.jsonl       — usage/event log
"""

import json
import logging
import threading
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from brynq.server import BROKER_DIR, _write_json_sync, _read_jsonl_sync, _append_jsonl_sync

logger = logging.getLogger(__name__)

# ── Paths ──────────────────────────────────────────────────────────────────

PLUGINS_DIR = BROKER_DIR / "plugins"
STATS_FILE = PLUGINS_DIR / "stats.jsonl"

PLUGINS_DIR.mkdir(parents=True, exist_ok=True)

# ── Constants ──────────────────────────────────────────────────────────────

VALID_STATUSES = ("installed", "enabled", "disabled", "uninstalled")

VALID_HOOKS = (
    "pre_message_post",
    "post_task_create",
    "pre_agent_register",
    "on_workflow_complete",
)

# ── In-Memory Registries ──────────────────────────────────────────────────
# Callable hooks and tools live in memory; metadata lives on disk.

_hooks_lock = threading.Lock()
_tools_lock = threading.Lock()

# {hook_name: [(plugin_name, callable), ...]}
_hook_registry: Dict[str, List[tuple]] = {}

# {(plugin_name, tool_name): {"description": str, "handler": callable, "parameters": dict|None}}
_tool_registry: Dict[tuple, dict] = {}


# ── Internal Helpers ──────────────────────────────────────────────────────


def _plugin_path(name: str) -> Path:
    return PLUGINS_DIR / f"{name}.json"


def _read_plugin(name: str) -> Optional[dict]:
    """Read a plugin's JSON file. Returns None if missing or corrupt."""
    path = _plugin_path(name)
    if not path.exists():
        return None
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return None


def _save_plugin(data: dict) -> None:
    """Persist plugin data to its JSON file."""
    _write_json_sync(_plugin_path(data["name"]), data)


def _log_stat(plugin_name: str, event: str, detail: str = "") -> None:
    """Append a stats entry to the shared stats log."""
    entry = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "plugin": plugin_name,
        "event": event,
        "detail": detail,
    }
    try:
        _append_jsonl_sync(STATS_FILE, entry)
    except OSError:
        logger.warning("Failed to write plugin stat for %s", plugin_name)


# ── Plugin Registry ──────────────────────────────────────────────────────


def register_plugin(
    name: str,
    version: str,
    author: str,
    description: str,
    hooks: Optional[Dict[str, Callable]] = None,
    tools: Optional[List[dict]] = None,
    event_handlers: Optional[Dict[str, Callable]] = None,
) -> dict:
    """
    Register a new plugin with the platform.

    Args:
        name: Unique plugin identifier (alphanumeric + underscores).
        version: Semver string (e.g. "1.0.0").
        author: Author name or org.
        description: Short description of what the plugin does.
        hooks: Dict mapping hook_name -> callable. Must be in VALID_HOOKS.
        tools: List of dicts with keys: name, description, handler, parameters (optional).
        event_handlers: Dict mapping event_type -> callable (stored in metadata only).

    Returns:
        Dict with plugin_id and metadata.

    Raises:
        ValueError on duplicate name or invalid hook names.
    """
    if not name or not isinstance(name, str):
        raise ValueError("Plugin name must be a non-empty string")

    existing = _read_plugin(name)
    if existing and existing.get("status") != "uninstalled":
        raise ValueError(f"Plugin '{name}' is already registered")

    plugin_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    # Validate hook names
    hooks = hooks or {}
    for hook_name in hooks:
        if hook_name not in VALID_HOOKS:
            raise ValueError(
                f"Invalid hook '{hook_name}'. Valid hooks: {', '.join(VALID_HOOKS)}"
            )

    # Build metadata (callables are NOT serialized — they stay in memory)
    plugin_data = {
        "plugin_id": plugin_id,
        "name": name,
        "version": version,
        "author": author,
        "description": description,
        "status": "installed",
        "hook_names": list(hooks.keys()),
        "tool_names": [t["name"] for t in (tools or [])],
        "event_handler_types": list((event_handlers or {}).keys()),
        "config": {},
        "created": now,
        "updated": now,
        "enabled_at": None,
        "disabled_at": None,
    }

    _save_plugin(plugin_data)

    # Register callable hooks in memory
    with _hooks_lock:
        for hook_name, fn in hooks.items():
            if hook_name not in _hook_registry:
                _hook_registry[hook_name] = []
            _hook_registry[hook_name].append((name, fn))

    # Register callable tools in memory
    if tools:
        with _tools_lock:
            for tool_def in tools:
                key = (name, tool_def["name"])
                _tool_registry[key] = {
                    "description": tool_def.get("description", ""),
                    "handler": tool_def["handler"],
                    "parameters": tool_def.get("parameters"),
                }

    _log_stat(name, "registered", f"v{version} by {author}")
    return plugin_data


# ── Plugin Discovery ─────────────────────────────────────────────────────


def list_plugins(status: Optional[str] = None) -> List[dict]:
    """
    Return all registered plugins, optionally filtered by status.

    Args:
        status: Filter to a specific status (installed/enabled/disabled/uninstalled).
    """
    plugins = []
    for path in sorted(PLUGINS_DIR.glob("*.json")):
        if path.name == "stats.jsonl":
            continue
        try:
            data = json.loads(path.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if status is None or data.get("status") == status:
            plugins.append(data)
    return plugins


def get_plugin(name: str) -> Optional[dict]:
    """Return details for a single plugin by name."""
    return _read_plugin(name)


def search_plugins(query: str) -> List[dict]:
    """
    Search plugins by name, description, or author (case-insensitive substring match).
    """
    query_lower = query.lower()
    results = []
    for plugin in list_plugins():
        searchable = " ".join([
            plugin.get("name", ""),
            plugin.get("description", ""),
            plugin.get("author", ""),
        ]).lower()
        if query_lower in searchable:
            results.append(plugin)
    return results


# ── Plugin Lifecycle ─────────────────────────────────────────────────────


def enable_plugin(name: str) -> dict:
    """
    Activate a plugin. Must be in 'installed' or 'disabled' status.

    Returns:
        Updated plugin data.

    Raises:
        ValueError if plugin not found or already enabled.
    """
    plugin = _read_plugin(name)
    if not plugin:
        raise ValueError(f"Plugin '{name}' not found")
    if plugin["status"] == "enabled":
        raise ValueError(f"Plugin '{name}' is already enabled")
    if plugin["status"] not in ("installed", "disabled"):
        raise ValueError(
            f"Cannot enable plugin '{name}' in status '{plugin['status']}'"
        )

    now = datetime.now(timezone.utc).isoformat()
    plugin["status"] = "enabled"
    plugin["enabled_at"] = now
    plugin["updated"] = now
    _save_plugin(plugin)
    _log_stat(name, "enabled")
    return plugin


def disable_plugin(name: str) -> dict:
    """
    Deactivate a plugin without removing it.

    Returns:
        Updated plugin data.

    Raises:
        ValueError if plugin not found or not enabled.
    """
    plugin = _read_plugin(name)
    if not plugin:
        raise ValueError(f"Plugin '{name}' not found")
    if plugin["status"] != "enabled":
        raise ValueError(
            f"Cannot disable plugin '{name}' in status '{plugin['status']}'"
        )

    now = datetime.now(timezone.utc).isoformat()
    plugin["status"] = "disabled"
    plugin["disabled_at"] = now
    plugin["updated"] = now
    _save_plugin(plugin)
    _log_stat(name, "disabled")
    return plugin


def uninstall_plugin(name: str) -> dict:
    """
    Remove a plugin completely — clears hooks and tools from memory, marks
    status as 'uninstalled'.

    Returns:
        Final plugin data.

    Raises:
        ValueError if plugin not found or already uninstalled.
    """
    plugin = _read_plugin(name)
    if not plugin:
        raise ValueError(f"Plugin '{name}' not found")
    if plugin["status"] == "uninstalled":
        raise ValueError(f"Plugin '{name}' is already uninstalled")

    # Remove hooks from in-memory registry
    with _hooks_lock:
        for hook_name in list(_hook_registry):
            _hook_registry[hook_name] = [
                (pname, fn)
                for pname, fn in _hook_registry[hook_name]
                if pname != name
            ]
            if not _hook_registry[hook_name]:
                del _hook_registry[hook_name]

    # Remove tools from in-memory registry
    with _tools_lock:
        keys_to_remove = [k for k in _tool_registry if k[0] == name]
        for k in keys_to_remove:
            del _tool_registry[k]

    now = datetime.now(timezone.utc).isoformat()
    plugin["status"] = "uninstalled"
    plugin["updated"] = now
    _save_plugin(plugin)
    _log_stat(name, "uninstalled")
    return plugin


# ── Hook System ──────────────────────────────────────────────────────────


def run_hooks(hook_name: str, context: dict) -> dict:
    """
    Execute all enabled plugin hooks for *hook_name* in registration order.

    Each hook callable receives ``context`` and returns a (possibly modified)
    dict. If a hook raises, the error is logged and the next hook proceeds
    with the unmodified context — one bad hook never crashes the platform.

    Args:
        hook_name: One of VALID_HOOKS.
        context: Mutable dict passed through each hook in sequence.

    Returns:
        The final context dict after all hooks have run.
    """
    with _hooks_lock:
        entries = list(_hook_registry.get(hook_name, []))

    for plugin_name, fn in entries:
        # Only run hooks for enabled plugins
        plugin = _read_plugin(plugin_name)
        if not plugin or plugin.get("status") != "enabled":
            continue
        try:
            result = fn(context)
            if isinstance(result, dict):
                context = result
            _log_stat(plugin_name, "hook_invoked", hook_name)
        except Exception as exc:
            logger.warning(
                "Hook '%s' in plugin '%s' raised %s: %s",
                hook_name, plugin_name, type(exc).__name__, exc,
            )
            _log_stat(plugin_name, "hook_error", f"{hook_name}: {exc}")

    return context


# ── Plugin Tools ─────────────────────────────────────────────────────────


def register_tool(
    plugin_name: str,
    tool_name: str,
    description: str,
    handler: Callable,
    parameters: Optional[dict] = None,
) -> dict:
    """
    Register a custom MCP-style tool under a plugin.

    Args:
        plugin_name: Owning plugin (must exist and not be uninstalled).
        tool_name: Unique tool name within the plugin.
        description: What the tool does.
        handler: Callable that takes **arguments and returns a result dict.
        parameters: Optional JSON-schema-like dict describing accepted args.

    Returns:
        Dict confirming registration.

    Raises:
        ValueError if plugin not found or tool already exists.
    """
    plugin = _read_plugin(plugin_name)
    if not plugin or plugin["status"] == "uninstalled":
        raise ValueError(f"Plugin '{plugin_name}' not found or uninstalled")

    key = (plugin_name, tool_name)
    with _tools_lock:
        if key in _tool_registry:
            raise ValueError(
                f"Tool '{tool_name}' already registered for plugin '{plugin_name}'"
            )
        _tool_registry[key] = {
            "description": description,
            "handler": handler,
            "parameters": parameters,
        }

    # Update the on-disk tool list
    if tool_name not in plugin.get("tool_names", []):
        plugin.setdefault("tool_names", []).append(tool_name)
        plugin["updated"] = datetime.now(timezone.utc).isoformat()
        _save_plugin(plugin)

    _log_stat(plugin_name, "tool_registered", tool_name)
    return {"plugin": plugin_name, "tool": tool_name, "status": "registered"}


def list_plugin_tools(plugin_name: Optional[str] = None) -> List[dict]:
    """
    List all registered tools, optionally filtered to a single plugin.

    Returns list of dicts with plugin, tool, description, parameters.
    """
    with _tools_lock:
        results = []
        for (pname, tname), info in sorted(_tool_registry.items()):
            if plugin_name and pname != plugin_name:
                continue
            results.append({
                "plugin": pname,
                "tool": tname,
                "description": info["description"],
                "parameters": info["parameters"],
            })
    return results


def execute_tool(plugin_name: str, tool_name: str, arguments: Optional[dict] = None) -> dict:
    """
    Execute a plugin tool by name.

    Args:
        plugin_name: Owning plugin.
        tool_name: Tool to execute.
        arguments: Dict of arguments passed to the handler.

    Returns:
        Dict with tool output or error information.
    """
    plugin = _read_plugin(plugin_name)
    if not plugin or plugin["status"] == "uninstalled":
        return {"ok": False, "error": f"Plugin '{plugin_name}' not found or uninstalled"}
    if plugin["status"] != "enabled":
        return {"ok": False, "error": f"Plugin '{plugin_name}' is not enabled"}

    key = (plugin_name, tool_name)
    with _tools_lock:
        entry = _tool_registry.get(key)

    if not entry:
        return {"ok": False, "error": f"Tool '{tool_name}' not found in plugin '{plugin_name}'"}

    arguments = arguments or {}
    try:
        result = entry["handler"](**arguments)
        _log_stat(plugin_name, "tool_called", tool_name)
        return {"ok": True, "result": result}
    except Exception as exc:
        logger.warning(
            "Tool '%s' in plugin '%s' raised %s: %s",
            tool_name, plugin_name, type(exc).__name__, exc,
        )
        _log_stat(plugin_name, "tool_error", f"{tool_name}: {exc}")
        return {"ok": False, "error": str(exc)}


# ── Plugin Config ────────────────────────────────────────────────────────


def set_plugin_config(name: str, key: str, value: Any) -> dict:
    """
    Set a per-plugin configuration value.

    Args:
        name: Plugin name.
        key: Config key.
        value: Any JSON-serializable value.

    Returns:
        Updated config dict.

    Raises:
        ValueError if plugin not found.
    """
    plugin = _read_plugin(name)
    if not plugin:
        raise ValueError(f"Plugin '{name}' not found")

    plugin.setdefault("config", {})[key] = value
    plugin["updated"] = datetime.now(timezone.utc).isoformat()
    _save_plugin(plugin)
    return plugin["config"]


def get_plugin_config(name: str, key: str, default: Any = None) -> Any:
    """
    Get a per-plugin configuration value.

    Args:
        name: Plugin name.
        key: Config key.
        default: Returned when the key is not set.

    Returns:
        The config value, or *default*.
    """
    plugin = _read_plugin(name)
    if not plugin:
        return default
    return plugin.get("config", {}).get(key, default)


# ── Plugin Stats ─────────────────────────────────────────────────────────


def get_plugin_stats(name: str) -> dict:
    """
    Return usage statistics for a plugin.

    Returns dict with:
        hook_invocations: int
        tool_calls: int
        errors: int
        uptime_seconds: float|None  (seconds since enabled, if currently enabled)
        events: list of recent stat entries
    """
    plugin = _read_plugin(name)
    if not plugin:
        return {"error": f"Plugin '{name}' not found"}

    all_stats = _read_jsonl_sync(STATS_FILE)
    plugin_stats = [s for s in all_stats if s.get("plugin") == name]

    hook_invocations = sum(1 for s in plugin_stats if s.get("event") == "hook_invoked")
    tool_calls = sum(1 for s in plugin_stats if s.get("event") == "tool_called")
    errors = sum(
        1 for s in plugin_stats if s.get("event") in ("hook_error", "tool_error")
    )

    uptime: Optional[float] = None
    if plugin.get("status") == "enabled" and plugin.get("enabled_at"):
        try:
            enabled_dt = datetime.fromisoformat(plugin["enabled_at"])
            uptime = (datetime.now(timezone.utc) - enabled_dt).total_seconds()
        except (ValueError, TypeError):
            uptime = None

    return {
        "plugin": name,
        "status": plugin.get("status"),
        "hook_invocations": hook_invocations,
        "tool_calls": tool_calls,
        "errors": errors,
        "uptime_seconds": uptime,
        "events": plugin_stats,
    }
