"""Severity Customization for MCP Security Audit.

Allows per-project severity overrides via a `.mcpauditrc` JSON config file.
Projects can:
- Elevate or reduce severity of specific rules
- Ignore rules entirely
- Set custom trust-score thresholds
- Configure global minimum severity filter

Config format (.mcpauditrc):
{
  "rules": {
    "SEC-001": {"severity": "LOW"},
    "MCP-003": {"ignore": true},
    "CMD-001": {"severity": "CRITICAL", "note": "Zero tolerance"}
  },
  "threshold": 60,
  "min_severity": "MEDIUM"
}

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

from __future__ import annotations

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


# ---------------------------------------------------------------------------
# Valid values
# ---------------------------------------------------------------------------

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

_SEVERITY_ORDER: dict[str, int] = {
    "CRITICAL": 0,
    "HIGH": 1,
    "MEDIUM": 2,
    "LOW": 3,
    "INFO": 4,
}


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

@dataclass
class RuleOverride:
    """Override configuration for a single rule."""

    rule_id: str
    severity: str | None = None  # New severity or None if unchanged
    ignore: bool = False
    note: str = ""

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {}
        if self.severity is not None:
            result["severity"] = self.severity
        if self.ignore:
            result["ignore"] = True
        if self.note:
            result["note"] = self.note
        return result


@dataclass
class SeverityConfig:
    """Parsed severity configuration from .mcpauditrc."""

    rules: dict[str, RuleOverride] = field(default_factory=dict)
    threshold: int = 0
    min_severity: str = ""
    raw: dict[str, Any] = field(default_factory=dict)
    source_path: str = ""
    errors: list[str] = field(default_factory=list)

    @property
    def is_valid(self) -> bool:
        """Whether the config parsed without errors."""
        return len(self.errors) == 0

    @property
    def ignored_rules(self) -> list[str]:
        """List of rule IDs that should be ignored."""
        return [
            rid for rid, ov in self.rules.items() if ov.ignore
        ]

    @property
    def overridden_rules(self) -> list[str]:
        """List of rule IDs with severity overrides."""
        return [
            rid for rid, ov in self.rules.items()
            if ov.severity is not None and not ov.ignore
        ]

    def to_dict(self) -> dict[str, Any]:
        rules_dict = {
            rid: ov.to_dict() for rid, ov in self.rules.items()
        }
        result: dict[str, Any] = {"rules": rules_dict}
        if self.threshold:
            result["threshold"] = self.threshold
        if self.min_severity:
            result["min_severity"] = self.min_severity
        return result


@dataclass
class OverrideResult:
    """Result of applying severity overrides to findings."""

    findings: list[dict[str, Any]]
    ignored_count: int = 0
    overridden_count: int = 0
    filtered_count: int = 0
    changes: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_findings": len(self.findings),
            "ignored_count": self.ignored_count,
            "overridden_count": self.overridden_count,
            "filtered_count": self.filtered_count,
            "changes": self.changes,
        }


# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------

def validate_config(data: dict[str, Any]) -> list[str]:
    """Validate a .mcpauditrc config dict.

    Args:
        data: Parsed JSON config dict.

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

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

    # Validate rules section
    rules = data.get("rules", {})
    if not isinstance(rules, dict):
        errors.append("'rules' must be an object")
    else:
        errors.extend(_validate_rules(rules))

    # Validate threshold
    threshold = data.get("threshold")
    if threshold is not None:
        if not isinstance(threshold, (int, float)):
            errors.append("'threshold' must be a number")
        elif threshold < 0 or threshold > 100:
            errors.append("'threshold' must be between 0 and 100")

    # Validate min_severity
    min_sev = data.get("min_severity")
    if min_sev is not None:
        if not isinstance(min_sev, str):
            errors.append("'min_severity' must be a string")
        elif min_sev.upper() not in _VALID_SEVERITIES:
            valid = ", ".join(sorted(_VALID_SEVERITIES))
            errors.append(
                f"'min_severity' invalid: '{min_sev}'. "
                f"Valid: {valid}"
            )

    return errors


def _validate_rules(rules: dict[str, Any]) -> list[str]:
    """Validate the rules section of the config."""
    errors: list[str] = []
    for rule_id, rule_cfg in rules.items():
        if not isinstance(rule_id, str):
            errors.append(f"Rule key must be a string, got: {type(rule_id)}")
            continue
        if not isinstance(rule_cfg, dict):
            errors.append(
                f"Rule '{rule_id}': value must be an object"
            )
            continue
        sev = rule_cfg.get("severity")
        if sev is not None:
            if not isinstance(sev, str):
                errors.append(
                    f"Rule '{rule_id}': severity must be a string"
                )
            elif sev.upper() not in _VALID_SEVERITIES:
                errors.append(
                    f"Rule '{rule_id}': invalid severity '{sev}'"
                )
        ignore = rule_cfg.get("ignore")
        if ignore is not None and not isinstance(ignore, bool):
            errors.append(
                f"Rule '{rule_id}': 'ignore' must be a boolean"
            )
    return errors


# ---------------------------------------------------------------------------
# Loading
# ---------------------------------------------------------------------------

def load_severity_config(path: str | Path) -> SeverityConfig:
    """Load and parse a .mcpauditrc config file.

    Args:
        path: Path to the .mcpauditrc JSON file.

    Returns:
        SeverityConfig with parsed rules and overrides.
    """
    config = SeverityConfig(source_path=str(path))
    filepath = Path(path)

    if not filepath.exists():
        config.errors.append(f"Config file not found: {filepath}")
        return config

    try:
        text = filepath.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError) as exc:
        config.errors.append(f"Cannot read config: {exc}")
        return config

    try:
        data = json.loads(text)
    except json.JSONDecodeError as exc:
        config.errors.append(f"Invalid JSON: {exc}")
        return config

    return parse_severity_config(data, source_path=str(filepath))


def parse_severity_config(
    data: dict[str, Any],
    source_path: str = "",
) -> SeverityConfig:
    """Parse a config dict into SeverityConfig.

    Args:
        data: Parsed JSON config.
        source_path: Optional source path for reporting.

    Returns:
        SeverityConfig instance.
    """
    config = SeverityConfig(
        raw=data,
        source_path=source_path,
    )

    errors = validate_config(data)
    if errors:
        config.errors = errors
        return config

    # Parse rules
    rules_data = data.get("rules", {})
    for rule_id, rule_cfg in rules_data.items():
        if not isinstance(rule_cfg, dict):
            continue
        sev = rule_cfg.get("severity")
        config.rules[rule_id] = RuleOverride(
            rule_id=rule_id,
            severity=sev.upper() if isinstance(sev, str) else None,
            ignore=bool(rule_cfg.get("ignore", False)),
            note=str(rule_cfg.get("note", "")),
        )

    # Parse threshold
    threshold = data.get("threshold")
    if isinstance(threshold, (int, float)):
        config.threshold = int(threshold)

    # Parse min_severity
    min_sev = data.get("min_severity")
    if isinstance(min_sev, str) and min_sev.upper() in _VALID_SEVERITIES:
        config.min_severity = min_sev.upper()

    return config


# ---------------------------------------------------------------------------
# Applying overrides
# ---------------------------------------------------------------------------

def apply_severity_overrides(
    findings: list[dict[str, Any]],
    config: SeverityConfig,
) -> list[dict[str, Any]]:
    """Apply severity overrides to findings.

    - Removes ignored rules
    - Changes severity of overridden rules
    - Filters by min_severity

    Args:
        findings: List of finding dicts.
        config: Parsed severity config.

    Returns:
        New list of finding dicts with overrides applied.
    """
    result = _apply_overrides_internal(findings, config)
    return result.findings


def apply_severity_overrides_detailed(
    findings: list[dict[str, Any]],
    config: SeverityConfig,
) -> OverrideResult:
    """Apply overrides and return detailed result with stats.

    Args:
        findings: List of finding dicts.
        config: Parsed severity config.

    Returns:
        OverrideResult with findings and change statistics.
    """
    return _apply_overrides_internal(findings, config)


def _apply_overrides_internal(
    findings: list[dict[str, Any]],
    config: SeverityConfig,
) -> OverrideResult:
    """Internal implementation of override application."""
    result = OverrideResult(findings=[])
    min_sev_order = _SEVERITY_ORDER.get(config.min_severity, 99)

    for finding in findings:
        finding_copy = dict(finding)
        rule_id = finding_copy.get("rule_id", "")

        # Check ignored rules
        override = config.rules.get(rule_id)
        if override and override.ignore:
            result.ignored_count += 1
            result.changes.append(f"Ignored {rule_id}")
            continue

        # Apply severity override
        if override and override.severity:
            old_sev = finding_copy.get("severity", "INFO")
            if old_sev != override.severity:
                finding_copy["severity"] = override.severity
                finding_copy["severity_original"] = old_sev
                result.overridden_count += 1
                result.changes.append(
                    f"{rule_id}: {old_sev} -> {override.severity}"
                )

        # Filter by min_severity
        current_sev = finding_copy.get("severity", "INFO")
        sev_order = _SEVERITY_ORDER.get(current_sev, 99)
        if config.min_severity and sev_order > min_sev_order:
            result.filtered_count += 1
            continue

        result.findings.append(finding_copy)

    return result


# ---------------------------------------------------------------------------
# Merge with defaults
# ---------------------------------------------------------------------------

def merge_configs(
    base: SeverityConfig,
    override: SeverityConfig,
) -> SeverityConfig:
    """Merge two configs, with override taking precedence.

    Args:
        base: Base configuration (defaults).
        override: Override configuration (project-specific).

    Returns:
        Merged SeverityConfig.
    """
    merged = SeverityConfig()

    # Copy base rules, then override
    for rid, ov in base.rules.items():
        merged.rules[rid] = RuleOverride(
            rule_id=ov.rule_id,
            severity=ov.severity,
            ignore=ov.ignore,
            note=ov.note,
        )
    for rid, ov in override.rules.items():
        merged.rules[rid] = RuleOverride(
            rule_id=ov.rule_id,
            severity=ov.severity,
            ignore=ov.ignore,
            note=ov.note,
        )

    # Override wins for threshold and min_severity
    merged.threshold = override.threshold or base.threshold
    merged.min_severity = override.min_severity or base.min_severity

    return merged


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

def format_config_text(config: SeverityConfig) -> str:
    """Format severity config as human-readable text.

    Args:
        config: Parsed config.

    Returns:
        Text summary.
    """
    lines = ["Severity Configuration"]
    lines.append("=" * 40)

    if config.source_path:
        lines.append(f"Source: {config.source_path}")

    if not config.is_valid:
        lines.append(f"INVALID — {len(config.errors)} error(s):")
        for err in config.errors:
            lines.append(f"  ! {err}")
        return "\n".join(lines)

    if config.threshold:
        lines.append(f"Threshold: {config.threshold}")
    if config.min_severity:
        lines.append(f"Min severity: {config.min_severity}")

    ignored = config.ignored_rules
    if ignored:
        lines.append(f"\nIgnored rules ({len(ignored)}):")
        for rid in sorted(ignored):
            lines.append(f"  - {rid}")

    overridden = config.overridden_rules
    if overridden:
        lines.append(f"\nSeverity overrides ({len(overridden)}):")
        for rid in sorted(overridden):
            ov = config.rules[rid]
            lines.append(f"  {rid} -> {ov.severity}")

    if not ignored and not overridden:
        lines.append("No overrides configured.")

    return "\n".join(lines)


def format_config_json(config: SeverityConfig) -> str:
    """Format severity config as JSON.

    Args:
        config: Parsed config.

    Returns:
        JSON string.
    """
    data = config.to_dict()
    data["is_valid"] = config.is_valid
    data["errors"] = config.errors
    if config.source_path:
        data["source_path"] = config.source_path
    return json.dumps(data, indent=2, ensure_ascii=False)


def create_default_rc() -> str:
    """Generate a default .mcpauditrc file content.

    Returns:
        JSON string with commented example config.
    """
    default = {
        "rules": {
            "SEC-001": {"severity": "CRITICAL", "note": "Example override"},
            "INFO-001": {"ignore": True, "note": "Suppress info leaks"},
        },
        "threshold": 70,
        "min_severity": "LOW",
    }
    return json.dumps(default, indent=2, ensure_ascii=False) + "\n"
