"""Security Trend Analyzer for MCP Security Audit.

Analyzes security scan results over time to identify trends,
regressions, and improvements in the security posture.

Features:
- Track scan results over time (snapshots)
- Detect regressions (new findings in previously clean areas)
- Calculate improvement rate
- Identify most common rule violations
- Generate trend report with charts (ASCII)
- Risk velocity: rate of new findings per scan

CLI: mcpaudit trend --add <scan.json> | --report | --regressions

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

from __future__ import annotations

import json
from collections import Counter
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional


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

@dataclass(frozen=True)
class ScanSnapshot:
    """Point-in-time scan result summary."""
    timestamp: str  # ISO format
    total_findings: int
    critical: int = 0
    high: int = 0
    medium: int = 0
    low: int = 0
    info: int = 0
    files_scanned: int = 0
    weighted_score: int = 0
    grade: str = "A+"
    rule_ids: tuple[str, ...] = ()
    label: str = ""

    @property
    def severity_counts(self) -> dict[str, int]:
        return {
            "CRITICAL": self.critical,
            "HIGH": self.high,
            "MEDIUM": self.medium,
            "LOW": self.low,
            "INFO": self.info,
        }

    def to_dict(self) -> dict[str, Any]:
        return {
            "timestamp": self.timestamp,
            "total_findings": self.total_findings,
            "critical": self.critical,
            "high": self.high,
            "medium": self.medium,
            "low": self.low,
            "info": self.info,
            "files_scanned": self.files_scanned,
            "weighted_score": self.weighted_score,
            "grade": self.grade,
            "rule_ids": list(self.rule_ids),
            "label": self.label,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ScanSnapshot:
        return cls(
            timestamp=data["timestamp"],
            total_findings=data["total_findings"],
            critical=data.get("critical", 0),
            high=data.get("high", 0),
            medium=data.get("medium", 0),
            low=data.get("low", 0),
            info=data.get("info", 0),
            files_scanned=data.get("files_scanned", 0),
            weighted_score=data.get("weighted_score", 0),
            grade=data.get("grade", "A+"),
            rule_ids=tuple(data.get("rule_ids", [])),
            label=data.get("label", ""),
        )


@dataclass
class TrendPoint:
    """A point in a trend series."""
    timestamp: str
    value: float
    label: str = ""


@dataclass
class RegressionEvent:
    """A detected regression between two snapshots."""
    timestamp: str
    new_rules: list[str]  # rules that appeared
    severity_increase: dict[str, int]  # severity -> count increase
    score_delta: int  # weighted score change
    description: str = ""


# ---------------------------------------------------------------------------
# Analyzer
# ---------------------------------------------------------------------------

class TrendAnalyzer:
    """Analyzes security trends over time."""

    def __init__(self) -> None:
        self._snapshots: list[ScanSnapshot] = []

    @property
    def snapshots(self) -> list[ScanSnapshot]:
        return list(self._snapshots)

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

    def add_snapshot(self, snapshot: ScanSnapshot) -> None:
        """Add a snapshot, maintaining chronological order."""
        self._snapshots.append(snapshot)
        self._snapshots.sort(key=lambda s: s.timestamp)

    def latest(self) -> Optional[ScanSnapshot]:
        return self._snapshots[-1] if self._snapshots else None

    def earliest(self) -> Optional[ScanSnapshot]:
        return self._snapshots[0] if self._snapshots else None

    # -- Trends --

    def findings_trend(self) -> list[TrendPoint]:
        """Total findings over time."""
        return [
            TrendPoint(
                timestamp=s.timestamp,
                value=float(s.total_findings),
                label=s.label,
            )
            for s in self._snapshots
        ]

    def score_trend(self) -> list[TrendPoint]:
        """Weighted score over time."""
        return [
            TrendPoint(
                timestamp=s.timestamp,
                value=float(s.weighted_score),
                label=s.label,
            )
            for s in self._snapshots
        ]

    def grade_trend(self) -> list[TrendPoint]:
        """Grade mapped to numeric (A+=6, A=5, B=4, C=3, D=2, F=1)."""
        grade_map = {"A+": 6, "A": 5, "B": 4, "C": 3, "D": 2, "F": 1}
        return [
            TrendPoint(
                timestamp=s.timestamp,
                value=float(grade_map.get(s.grade, 0)),
                label=s.grade,
            )
            for s in self._snapshots
        ]

    def severity_trend(self, severity: str) -> list[TrendPoint]:
        """Trend for a specific severity level."""
        return [
            TrendPoint(
                timestamp=s.timestamp,
                value=float(s.severity_counts.get(severity, 0)),
            )
            for s in self._snapshots
        ]

    # -- Analysis --

    def improvement_rate(self) -> Optional[float]:
        """Rate of improvement: negative = improving (less findings).
        None if fewer than 2 snapshots."""
        if len(self._snapshots) < 2:
            return None
        first = self._snapshots[0].total_findings
        last = self._snapshots[-1].total_findings
        if first == 0:
            return 0.0
        return (last - first) / first

    def risk_velocity(self) -> Optional[float]:
        """Average new findings per scan. None if < 2 snapshots."""
        if len(self._snapshots) < 2:
            return None
        deltas = []
        for i in range(1, len(self._snapshots)):
            delta = self._snapshots[i].total_findings - self._snapshots[i - 1].total_findings
            deltas.append(max(0, delta))  # only count increases
        return sum(deltas) / len(deltas) if deltas else 0.0

    def most_common_rules(self, top_n: int = 10) -> list[tuple[str, int]]:
        """Most frequently violated rules across all snapshots."""
        counter: Counter[str] = Counter()
        for snapshot in self._snapshots:
            counter.update(snapshot.rule_ids)
        return counter.most_common(top_n)

    def detect_regressions(self) -> list[RegressionEvent]:
        """Detect regressions between consecutive snapshots."""
        regressions = []
        for i in range(1, len(self._snapshots)):
            prev = self._snapshots[i - 1]
            curr = self._snapshots[i]

            prev_rules = set(prev.rule_ids)
            curr_rules = set(curr.rule_ids)
            new_rules = sorted(curr_rules - prev_rules)

            sev_increase = {}
            for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
                delta = curr.severity_counts.get(sev, 0) - prev.severity_counts.get(sev, 0)
                if delta > 0:
                    sev_increase[sev] = delta

            score_delta = curr.weighted_score - prev.weighted_score

            if new_rules or sev_increase or score_delta > 0:
                desc_parts = []
                if new_rules:
                    desc_parts.append(f"{len(new_rules)} new rules")
                if sev_increase:
                    desc_parts.append(
                        ", ".join(f"+{v} {k}" for k, v in sev_increase.items())
                    )
                regressions.append(RegressionEvent(
                    timestamp=curr.timestamp,
                    new_rules=new_rules,
                    severity_increase=sev_increase,
                    score_delta=score_delta,
                    description="; ".join(desc_parts) if desc_parts else "",
                ))
        return regressions

    def overall_direction(self) -> str:
        """IMPROVING, DEGRADING, or STABLE."""
        rate = self.improvement_rate()
        if rate is None:
            return "UNKNOWN"
        if rate < -0.1:
            return "IMPROVING"
        if rate > 0.1:
            return "DEGRADING"
        return "STABLE"

    # -- Persistence --

    def to_dict(self) -> dict[str, Any]:
        return {
            "version": "1.0",
            "snapshots": [s.to_dict() for s in self._snapshots],
        }

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

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> TrendAnalyzer:
        analyzer = cls()
        for s in data.get("snapshots", []):
            analyzer._snapshots.append(ScanSnapshot.from_dict(s))
        return analyzer

    @classmethod
    def from_json(cls, json_str: str) -> TrendAnalyzer:
        return cls.from_dict(json.loads(json_str))

    # -- Report --

    def ascii_chart(self, points: list[TrendPoint], width: int = 40, height: int = 10) -> str:
        """Generate an ASCII bar chart for trend points."""
        if not points:
            return "(no data)"
        values = [p.value for p in points]
        max_val = max(values) if values else 1
        if max_val == 0:
            max_val = 1

        lines = []
        for row in range(height, 0, -1):
            threshold = max_val * row / height
            bar = ""
            for val in values:
                bar += "#" if val >= threshold else " "
            label = f"{threshold:6.0f} |"
            lines.append(f"{label}{bar}")
        lines.append(f"{'':>7}+{''.join('-' for _ in values)}")
        return "\n".join(lines)

    def markdown_report(self) -> str:
        """Generate a Markdown trend report."""
        lines = ["# Security Trend Report", ""]

        if not self._snapshots:
            lines.append("No scan data available.")
            return "\n".join(lines)

        # Summary
        lines.append("## Summary")
        lines.append(f"- **Scans analyzed**: {self.count}")
        lines.append(f"- **Direction**: {self.overall_direction()}")
        rate = self.improvement_rate()
        if rate is not None:
            lines.append(f"- **Change rate**: {rate:+.1%}")
        velocity = self.risk_velocity()
        if velocity is not None:
            lines.append(f"- **Risk velocity**: {velocity:.1f} new findings/scan")

        latest = self.latest()
        if latest:
            lines.append(f"- **Latest grade**: {latest.grade}")
            lines.append(f"- **Latest findings**: {latest.total_findings}")
        lines.append("")

        # Trend table
        lines.append("## Scan History")
        lines.append("| Date | Findings | Score | Grade | C | H | M | L |")
        lines.append("|------|----------|-------|-------|---|---|---|---|")
        for s in self._snapshots:
            date = s.timestamp[:10] if len(s.timestamp) >= 10 else s.timestamp
            lines.append(
                f"| {date} | {s.total_findings} | {s.weighted_score} "
                f"| {s.grade} | {s.critical} | {s.high} | {s.medium} | {s.low} |"
            )
        lines.append("")

        # Regressions
        regs = self.detect_regressions()
        if regs:
            lines.append("## Regressions Detected")
            for r in regs:
                lines.append(f"- **{r.timestamp[:10]}**: {r.description} (score delta: +{r.score_delta})")
            lines.append("")

        # Most common rules
        common = self.most_common_rules(5)
        if common:
            lines.append("## Most Common Rules")
            for rule_id, count in common:
                lines.append(f"- `{rule_id}`: {count} occurrences")
            lines.append("")

        # ASCII chart
        findings = self.findings_trend()
        if len(findings) >= 2:
            lines.append("## Findings Trend")
            lines.append("```")
            lines.append(self.ascii_chart(findings))
            lines.append("```")
            lines.append("")

        return "\n".join(lines)
