"""Plugin System for MCP Security Audit.

Extensible plugin architecture for custom security rules:
- Plugin manifests with metadata (name, version, author)
- Custom rules with patterns, severity, and fix suggestions
- Plugin loading from directory with manifest validation
- Plugin registry for managing available plugins
- Plugin runner for executing rules against scan results

CLI: mcpaudit plugin list | load <dir> | run <name>

Zero external dependencies. Part of mcpaudit v2.1.0.
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_VALID_SEVERITIES = frozenset({
    "CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO",
})

_MANIFEST_REQUIRED = frozenset({"name", "version", "rules"})
_MANIFEST_FILE = "plugin.json"


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------

@dataclass
class PluginRule:
    """A custom security rule defined by a plugin."""

    rule_id: str
    pattern: str = ""
    severity: str = "MEDIUM"
    title: str = ""
    description: str = ""
    fix_suggestion: str = ""
    file_pattern: str = "*"
    enabled: bool = True

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "pattern": self.pattern,
            "severity": self.severity,
            "title": self.title,
            "description": self.description,
            "fix_suggestion": self.fix_suggestion,
            "file_pattern": self.file_pattern,
            "enabled": self.enabled,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> PluginRule:
        return cls(
            rule_id=data.get("rule_id", ""),
            pattern=data.get("pattern", ""),
            severity=data.get("severity", "MEDIUM"),
            title=data.get("title", ""),
            description=data.get("description", ""),
            fix_suggestion=data.get("fix_suggestion", ""),
            file_pattern=data.get("file_pattern", "*"),
            enabled=data.get("enabled", True),
        )

    def validate(self) -> list[str]:
        """Validate this rule."""
        errors: list[str] = []
        if not self.rule_id:
            errors.append("rule_id is required")
        if not self.pattern:
            errors.append(f"Rule '{self.rule_id}': pattern is required")
        else:
            try:
                re.compile(self.pattern)
            except re.error as exc:
                errors.append(
                    f"Rule '{self.rule_id}': "
                    f"invalid regex pattern: {exc}"
                )
        if self.severity not in _VALID_SEVERITIES:
            errors.append(
                f"Rule '{self.rule_id}': "
                f"invalid severity '{self.severity}'"
            )
        return errors


@dataclass
class PluginManifest:
    """Metadata for a plugin."""

    name: str = ""
    version: str = "0.1.0"
    author: str = ""
    description: str = ""
    rules: list[PluginRule] = field(default_factory=list)
    source_path: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "version": self.version,
            "author": self.author,
            "description": self.description,
            "rules": [r.to_dict() for r in self.rules],
        }

    @classmethod
    def from_dict(
        cls, data: dict[str, Any], source_path: str = "",
    ) -> PluginManifest:
        rules = []
        for rd in data.get("rules", []):
            if isinstance(rd, dict):
                rules.append(PluginRule.from_dict(rd))
        return cls(
            name=data.get("name", ""),
            version=data.get("version", "0.1.0"),
            author=data.get("author", ""),
            description=data.get("description", ""),
            rules=rules,
            source_path=source_path,
        )

    def validate(self) -> list[str]:
        """Validate the manifest."""
        errors: list[str] = []
        if not self.name:
            errors.append("Plugin name is required")
        if not self.version:
            errors.append("Plugin version is required")
        if not self.rules:
            errors.append("Plugin must define at least one rule")
        for rule in self.rules:
            errors.extend(rule.validate())
        return errors


@dataclass
class PluginMatch:
    """A match produced by running a plugin rule."""

    rule_id: str
    severity: str
    title: str
    description: str
    file: str
    line: int
    code_snippet: str = ""
    fix_suggestion: str = ""
    plugin_name: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "severity": self.severity,
            "title": self.title,
            "description": self.description,
            "file": self.file,
            "line": self.line,
            "code_snippet": self.code_snippet,
            "fix_suggestion": self.fix_suggestion,
            "plugin_name": self.plugin_name,
        }


# ---------------------------------------------------------------------------
# Plugin loader
# ---------------------------------------------------------------------------

class PluginLoader:
    """Load plugins from directories."""

    @staticmethod
    def load_manifest(path: str | Path) -> tuple[
        PluginManifest | None, list[str],
    ]:
        """Load a plugin manifest from a file.

        Args:
            path: Path to plugin.json.

        Returns:
            Tuple of (manifest, errors).
        """
        filepath = Path(path)
        if not filepath.exists():
            return None, [f"Manifest not found: {filepath}"]

        try:
            text = filepath.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError) as exc:
            return None, [f"Cannot read manifest: {exc}"]

        try:
            data = json.loads(text)
        except json.JSONDecodeError as exc:
            return None, [f"Invalid JSON: {exc}"]

        if not isinstance(data, dict):
            return None, ["Manifest must be a JSON object"]

        manifest = PluginManifest.from_dict(
            data, source_path=str(filepath),
        )
        errors = manifest.validate()
        if errors:
            return manifest, errors

        return manifest, []

    @staticmethod
    def load_from_directory(
        directory: str | Path,
    ) -> tuple[PluginManifest | None, list[str]]:
        """Load a plugin from a directory containing plugin.json.

        Args:
            directory: Path to plugin directory.

        Returns:
            Tuple of (manifest, errors).
        """
        dirpath = Path(directory)
        if not dirpath.is_dir():
            return None, [f"Not a directory: {dirpath}"]

        manifest_path = dirpath / _MANIFEST_FILE
        return PluginLoader.load_manifest(manifest_path)


# ---------------------------------------------------------------------------
# Plugin registry
# ---------------------------------------------------------------------------

class PluginRegistry:
    """Registry for managing loaded plugins."""

    def __init__(self) -> None:
        self._plugins: dict[str, PluginManifest] = {}

    @property
    def count(self) -> int:
        return len(self._plugins)

    def register(
        self, manifest: PluginManifest,
    ) -> list[str]:
        """Register a plugin. Returns errors."""
        errors = manifest.validate()
        if errors:
            return errors
        self._plugins[manifest.name] = manifest
        return []

    def unregister(self, name: str) -> bool:
        """Unregister a plugin. Returns True if found."""
        if name in self._plugins:
            del self._plugins[name]
            return True
        return False

    def get(self, name: str) -> PluginManifest | None:
        """Get a plugin by name."""
        return self._plugins.get(name)

    def list_plugins(self) -> list[PluginManifest]:
        """List all registered plugins."""
        return list(self._plugins.values())

    def list_names(self) -> list[str]:
        """List all registered plugin names."""
        return sorted(self._plugins.keys())

    def get_all_rules(self) -> list[PluginRule]:
        """Get all rules from all plugins."""
        rules: list[PluginRule] = []
        for manifest in self._plugins.values():
            rules.extend(manifest.rules)
        return rules

    def clear(self) -> None:
        """Remove all plugins."""
        self._plugins.clear()


# ---------------------------------------------------------------------------
# Plugin runner
# ---------------------------------------------------------------------------

class PluginRunner:
    """Execute plugin rules against content."""

    def __init__(self, registry: PluginRegistry) -> None:
        self._registry = registry

    def run_rule(
        self,
        rule: PluginRule,
        content: str,
        file_path: str = "",
        plugin_name: str = "",
    ) -> list[PluginMatch]:
        """Run a single rule against content.

        Args:
            rule: The rule to execute.
            content: File content to scan.
            file_path: Path of the file being scanned.
            plugin_name: Name of the plugin owning the rule.

        Returns:
            List of matches found.
        """
        if not rule.enabled:
            return []

        matches: list[PluginMatch] = []
        try:
            compiled = re.compile(rule.pattern)
        except re.error:
            return []

        lines = content.split("\n")
        for line_num, line_text in enumerate(lines, start=1):
            if compiled.search(line_text):
                matches.append(PluginMatch(
                    rule_id=rule.rule_id,
                    severity=rule.severity,
                    title=rule.title or f"Match: {rule.rule_id}",
                    description=rule.description,
                    file=file_path,
                    line=line_num,
                    code_snippet=line_text.strip(),
                    fix_suggestion=rule.fix_suggestion,
                    plugin_name=plugin_name,
                ))

        return matches

    def run_plugin(
        self,
        plugin_name: str,
        content: str,
        file_path: str = "",
    ) -> list[PluginMatch]:
        """Run all rules from a plugin against content.

        Args:
            plugin_name: Name of the registered plugin.
            content: File content.
            file_path: Path of the file.

        Returns:
            List of matches.
        """
        manifest = self._registry.get(plugin_name)
        if manifest is None:
            return []

        all_matches: list[PluginMatch] = []
        for rule in manifest.rules:
            all_matches.extend(
                self.run_rule(rule, content, file_path, plugin_name)
            )
        return all_matches

    def run_all(
        self,
        content: str,
        file_path: str = "",
    ) -> list[PluginMatch]:
        """Run all rules from all plugins against content.

        Args:
            content: File content.
            file_path: Path of the file.

        Returns:
            List of all matches.
        """
        all_matches: list[PluginMatch] = []
        for manifest in self._registry.list_plugins():
            for rule in manifest.rules:
                all_matches.extend(
                    self.run_rule(
                        rule, content, file_path, manifest.name,
                    )
                )
        return all_matches


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

def format_plugins_text(
    registry: PluginRegistry,
) -> str:
    """Format registered plugins as text summary.

    Args:
        registry: Plugin registry.

    Returns:
        Text summary.
    """
    plugins = registry.list_plugins()
    if not plugins:
        return "No plugins registered.\n"

    lines = [f"Registered Plugins ({registry.count})"]
    lines.append("=" * 40)
    for p in plugins:
        lines.append(f"  {p.name} v{p.version}")
        if p.author:
            lines.append(f"    Author: {p.author}")
        if p.description:
            lines.append(f"    {p.description}")
        lines.append(f"    Rules: {len(p.rules)}")

    return "\n".join(lines) + "\n"


def format_matches_json(
    matches: list[PluginMatch],
) -> str:
    """Format plugin matches as JSON.

    Args:
        matches: List of matches.

    Returns:
        JSON string.
    """
    data = {
        "matches": [m.to_dict() for m in matches],
        "total": len(matches),
    }
    return json.dumps(data, indent=2, ensure_ascii=False)
