"""MCP Scan Profiles — Predefined scanning configurations.

Provides preset scan profiles (quick, full, ci, compliance, dev) and
support for custom profiles loaded from JSON files. Each profile
controls which rules are enabled, severity thresholds, output format,
and additional scanning options.

CLI: mcpaudit scan <path> --profile quick|full|ci|compliance|dev
     mcpaudit scan <path> --custom-profile my_profile.json

Zero external dependencies — pure Python stdlib.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .models import Severity


# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------

@dataclass
class ScanProfile:
    """A scan profile defining how to scan a target.

    Attributes:
        name: Unique profile name.
        description: Human-readable description.
        rules_enabled: List of rule IDs to enable (None = all rules).
        rules_disabled: List of rule IDs to disable.
        severity_threshold: Minimum severity to report.
        output_format: Default output format (text, json, html, sarif).
        options: Additional options (timeout, max_files, etc).
    """

    name: str
    description: str = ""
    rules_enabled: list[str] | None = None
    rules_disabled: list[str] = field(default_factory=list)
    severity_threshold: str = "LOW"
    output_format: str = "text"
    options: dict[str, Any] = field(default_factory=dict)

    @property
    def severity(self) -> Severity:
        """Parse severity_threshold to Severity enum."""
        return Severity.from_string(self.severity_threshold)

    def to_dict(self) -> dict[str, Any]:
        """Serialize profile to dictionary."""
        return {
            "name": self.name,
            "description": self.description,
            "rules_enabled": self.rules_enabled,
            "rules_disabled": list(self.rules_disabled),
            "severity_threshold": self.severity_threshold,
            "output_format": self.output_format,
            "options": dict(self.options),
        }

    def to_json(self) -> str:
        """Serialize profile to JSON string."""
        return json.dumps(self.to_dict(), indent=2)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ScanProfile:
        """Deserialize from dictionary.

        Args:
            data: Dictionary with profile fields.

        Returns:
            ScanProfile instance.

        Raises:
            ValueError: If required 'name' field is missing.
        """
        name = data.get("name", "")
        if not name:
            raise ValueError("Profile must have a 'name' field")
        return cls(
            name=name,
            description=data.get("description", ""),
            rules_enabled=data.get("rules_enabled"),
            rules_disabled=data.get("rules_disabled", []),
            severity_threshold=data.get("severity_threshold", "LOW"),
            output_format=data.get("output_format", "text"),
            options=data.get("options", {}),
        )

    @classmethod
    def from_json(cls, json_str: str) -> ScanProfile:
        """Deserialize from JSON string.

        Args:
            json_str: JSON string with profile data.

        Returns:
            ScanProfile instance.
        """
        data = json.loads(json_str)
        return cls.from_dict(data)


# ---------------------------------------------------------------------------
# Built-in profiles
# ---------------------------------------------------------------------------

PROFILE_QUICK = ScanProfile(
    name="quick",
    description="Fast scan, only CRITICAL and HIGH findings, text output",
    rules_enabled=None,
    severity_threshold="HIGH",
    output_format="text",
    options={
        "max_files": 100,
        "deobfuscate": False,
        "timeout_seconds": 30,
    },
)

PROFILE_FULL = ScanProfile(
    name="full",
    description="Complete scan, all rules, all severities, thorough analysis",
    rules_enabled=None,
    severity_threshold="INFO",
    output_format="text",
    options={
        "deobfuscate": True,
        "timeout_seconds": 300,
    },
)

PROFILE_CI = ScanProfile(
    name="ci",
    description="CI/CD pipeline profile, JSON output, fail on HIGH+",
    rules_enabled=None,
    severity_threshold="HIGH",
    output_format="json",
    options={
        "fail_on_findings": True,
        "deobfuscate": True,
        "timeout_seconds": 120,
        "quiet": True,
    },
)

PROFILE_COMPLIANCE = ScanProfile(
    name="compliance",
    description="Compliance-focused scan, OWASP rules, all severities, HTML report",
    rules_enabled=[
        # OWASP Agentic Top 10
        "MCP-004", "CMD-001", "CMD-002", "SQL-001", "PATH-001", "DESER-001",
        "PERM-001", "PRIV-001", "INFO-001", "ERR-001", "AUTH-001", "AUTHZ-001",
        "VAL-001", "IDOR-001", "SSRF-001", "SEC-001", "MCP-003", "FILE-001",
        "LOG-001", "RATE-001", "RES-001", "REDOS-001", "PATH-002", "TEMP-001",
        "CORS-001", "MCP-001", "MCP-002",
    ],
    severity_threshold="INFO",
    output_format="html",
    options={
        "deobfuscate": True,
        "compliance_report": True,
        "timeout_seconds": 300,
    },
)

PROFILE_DEV = ScanProfile(
    name="dev",
    description="Development profile, relaxed, no MCP protocol rules, text output",
    rules_disabled=[
        "MCP-001", "MCP-002", "MCP-003", "MCP-004", "MCP-005", "MCP-006",
        "MCP-007", "MCP-008", "MCP-009", "MCP-010", "MCP-011", "MCP-012",
        "MCP-013", "MCP-014", "MCP-015", "MCP-016", "MCP-017", "MCP-018",
        "MCP-019",
    ],
    severity_threshold="MEDIUM",
    output_format="text",
    options={
        "deobfuscate": False,
        "timeout_seconds": 60,
    },
)


# ---------------------------------------------------------------------------
# Profile registry
# ---------------------------------------------------------------------------

_BUILTIN_PROFILES: dict[str, ScanProfile] = {
    "quick": PROFILE_QUICK,
    "full": PROFILE_FULL,
    "ci": PROFILE_CI,
    "compliance": PROFILE_COMPLIANCE,
    "dev": PROFILE_DEV,
}


class ProfileRegistry:
    """Registry for scan profiles.

    Manages built-in and custom profiles.
    """

    def __init__(self) -> None:
        self._profiles: dict[str, ScanProfile] = dict(_BUILTIN_PROFILES)

    def get(self, name: str) -> ScanProfile:
        """Get a profile by name.

        Args:
            name: Profile name (case-insensitive).

        Returns:
            ScanProfile instance.

        Raises:
            ValueError: If profile name is unknown.
        """
        key = name.lower().strip()
        if key not in self._profiles:
            valid = ", ".join(sorted(self._profiles.keys()))
            raise ValueError(
                f"Unknown profile '{name}'. Available: {valid}"
            )
        return self._profiles[key]

    def register(self, profile: ScanProfile) -> None:
        """Register a custom profile.

        Args:
            profile: ScanProfile to register.

        Raises:
            ValueError: If profile name is empty.
        """
        if not profile.name:
            raise ValueError("Profile must have a name")
        self._profiles[profile.name.lower().strip()] = profile

    def unregister(self, name: str) -> bool:
        """Remove a profile from the registry.

        Args:
            name: Profile name to remove.

        Returns:
            True if removed, False if not found.
        """
        key = name.lower().strip()
        if key in self._profiles:
            del self._profiles[key]
            return True
        return False

    def list_profiles(self) -> list[ScanProfile]:
        """List all registered profiles, sorted by name."""
        return [
            self._profiles[k]
            for k in sorted(self._profiles.keys())
        ]

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

    @property
    def builtin_names(self) -> list[str]:
        """Return names of built-in profiles."""
        return sorted(_BUILTIN_PROFILES.keys())

    def has(self, name: str) -> bool:
        """Check if a profile exists.

        Args:
            name: Profile name.

        Returns:
            True if profile is registered.
        """
        return name.lower().strip() in self._profiles

    def load_from_file(self, path: str | Path) -> ScanProfile:
        """Load a custom profile from a JSON file.

        Args:
            path: Path to JSON profile file.

        Returns:
            Loaded ScanProfile instance.

        Raises:
            FileNotFoundError: If file does not exist.
            ValueError: If JSON is invalid or missing required fields.
        """
        p = Path(path)
        if not p.exists():
            raise FileNotFoundError(f"Profile file not found: {p}")
        content = p.read_text(encoding="utf-8")
        profile = ScanProfile.from_json(content)
        self.register(profile)
        return profile

    def save_to_file(self, name: str, path: str | Path) -> None:
        """Save a profile to a JSON file.

        Args:
            name: Profile name to save.
            path: Output file path.

        Raises:
            ValueError: If profile not found.
        """
        profile = self.get(name)
        p = Path(path)
        p.write_text(profile.to_json(), encoding="utf-8")


# Singleton registry
_registry = ProfileRegistry()


# ---------------------------------------------------------------------------
# Module-level functions
# ---------------------------------------------------------------------------

def get_profile(name: str) -> ScanProfile:
    """Get a scan profile by name.

    Args:
        name: Profile name (quick, full, ci, compliance, dev, or custom).

    Returns:
        ScanProfile instance.

    Raises:
        ValueError: If profile not found.
    """
    return _registry.get(name)


def list_scan_profiles() -> list[ScanProfile]:
    """List all available scan profiles."""
    return _registry.list_profiles()


def list_profile_names() -> list[str]:
    """List all profile names."""
    return _registry.list_names()


def register_profile(profile: ScanProfile) -> None:
    """Register a custom scan profile."""
    _registry.register(profile)


def load_profile_from_file(path: str | Path) -> ScanProfile:
    """Load a custom profile from a JSON file.

    Args:
        path: Path to JSON profile file.

    Returns:
        Loaded ScanProfile.
    """
    return _registry.load_from_file(path)


def get_registry() -> ProfileRegistry:
    """Get the global profile registry."""
    return _registry


def apply_profile_filters(
    profile: ScanProfile,
    findings: list[Any],
) -> list[Any]:
    """Filter findings according to profile settings.

    Filters by severity threshold and enabled/disabled rules.

    Args:
        profile: ScanProfile to apply.
        findings: List of Finding objects.

    Returns:
        Filtered list of findings.
    """
    threshold = profile.severity
    severity_order = list(Severity)
    threshold_idx = severity_order.index(threshold)

    result = [
        f for f in findings
        if severity_order.index(f.severity) <= threshold_idx
    ]

    # Filter by enabled rules
    if profile.rules_enabled is not None:
        enabled_set = set(profile.rules_enabled)
        result = [f for f in result if f.rule_id in enabled_set]

    # Filter by disabled rules
    if profile.rules_disabled:
        disabled_set = set(profile.rules_disabled)
        result = [f for f in result if f.rule_id not in disabled_set]

    return result


def apply_profile_to_rules(
    profile: ScanProfile,
    rules: list[Any],
) -> list[Any]:
    """Filter rules according to profile settings.

    Args:
        profile: ScanProfile to apply.
        rules: List of Rule instances.

    Returns:
        Filtered list of rules.
    """
    result = list(rules)

    if profile.rules_enabled is not None:
        enabled_set = set(profile.rules_enabled)
        result = [r for r in result if r.rule_id in enabled_set]

    if profile.rules_disabled:
        disabled_set = set(profile.rules_disabled)
        result = [r for r in result if r.rule_id not in disabled_set]

    return result


def format_profiles_text() -> str:
    """Format all profiles as human-readable text.

    Returns:
        Formatted string describing each profile.
    """
    lines: list[str] = []
    lines.append(f"\n{'=' * 60}")
    lines.append("  MCP Security Audit — Scan Profiles")
    lines.append(f"{'=' * 60}\n")

    for profile in list_scan_profiles():
        rules_desc = (
            "all" if profile.rules_enabled is None
            else str(len(profile.rules_enabled))
        )
        disabled_desc = ""
        if profile.rules_disabled:
            disabled_desc = f", {len(profile.rules_disabled)} disabled"

        lines.append(f"  {profile.name}")
        lines.append(f"    {profile.description}")
        lines.append(
            f"    Rules: {rules_desc}{disabled_desc} | "
            f"Severity: >= {profile.severity_threshold} | "
            f"Format: {profile.output_format}"
        )
        if profile.options:
            opts = ", ".join(f"{k}={v}" for k, v in profile.options.items())
            lines.append(f"    Options: {opts}")
        lines.append("")

    lines.append(f"{'=' * 60}")
    return "\n".join(lines)


def format_profiles_json() -> str:
    """Format all profiles as JSON.

    Returns:
        JSON string with all profiles.
    """
    profiles = [p.to_dict() for p in list_scan_profiles()]
    return json.dumps({"profiles": profiles}, indent=2)


def validate_profile(profile: ScanProfile) -> list[str]:
    """Validate a scan profile for correctness.

    Args:
        profile: Profile to validate.

    Returns:
        List of validation errors (empty if valid).
    """
    errors: list[str] = []

    if not profile.name:
        errors.append("Profile name is required")

    if not profile.name.replace("-", "").replace("_", "").isalnum():
        errors.append(
            f"Profile name '{profile.name}' contains invalid characters"
        )

    try:
        Severity.from_string(profile.severity_threshold)
    except ValueError:
        errors.append(
            f"Invalid severity threshold: {profile.severity_threshold}"
        )

    valid_formats = {"text", "json", "html", "sarif"}
    if profile.output_format not in valid_formats:
        errors.append(
            f"Invalid output format: {profile.output_format}. "
            f"Valid: {', '.join(sorted(valid_formats))}"
        )

    return errors
