"""Rule Validator for MCP Security Audit.

Validates that security rules are well-formed, consistent, and
properly documented before they are included in scan profiles.

Features:
- Validate rule structure (required fields, types)
- Check for duplicate rule IDs
- Verify severity consistency across related rules
- Validate CWE/OWASP references
- Detect conflicting rules
- Generate validation report

CLI: mcpaudit validate-rules --rules-dir | --profile

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

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from typing import Any, Optional


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

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

_CWE_PATTERN = re.compile(r"^CWE-\d+$")

_OWASP_CATEGORIES = frozenset({
    "A01", "A02", "A03", "A04", "A05",
    "A06", "A07", "A08", "A09", "A10",
})

_RULE_ID_PATTERN = re.compile(r"^[A-Z][A-Z0-9]*-\d{3,}$")

_REQUIRED_FIELDS = frozenset({"rule_id", "severity", "title"})


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

@dataclass
class ValidationIssue:
    """An issue found during rule validation."""
    rule_id: str
    level: str  # ERROR, WARNING, INFO
    message: str
    field: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "level": self.level,
            "message": self.message,
            "field": self.field,
        }


@dataclass
class ValidationResult:
    """Result of validating a set of rules."""
    rules_checked: int = 0
    issues: list[ValidationIssue] = field(default_factory=list)

    @property
    def error_count(self) -> int:
        return sum(1 for i in self.issues if i.level == "ERROR")

    @property
    def warning_count(self) -> int:
        return sum(1 for i in self.issues if i.level == "WARNING")

    @property
    def is_valid(self) -> bool:
        return self.error_count == 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "rules_checked": self.rules_checked,
            "errors": self.error_count,
            "warnings": self.warning_count,
            "is_valid": self.is_valid,
            "issues": [i.to_dict() for i in self.issues],
        }

    def to_json(self, indent: int = 2) -> str:
        return json.dumps(self.to_dict(), indent=indent)


# ---------------------------------------------------------------------------
# Validator
# ---------------------------------------------------------------------------

class RuleValidator:
    """Validates security rules for correctness and consistency."""

    def __init__(self) -> None:
        self._custom_checks: list[Any] = []

    def validate_rule(self, rule: dict[str, Any]) -> list[ValidationIssue]:
        """Validate a single rule definition."""
        issues = []
        rule_id = rule.get("rule_id", "UNKNOWN")

        # Required fields
        for field_name in _REQUIRED_FIELDS:
            if field_name not in rule or not rule[field_name]:
                issues.append(ValidationIssue(
                    rule_id=rule_id,
                    level="ERROR",
                    message=f"Missing required field: {field_name}",
                    field=field_name,
                ))

        # Rule ID format
        if "rule_id" in rule and rule["rule_id"]:
            if not _RULE_ID_PATTERN.match(rule["rule_id"]):
                issues.append(ValidationIssue(
                    rule_id=rule_id,
                    level="ERROR",
                    message=f"Invalid rule ID format: '{rule['rule_id']}'. Expected PREFIX-NNN",
                    field="rule_id",
                ))

        # Severity
        severity = rule.get("severity", "")
        if severity and severity not in _VALID_SEVERITIES:
            issues.append(ValidationIssue(
                rule_id=rule_id,
                level="ERROR",
                message=f"Invalid severity: '{severity}'",
                field="severity",
            ))

        # CWE reference
        cwe = rule.get("cwe", "")
        if cwe and not _CWE_PATTERN.match(cwe):
            issues.append(ValidationIssue(
                rule_id=rule_id,
                level="WARNING",
                message=f"Invalid CWE format: '{cwe}'. Expected CWE-NNN",
                field="cwe",
            ))

        # OWASP category
        owasp = rule.get("owasp", "")
        if owasp and owasp not in _OWASP_CATEGORIES:
            issues.append(ValidationIssue(
                rule_id=rule_id,
                level="WARNING",
                message=f"Unknown OWASP category: '{owasp}'",
                field="owasp",
            ))

        # Description length
        desc = rule.get("description", "")
        if desc and len(desc) < 10:
            issues.append(ValidationIssue(
                rule_id=rule_id,
                level="WARNING",
                message="Description is very short (< 10 chars)",
                field="description",
            ))

        # Recommendation
        if not rule.get("recommendation"):
            issues.append(ValidationIssue(
                rule_id=rule_id,
                level="INFO",
                message="No recommendation provided",
                field="recommendation",
            ))

        return issues

    def validate_rules(self, rules: list[dict[str, Any]]) -> ValidationResult:
        """Validate a list of rules."""
        result = ValidationResult(rules_checked=len(rules))

        # Individual validation
        for rule in rules:
            result.issues.extend(self.validate_rule(rule))

        # Duplicate check
        seen_ids: dict[str, int] = {}
        for rule in rules:
            rid = rule.get("rule_id", "")
            if rid:
                seen_ids[rid] = seen_ids.get(rid, 0) + 1
        for rid, count in seen_ids.items():
            if count > 1:
                result.issues.append(ValidationIssue(
                    rule_id=rid,
                    level="ERROR",
                    message=f"Duplicate rule ID: '{rid}' appears {count} times",
                    field="rule_id",
                ))

        # Severity consistency check
        self._check_severity_consistency(rules, result)

        return result

    def _check_severity_consistency(self, rules: list[dict[str, Any]],
                                     result: ValidationResult) -> None:
        """Check that related rules have consistent severities."""
        # Group by prefix
        prefix_severities: dict[str, set[str]] = {}
        for rule in rules:
            rid = rule.get("rule_id", "")
            sev = rule.get("severity", "")
            if rid and sev:
                prefix = rid.split("-")[0] if "-" in rid else rid
                prefix_severities.setdefault(prefix, set()).add(sev)

        # Warn if a prefix has both CRITICAL and LOW/INFO
        for prefix, sevs in prefix_severities.items():
            if "CRITICAL" in sevs and ("LOW" in sevs or "INFO" in sevs):
                result.issues.append(ValidationIssue(
                    rule_id=prefix,
                    level="WARNING",
                    message=f"Prefix '{prefix}' has both CRITICAL and LOW/INFO rules — verify intentional",
                    field="severity",
                ))

    def markdown_report(self, result: ValidationResult) -> str:
        """Generate validation report."""
        lines = ["# Rule Validation Report", ""]
        lines.append(f"- **Rules checked**: {result.rules_checked}")
        lines.append(f"- **Errors**: {result.error_count}")
        lines.append(f"- **Warnings**: {result.warning_count}")
        lines.append(f"- **Valid**: {'Yes' if result.is_valid else 'No'}")
        lines.append("")

        if result.issues:
            errors = [i for i in result.issues if i.level == "ERROR"]
            warnings = [i for i in result.issues if i.level == "WARNING"]

            if errors:
                lines.append("## Errors")
                for i in errors:
                    lines.append(f"- **{i.rule_id}** [{i.field}]: {i.message}")
                lines.append("")

            if warnings:
                lines.append("## Warnings")
                for i in warnings:
                    lines.append(f"- **{i.rule_id}** [{i.field}]: {i.message}")
                lines.append("")
        else:
            lines.append("All rules are valid.")

        return "\n".join(lines)
