"""Rule Impact Analyzer for MCP Security Audit.

Analyzes the impact of each security rule across a codebase, including
per-rule hit counts, false positive rate estimation, effectiveness scores,
and tuning recommendations.

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, Sequence


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

# Lines of code thresholds for effectiveness scoring
_LOC_THRESHOLD_LOW = 100
_LOC_THRESHOLD_HIGH = 10000

# Finding/LOC ratio thresholds
_RATIO_NOISE = 0.10     # > 10% of lines flagged = likely noisy
_RATIO_HIGH = 0.05      # > 5% = high impact
_RATIO_MODERATE = 0.01  # > 1% = moderate impact

# False positive heuristic factors
_FP_INDICATORS: dict[str, float] = {
    "INFO": 0.30,        # INFO-level often informational
    "test": 0.40,        # Findings in test files
    "example": 0.35,     # Findings in example files
    "vendor": 0.50,      # Third-party code
    "generated": 0.60,   # Auto-generated code
    "LOW": 0.15,         # LOW-severity slightly higher FP
}

# Severity weight for impact calculation
_SEVERITY_IMPACT: dict[str, int] = {
    "CRITICAL": 10,
    "HIGH": 5,
    "MEDIUM": 2,
    "LOW": 1,
    "INFO": 0,
}


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

@dataclass
class RuleHit:
    """A single hit of a rule in a file."""

    rule_id: str
    file: str
    line: int
    severity: str = "INFO"
    title: str = ""

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


@dataclass
class RuleImpact:
    """Impact analysis for a single rule."""

    rule_id: str
    severity: str
    hit_count: int = 0
    file_count: int = 0
    total_loc: int = 0
    effectiveness: float = 0.0  # findings / LOC ratio
    fp_rate: float = 0.0       # estimated false positive rate 0-1
    recommendation: str = "keep"  # "keep", "tune", "disable"
    reason: str = ""
    files_affected: list[str] = field(default_factory=list)
    impact_score: float = 0.0  # weighted impact

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "severity": self.severity,
            "hit_count": self.hit_count,
            "file_count": self.file_count,
            "total_loc": self.total_loc,
            "effectiveness": round(self.effectiveness, 6),
            "fp_rate": round(self.fp_rate, 4),
            "recommendation": self.recommendation,
            "reason": self.reason,
            "files_affected": self.files_affected[:10],  # Top 10
            "impact_score": round(self.impact_score, 2),
        }


@dataclass
class ImpactReport:
    """Complete rule impact analysis report."""

    rules: list[RuleImpact] = field(default_factory=list)
    total_rules: int = 0
    total_findings: int = 0
    total_loc: int = 0
    rules_to_tune: list[str] = field(default_factory=list)
    rules_to_disable: list[str] = field(default_factory=list)
    rules_to_keep: list[str] = field(default_factory=list)
    top_noisy: list[str] = field(default_factory=list)
    top_effective: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_rules": self.total_rules,
            "total_findings": self.total_findings,
            "total_loc": self.total_loc,
            "rules_to_tune": self.rules_to_tune,
            "rules_to_disable": self.rules_to_disable,
            "rules_to_keep": self.rules_to_keep,
            "top_noisy": self.top_noisy,
            "top_effective": self.top_effective,
            "rules": [r.to_dict() for r in self.rules],
        }


# ---------------------------------------------------------------------------
# False positive estimation
# ---------------------------------------------------------------------------

def estimate_fp_rate(
    hits: list[RuleHit],
    severity: str = "INFO",
) -> float:
    """Estimate false positive rate using heuristics.

    Considers file paths (test, vendor, generated, example) and
    severity level to estimate the likelihood of false positives.

    Args:
        hits: List of rule hits to analyze.
        severity: The rule's severity level.

    Returns:
        Estimated FP rate between 0.0 and 1.0.
    """
    if not hits:
        return 0.0

    fp_score = 0.0
    total_indicators = 0

    # Severity-based baseline
    sev_fp = _FP_INDICATORS.get(severity, 0.0)
    if sev_fp > 0:
        fp_score += sev_fp
        total_indicators += 1

    # Path-based indicators
    test_count = 0
    example_count = 0
    vendor_count = 0
    generated_count = 0

    for hit in hits:
        path_lower = hit.file.lower()
        if "test" in path_lower or "spec" in path_lower:
            test_count += 1
        if "example" in path_lower or "sample" in path_lower:
            example_count += 1
        if "vendor" in path_lower or "node_modules" in path_lower:
            vendor_count += 1
        if "generated" in path_lower or "auto_gen" in path_lower:
            generated_count += 1

    total = len(hits)
    if total > 0:
        if test_count / total > 0.5:
            fp_score += _FP_INDICATORS["test"]
            total_indicators += 1
        if example_count / total > 0.3:
            fp_score += _FP_INDICATORS["example"]
            total_indicators += 1
        if vendor_count / total > 0.3:
            fp_score += _FP_INDICATORS["vendor"]
            total_indicators += 1
        if generated_count / total > 0.3:
            fp_score += _FP_INDICATORS["generated"]
            total_indicators += 1

    if total_indicators == 0:
        return 0.05  # Base rate

    fp_rate = fp_score / total_indicators
    return min(1.0, max(0.0, fp_rate))


# ---------------------------------------------------------------------------
# Effectiveness & recommendation
# ---------------------------------------------------------------------------

def compute_effectiveness(
    hit_count: int,
    total_loc: int,
) -> float:
    """Compute rule effectiveness as findings per line of code.

    Args:
        hit_count: Number of findings for this rule.
        total_loc: Total lines of code scanned.

    Returns:
        Findings/LOC ratio (0.0 if no LOC).
    """
    if total_loc <= 0:
        return 0.0
    return hit_count / total_loc


def recommend_action(
    hit_count: int,
    effectiveness: float,
    fp_rate: float,
    severity: str,
) -> tuple[str, str]:
    """Recommend an action for a rule based on its impact metrics.

    Args:
        hit_count: Number of hits.
        effectiveness: Findings/LOC ratio.
        fp_rate: Estimated false positive rate.
        severity: Rule severity.

    Returns:
        Tuple of (recommendation, reason) where recommendation is
        one of "keep", "tune", "disable".
    """
    # Rules with no hits
    if hit_count == 0:
        return "keep", "No findings — rule is clean"

    # Very high FP rate
    if fp_rate > 0.50:
        return "disable", f"High estimated FP rate ({fp_rate:.0%})"

    # Critical rules always kept
    if severity == "CRITICAL":
        if fp_rate > 0.30:
            return "tune", "Critical rule with elevated FP rate — tune thresholds"
        return "keep", "Critical severity — always active"

    # Very noisy (> 10% of code flagged)
    if effectiveness > _RATIO_NOISE:
        return "disable", f"Too noisy — {effectiveness:.2%} of code flagged"

    # Moderately noisy with low severity
    if effectiveness > _RATIO_HIGH and severity in ("LOW", "INFO"):
        return "tune", f"High hit rate for {severity} severity — consider tuning"

    # Moderate FP rate
    if fp_rate > 0.30:
        return "tune", f"Moderate FP rate ({fp_rate:.0%}) — tune pattern"

    # High-impact rules
    if severity == "HIGH" and hit_count > 0:
        return "keep", f"High severity with {hit_count} finding(s)"

    return "keep", f"{hit_count} finding(s) at acceptable rates"


# ---------------------------------------------------------------------------
# Core analysis
# ---------------------------------------------------------------------------

def analyze_rule_impact(
    findings: list[dict[str, Any]],
    total_loc: int = 0,
) -> ImpactReport:
    """Analyze the impact of each rule across scan findings.

    Args:
        findings: List of finding dicts with rule_id, severity, file, line.
        total_loc: Total lines of code scanned (0 = not provided).

    Returns:
        ImpactReport with per-rule analysis.
    """
    # Group hits by rule_id
    rule_hits: dict[str, list[RuleHit]] = {}
    rule_severities: dict[str, str] = {}

    for f in findings:
        rule_id = f.get("rule_id", "UNKNOWN")
        hit = RuleHit(
            rule_id=rule_id,
            file=f.get("file", ""),
            line=f.get("line", 0),
            severity=f.get("severity", "INFO"),
            title=f.get("title", ""),
        )
        if rule_id not in rule_hits:
            rule_hits[rule_id] = []
        rule_hits[rule_id].append(hit)
        rule_severities[rule_id] = f.get("severity", "INFO")

    # Analyze each rule
    impacts: list[RuleImpact] = []
    total_findings = 0

    for rule_id, hits in sorted(rule_hits.items()):
        severity = rule_severities.get(rule_id, "INFO")
        hit_count = len(hits)
        total_findings += hit_count

        files = sorted(set(h.file for h in hits if h.file))
        file_count = len(files)

        eff = compute_effectiveness(hit_count, total_loc)
        fp = estimate_fp_rate(hits, severity)
        rec, reason = recommend_action(hit_count, eff, fp, severity)

        sev_weight = _SEVERITY_IMPACT.get(severity, 1)
        impact_score = hit_count * sev_weight * (1.0 - fp)

        impacts.append(RuleImpact(
            rule_id=rule_id,
            severity=severity,
            hit_count=hit_count,
            file_count=file_count,
            total_loc=total_loc,
            effectiveness=eff,
            fp_rate=fp,
            recommendation=rec,
            reason=reason,
            files_affected=files,
            impact_score=impact_score,
        ))

    # Sort by impact score descending
    impacts.sort(key=lambda x: x.impact_score, reverse=True)

    # Categorize
    to_tune = [r.rule_id for r in impacts if r.recommendation == "tune"]
    to_disable = [r.rule_id for r in impacts if r.recommendation == "disable"]
    to_keep = [r.rule_id for r in impacts if r.recommendation == "keep"]

    # Top noisy (high hit count, low severity)
    noisy = sorted(
        [r for r in impacts if r.hit_count > 0],
        key=lambda x: x.effectiveness,
        reverse=True,
    )
    top_noisy = [r.rule_id for r in noisy[:5]]

    # Top effective (high impact score)
    top_effective = [r.rule_id for r in impacts[:5] if r.hit_count > 0]

    return ImpactReport(
        rules=impacts,
        total_rules=len(impacts),
        total_findings=total_findings,
        total_loc=total_loc,
        rules_to_tune=to_tune,
        rules_to_disable=to_disable,
        rules_to_keep=to_keep,
        top_noisy=top_noisy,
        top_effective=top_effective,
    )


def analyze_rule_impact_from_scan(
    scan_data: dict[str, Any],
) -> ImpactReport:
    """Analyze rule impact from raw scan JSON data.

    Extracts findings and LOC from the scan data structure.

    Args:
        scan_data: Scan result dict from MCPSecurityScanner.

    Returns:
        ImpactReport.
    """
    findings: list[dict[str, Any]] = []
    total_loc = 0

    results = scan_data.get("results", [])
    for r in results:
        total_loc += r.get("lines_scanned", 0)
        for f in r.get("findings", []):
            findings.append(f)

    # Also check summary for LOC
    summary = scan_data.get("summary", {})
    if summary.get("lines_scanned") and total_loc == 0:
        total_loc = summary["lines_scanned"]

    return analyze_rule_impact(findings, total_loc)


def analyze_rule_impact_from_file(
    path: Path | str,
) -> ImpactReport:
    """Analyze rule impact from a saved scan report JSON file.

    Args:
        path: Path to the scan report JSON file.

    Returns:
        ImpactReport.
    """
    path = Path(path)
    data = json.loads(path.read_text(encoding="utf-8"))
    return analyze_rule_impact_from_scan(data)


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

def format_impact_text(report: ImpactReport) -> str:
    """Format a rule impact report as human-readable text.

    Args:
        report: ImpactReport to format.

    Returns:
        Formatted text string.
    """
    lines: list[str] = []
    lines.append("=" * 60)
    lines.append("MCP Security Audit — Rule Impact Analysis")
    lines.append("=" * 60)
    lines.append(f"Rules analyzed: {report.total_rules}")
    lines.append(f"Total findings: {report.total_findings}")
    if report.total_loc > 0:
        lines.append(f"Total LOC: {report.total_loc:,}")
    lines.append("")

    # Recommendations summary
    if report.rules_to_disable:
        lines.append(f"Rules to DISABLE ({len(report.rules_to_disable)}):")
        for rid in report.rules_to_disable:
            lines.append(f"  - {rid}")
        lines.append("")

    if report.rules_to_tune:
        lines.append(f"Rules to TUNE ({len(report.rules_to_tune)}):")
        for rid in report.rules_to_tune:
            lines.append(f"  - {rid}")
        lines.append("")

    lines.append(f"Rules to KEEP: {len(report.rules_to_keep)}")
    lines.append("")

    # Per-rule detail
    lines.append("Rule Details:")
    lines.append(f"{'Rule':<16s} {'Sev':<10s} {'Hits':>5s} {'Files':>5s} "
                 f"{'Eff':>8s} {'FP%':>6s} {'Rec':<10s}")
    lines.append("-" * 70)

    for r in report.rules:
        eff_str = f"{r.effectiveness:.4f}" if r.total_loc > 0 else "N/A"
        lines.append(
            f"{r.rule_id:<16s} {r.severity:<10s} {r.hit_count:>5d} "
            f"{r.file_count:>5d} {eff_str:>8s} {r.fp_rate:>5.0%} "
            f"{r.recommendation:<10s}"
        )
        if r.reason:
            lines.append(f"  -> {r.reason}")

    return "\n".join(lines)


def format_impact_json(report: ImpactReport) -> str:
    """Format a rule impact report as JSON.

    Args:
        report: ImpactReport to format.

    Returns:
        JSON string.
    """
    return json.dumps(report.to_dict(), indent=2)
