"""Scan Comparison Report for MCP Security Audit.

Compares before/after scans with detailed metrics:
- New, resolved, persistent findings
- Improvement score (delta trust score)
- Regression alerts (reappearing findings)
- HTML diff with visual color coding
- Timeline progression from historical scans

CLI: mcpaudit compare scan1.json scan2.json --output diff.html

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

from __future__ import annotations

import hashlib
import html as html_mod
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


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

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

_SEVERITY_WEIGHT: dict[str, int] = {
    "CRITICAL": 10,
    "HIGH": 5,
    "MEDIUM": 2,
    "LOW": 1,
    "INFO": 0,
}

_SEVERITY_COLORS: dict[str, str] = {
    "CRITICAL": "#dc2626",
    "HIGH": "#f97316",
    "MEDIUM": "#eab308",
    "LOW": "#22c55e",
    "INFO": "#6b7280",
}


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

@dataclass
class ComparedFinding:
    """A finding in the comparison result."""

    status: str  # "new", "resolved", "persistent", "regression"
    rule_id: str = ""
    severity: str = ""
    title: str = ""
    description: str = ""
    file: str = ""
    line: int = 0
    code_snippet: str = ""
    previous_severity: str = ""

    def to_dict(self) -> dict[str, Any]:
        result = {
            "status": self.status,
            "rule_id": self.rule_id,
            "severity": self.severity,
            "title": self.title,
            "file": self.file,
            "line": self.line,
        }
        if self.description:
            result["description"] = self.description
        if self.code_snippet:
            result["code_snippet"] = self.code_snippet
        if self.previous_severity:
            result["previous_severity"] = self.previous_severity
        return result


@dataclass
class SeverityDelta:
    """Change in severity counts between two scans."""

    before: dict[str, int] = field(default_factory=dict)
    after: dict[str, int] = field(default_factory=dict)

    @property
    def delta(self) -> dict[str, int]:
        all_sevs = set(list(self.before.keys()) + list(self.after.keys()))
        return {
            s: self.after.get(s, 0) - self.before.get(s, 0)
            for s in all_sevs
        }

    def to_dict(self) -> dict[str, Any]:
        return {
            "before": dict(self.before),
            "after": dict(self.after),
            "delta": self.delta,
        }


@dataclass
class ComparisonReport:
    """Full comparison report between two scans."""

    before_label: str = ""
    after_label: str = ""
    before_score: int = 0
    after_score: int = 0
    before_grade: str = ""
    after_grade: str = ""
    new_findings: list[ComparedFinding] = field(default_factory=list)
    resolved_findings: list[ComparedFinding] = field(default_factory=list)
    persistent_findings: list[ComparedFinding] = field(default_factory=list)
    regression_findings: list[ComparedFinding] = field(default_factory=list)
    severity_delta: SeverityDelta = field(default_factory=SeverityDelta)
    timestamp: str = ""

    @property
    def improvement_score(self) -> int:
        """Delta trust score (positive = improvement)."""
        return self.after_score - self.before_score

    @property
    def total_new(self) -> int:
        return len(self.new_findings)

    @property
    def total_resolved(self) -> int:
        return len(self.resolved_findings)

    @property
    def total_persistent(self) -> int:
        return len(self.persistent_findings)

    @property
    def total_regressions(self) -> int:
        return len(self.regression_findings)

    @property
    def improved(self) -> bool:
        return self.improvement_score > 0

    @property
    def degraded(self) -> bool:
        return self.improvement_score < 0

    @property
    def has_regressions(self) -> bool:
        return self.total_regressions > 0

    @property
    def exit_code(self) -> int:
        """CI exit code: 0=ok, 1=new high, 2=new critical."""
        for f in self.new_findings + self.regression_findings:
            if f.severity == "CRITICAL":
                return 2
        for f in self.new_findings + self.regression_findings:
            if f.severity == "HIGH":
                return 1
        return 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "before_label": self.before_label,
            "after_label": self.after_label,
            "before_score": self.before_score,
            "after_score": self.after_score,
            "before_grade": self.before_grade,
            "after_grade": self.after_grade,
            "improvement_score": self.improvement_score,
            "improved": self.improved,
            "degraded": self.degraded,
            "has_regressions": self.has_regressions,
            "exit_code": self.exit_code,
            "summary": {
                "new": self.total_new,
                "resolved": self.total_resolved,
                "persistent": self.total_persistent,
                "regressions": self.total_regressions,
            },
            "severity_delta": self.severity_delta.to_dict(),
            "new_findings": [f.to_dict() for f in self.new_findings],
            "resolved_findings": [
                f.to_dict() for f in self.resolved_findings
            ],
            "persistent_findings": [
                f.to_dict() for f in self.persistent_findings
            ],
            "regression_findings": [
                f.to_dict() for f in self.regression_findings
            ],
            "timestamp": self.timestamp,
        }


@dataclass
class TimelinePoint:
    """A single point in the scan timeline."""

    label: str = ""
    score: int = 0
    grade: str = ""
    total_findings: int = 0
    severity_counts: dict[str, int] = field(default_factory=dict)
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "label": self.label,
            "score": self.score,
            "grade": self.grade,
            "total_findings": self.total_findings,
            "severity_counts": dict(self.severity_counts),
            "timestamp": self.timestamp,
        }


@dataclass
class TimelineReport:
    """Timeline of scan progression."""

    points: list[TimelinePoint] = field(default_factory=list)

    @property
    def trend(self) -> str:
        """Overall trend: improving, degrading, or stable."""
        if len(self.points) < 2:
            return "stable"
        first = self.points[0].score
        last = self.points[-1].score
        if last > first + 5:
            return "improving"
        if last < first - 5:
            return "degrading"
        return "stable"

    def to_dict(self) -> dict[str, Any]:
        return {
            "trend": self.trend,
            "points": [p.to_dict() for p in self.points],
        }


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _finding_key(finding: dict[str, Any]) -> str:
    """Stable fingerprint for matching findings across scans."""
    rule_id = finding.get("rule_id", "")
    file_path = finding.get("file", "")
    file_base = Path(file_path).name if file_path else ""
    title = finding.get("title", "")
    raw = f"{rule_id}|{file_base}|{title}"
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]


def _extract_findings(data: dict[str, Any]) -> list[dict[str, Any]]:
    """Extract finding dicts from various scan formats."""
    findings: list[dict[str, Any]] = []
    if "results" in data:
        for r in data["results"]:
            if isinstance(r, dict) and "findings" in r:
                findings.extend(r["findings"])
        return findings
    if "findings" in data and isinstance(data["findings"], list):
        return data["findings"]
    return findings


def _count_severities(
    findings: list[dict[str, Any]],
) -> dict[str, int]:
    """Count findings by severity."""
    counts: dict[str, int] = {}
    for f in findings:
        sev = f.get("severity", "UNKNOWN")
        counts[sev] = counts.get(sev, 0) + 1
    return counts


def _estimate_score(findings: list[dict[str, Any]]) -> int:
    """Estimate trust score from findings (100 = clean)."""
    if not findings:
        return 100
    penalty = sum(
        _SEVERITY_WEIGHT.get(f.get("severity", ""), 0)
        for f in findings
    )
    return max(0, 100 - penalty)


def _extract_score(data: dict[str, Any]) -> tuple[int, str]:
    """Extract trust score and grade from scan data."""
    summary = data.get("summary", {})
    trust = summary.get("trust_score", {})
    score = 0
    grade = ""
    if isinstance(trust, dict):
        score = trust.get("score", 0)
        grade = trust.get("grade", "")
    elif isinstance(trust, (int, float)):
        score = int(trust)
    if score == 0:
        findings = _extract_findings(data)
        score = _estimate_score(findings)
    grade = grade or summary.get("overall_grade", "")
    return score, grade


def _to_compared(
    finding: dict[str, Any],
    status: str,
) -> ComparedFinding:
    """Convert a raw finding dict to ComparedFinding."""
    return ComparedFinding(
        status=status,
        rule_id=finding.get("rule_id", ""),
        severity=finding.get("severity", "INFO"),
        title=finding.get("title", ""),
        description=finding.get("description", ""),
        file=finding.get("file", ""),
        line=finding.get("line", 0),
        code_snippet=finding.get("code_snippet", ""),
    )


# ---------------------------------------------------------------------------
# Core comparison
# ---------------------------------------------------------------------------

def compare_scans(
    before: dict[str, Any],
    after: dict[str, Any],
    before_label: str = "before",
    after_label: str = "after",
) -> ComparisonReport:
    """Compare two scan results and produce a detailed report.

    Args:
        before: 'Before' scan data dict.
        after: 'After' scan data dict.
        before_label: Label for the before scan.
        after_label: Label for the after scan.

    Returns:
        ComparisonReport with categorized findings.
    """
    report = ComparisonReport(
        before_label=before_label,
        after_label=after_label,
        timestamp=datetime.now(timezone.utc).strftime(
            "%Y-%m-%d %H:%M UTC"
        ),
    )

    before_findings = _extract_findings(before)
    after_findings = _extract_findings(after)

    # Build keyed indices
    before_by_key: dict[str, dict[str, Any]] = {}
    for f in before_findings:
        before_by_key[_finding_key(f)] = f
    after_by_key: dict[str, dict[str, Any]] = {}
    for f in after_findings:
        after_by_key[_finding_key(f)] = f

    before_keys = set(before_by_key.keys())
    after_keys = set(after_by_key.keys())

    # New findings
    for key in sorted(after_keys - before_keys):
        report.new_findings.append(
            _to_compared(after_by_key[key], "new")
        )

    # Resolved findings
    for key in sorted(before_keys - after_keys):
        report.resolved_findings.append(
            _to_compared(before_by_key[key], "resolved")
        )

    # Persistent findings
    for key in sorted(before_keys & after_keys):
        report.persistent_findings.append(
            _to_compared(after_by_key[key], "persistent")
        )

    # Scores
    b_score, b_grade = _extract_score(before)
    a_score, a_grade = _extract_score(after)
    report.before_score = b_score
    report.after_score = a_score
    report.before_grade = b_grade
    report.after_grade = a_grade

    # Severity delta
    report.severity_delta = SeverityDelta(
        before=_count_severities(before_findings),
        after=_count_severities(after_findings),
    )

    return report


def compare_scan_files(
    before_path: str | Path,
    after_path: str | Path,
) -> ComparisonReport:
    """Compare two scan JSON files.

    Args:
        before_path: Path to 'before' scan JSON.
        after_path: Path to 'after' scan JSON.

    Returns:
        ComparisonReport.
    """
    bp = Path(before_path)
    ap = Path(after_path)

    before_data = json.loads(bp.read_text(encoding="utf-8"))
    after_data = json.loads(ap.read_text(encoding="utf-8"))

    return compare_scans(
        before_data, after_data,
        before_label=str(bp), after_label=str(ap),
    )


def detect_regressions_from_history(
    scans: list[dict[str, Any]],
) -> list[ComparedFinding]:
    """Detect regression findings across a list of historical scans.

    A regression is a finding that was resolved in a previous scan
    but reappears in a later scan.

    Args:
        scans: Ordered list of scan data dicts (oldest first).

    Returns:
        List of regression ComparedFinding objects.
    """
    if len(scans) < 2:
        return []

    regressions: list[ComparedFinding] = []
    ever_resolved: set[str] = set()

    for i in range(1, len(scans)):
        prev_findings = _extract_findings(scans[i - 1])
        curr_findings = _extract_findings(scans[i])

        prev_keys = {_finding_key(f) for f in prev_findings}
        curr_by_key = {_finding_key(f): f for f in curr_findings}
        curr_keys = set(curr_by_key.keys())

        # Track resolved
        newly_resolved = prev_keys - curr_keys
        ever_resolved.update(newly_resolved)

        # Check for regressions
        for key in sorted(curr_keys & ever_resolved):
            cf = _to_compared(curr_by_key[key], "regression")
            regressions.append(cf)

    return regressions


# ---------------------------------------------------------------------------
# Timeline
# ---------------------------------------------------------------------------

def build_timeline(
    scans: list[dict[str, Any]],
    labels: list[str] | None = None,
) -> TimelineReport:
    """Build a timeline from a series of scan results.

    Args:
        scans: Ordered list of scan data dicts.
        labels: Optional labels for each scan.

    Returns:
        TimelineReport with progression data.
    """
    timeline = TimelineReport()
    scan_labels = labels or [f"scan-{i}" for i in range(len(scans))]

    for i, scan_data in enumerate(scans):
        findings = _extract_findings(scan_data)
        score, grade = _extract_score(scan_data)
        label = scan_labels[i] if i < len(scan_labels) else f"scan-{i}"

        timeline.points.append(TimelinePoint(
            label=label,
            score=score,
            grade=grade,
            total_findings=len(findings),
            severity_counts=_count_severities(findings),
        ))

    return timeline


# ---------------------------------------------------------------------------
# HTML report generation
# ---------------------------------------------------------------------------

def _esc(text: str) -> str:
    """HTML-escape text."""
    return html_mod.escape(str(text), quote=True)


def _comparison_css() -> str:
    """Generate CSS for the comparison HTML report."""
    return """
    :root {
      --bg: #0f172a; --bg2: #1e293b; --bg3: #334155;
      --text: #e2e8f0; --text2: #94a3b8; --border: #334155;
      --accent: #3b82f6; --green: #22c55e; --red: #dc2626;
      --orange: #f97316; --yellow: #eab308;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
                   Roboto, sans-serif;
      background: var(--bg); color: var(--text); line-height: 1.6;
    }
    .container { max-width: 1100px; margin: 0 auto; padding: 24px; }
    h1 { font-size: 22px; }
    h2 { font-size: 18px; margin: 20px 0 10px; color: var(--accent); }
    .subtitle { color: var(--text2); font-size: 13px; }
    .score-panel {
      display: flex; gap: 24px; margin: 16px 0; align-items: center;
      flex-wrap: wrap;
    }
    .score-box {
      background: var(--bg2); border: 1px solid var(--border);
      border-radius: 8px; padding: 16px 24px; text-align: center;
    }
    .score-box .big {
      font-size: 32px; font-weight: 700;
    }
    .score-box .lbl {
      font-size: 11px; color: var(--text2); text-transform: uppercase;
    }
    .arrow { font-size: 28px; color: var(--text2); }
    .delta-positive { color: var(--green); }
    .delta-negative { color: var(--red); }
    .delta-neutral { color: var(--text2); }
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
      gap: 12px; margin: 12px 0;
    }
    .stat-card {
      background: var(--bg2); border: 1px solid var(--border);
      border-radius: 8px; padding: 12px; text-align: center;
    }
    .stat-card .val { font-size: 24px; font-weight: 700; }
    .stat-card .lbl {
      font-size: 11px; color: var(--text2); text-transform: uppercase;
    }
    .finding-list { margin: 8px 0; }
    .finding-row {
      display: flex; align-items: center; gap: 10px;
      padding: 8px 12px; border-bottom: 1px solid var(--border);
      font-size: 14px;
    }
    .finding-row:hover { background: var(--bg3); }
    .badge {
      padding: 2px 8px; border-radius: 10px;
      font-size: 10px; font-weight: 700; color: #fff;
    }
    .badge-new { background: var(--red); }
    .badge-resolved { background: var(--green); }
    .badge-persistent { background: var(--text2); }
    .badge-regression { background: var(--orange); }
    .sev { font-size: 11px; font-weight: 600; }
    .rule { color: var(--text2); font-family: monospace; font-size: 12px; }
    .ftitle { flex: 1; }
    .floc { color: var(--text2); font-size: 12px; }
    .timeline-bar {
      display: flex; align-items: flex-end; gap: 4px;
      height: 100px; margin: 12px 0;
    }
    .tl-col {
      flex: 1; display: flex; flex-direction: column;
      align-items: center; gap: 4px;
    }
    .tl-bar {
      width: 100%; border-radius: 4px 4px 0 0;
      background: var(--accent); min-height: 4px;
    }
    .tl-label { font-size: 10px; color: var(--text2); }
    .footer {
      text-align: center; padding: 20px; margin-top: 24px;
      color: var(--text2); font-size: 11px;
      border-top: 1px solid var(--border);
    }
    """


def _render_score_panel(report: ComparisonReport) -> str:
    """Render the score comparison panel."""
    delta = report.improvement_score
    if delta > 0:
        delta_cls = "delta-positive"
        delta_txt = f"+{delta}"
    elif delta < 0:
        delta_cls = "delta-negative"
        delta_txt = str(delta)
    else:
        delta_cls = "delta-neutral"
        delta_txt = "0"

    return (
        '<div class="score-panel">'
        '<div class="score-box">'
        f'<div class="big">{report.before_score}</div>'
        f'<div class="lbl">{_esc(report.before_label)}</div></div>'
        '<div class="arrow">&rarr;</div>'
        '<div class="score-box">'
        f'<div class="big">{report.after_score}</div>'
        f'<div class="lbl">{_esc(report.after_label)}</div></div>'
        '<div class="score-box">'
        f'<div class="big {delta_cls}">{delta_txt}</div>'
        '<div class="lbl">Delta</div></div>'
        "</div>"
    )


def _render_stats(report: ComparisonReport) -> str:
    """Render summary statistics cards."""
    stats = [
        (str(report.total_new), "New", "var(--red)"),
        (str(report.total_resolved), "Resolved", "var(--green)"),
        (str(report.total_persistent), "Persistent", "var(--text2)"),
        (str(report.total_regressions), "Regressions", "var(--orange)"),
    ]
    cards = []
    for val, lbl, color in stats:
        cards.append(
            f'<div class="stat-card">'
            f'<div class="val" style="color:{color}">{val}</div>'
            f'<div class="lbl">{lbl}</div></div>'
        )
    return '<div class="stats-grid">' + "".join(cards) + "</div>"


def _render_finding_row(
    finding: ComparedFinding,
    badge_class: str,
    badge_text: str,
) -> str:
    """Render a single finding row."""
    sev = finding.severity
    color = _SEVERITY_COLORS.get(sev, "#6b7280")
    loc = f"{finding.file}:{finding.line}" if finding.file else ""
    return (
        '<div class="finding-row">'
        f'<span class="badge {badge_class}">{badge_text}</span>'
        f'<span class="sev" style="color:{color}">{_esc(sev)}</span>'
        f'<span class="rule">{_esc(finding.rule_id)}</span>'
        f'<span class="ftitle">{_esc(finding.title)}</span>'
        f'<span class="floc">{_esc(loc)}</span>'
        "</div>"
    )


def _render_findings_section(
    title: str,
    findings: list[ComparedFinding],
    badge_class: str,
    badge_text: str,
) -> str:
    """Render a section of findings."""
    if not findings:
        return ""
    rows = "".join(
        _render_finding_row(f, badge_class, badge_text)
        for f in sorted(
            findings,
            key=lambda f: _SEVERITY_ORDER.get(f.severity, 99),
        )
    )
    return (
        f'<h2>{_esc(title)} ({len(findings)})</h2>'
        f'<div class="finding-list">{rows}</div>'
    )


def _render_timeline(timeline: TimelineReport) -> str:
    """Render timeline visualization."""
    if not timeline.points:
        return ""
    max_score = max(
        (p.score for p in timeline.points), default=100
    ) or 100

    cols = []
    for p in timeline.points:
        h = max(4, int(p.score / max_score * 80))
        cols.append(
            f'<div class="tl-col">'
            f'<div class="tl-bar" style="height:{h}px" '
            f'title="{_esc(p.label)}: {p.score}"></div>'
            f'<div class="tl-label">{_esc(p.label)}</div></div>'
        )
    return (
        '<h2>Timeline</h2>'
        '<div class="timeline-bar">' + "".join(cols) + "</div>"
    )


def generate_comparison_html(
    report: ComparisonReport,
    timeline: TimelineReport | None = None,
) -> str:
    """Generate a self-contained HTML comparison report.

    Args:
        report: ComparisonReport from compare_scans().
        timeline: Optional TimelineReport for progression view.

    Returns:
        Complete HTML string.
    """
    ts = report.timestamp or datetime.now(
        timezone.utc
    ).strftime("%Y-%m-%d %H:%M UTC")

    sections = [
        f"<h1>MCP Security — Scan Comparison</h1>",
        f'<p class="subtitle">Generated {_esc(ts)}</p>',
        _render_score_panel(report),
        _render_stats(report),
    ]

    # Finding sections
    sections.append(_render_findings_section(
        "New Findings", report.new_findings,
        "badge-new", "NEW",
    ))
    sections.append(_render_findings_section(
        "Regression Findings", report.regression_findings,
        "badge-regression", "REGR",
    ))
    sections.append(_render_findings_section(
        "Resolved Findings", report.resolved_findings,
        "badge-resolved", "FIXED",
    ))
    sections.append(_render_findings_section(
        "Persistent Findings", report.persistent_findings,
        "badge-persistent", "SAME",
    ))

    if timeline:
        sections.append(_render_timeline(timeline))

    sections.append(
        '<div class="footer">Generated by mcpaudit '
        "— MCP Security Audit Scanner</div>"
    )

    return (
        '<!DOCTYPE html>\n<html lang="en">\n<head>\n'
        '<meta charset="UTF-8">\n'
        '<meta name="viewport" '
        'content="width=device-width, initial-scale=1.0">\n'
        "<title>MCP Scan Comparison</title>\n"
        f"<style>{_comparison_css()}</style>\n"
        "</head>\n<body>\n"
        '<div class="container">'
        + "".join(sections)
        + "</div>\n</body>\n</html>"
    )


# ---------------------------------------------------------------------------
# Text / JSON formatting
# ---------------------------------------------------------------------------

def format_comparison_text(report: ComparisonReport) -> str:
    """Format comparison report as human-readable text.

    Args:
        report: ComparisonReport.

    Returns:
        Text string.
    """
    lines = []
    lines.append("=" * 55)
    lines.append("  MCP Security — Scan Comparison")
    lines.append("=" * 55)
    lines.append(
        f"  Before: {report.before_label} "
        f"(score: {report.before_score})"
    )
    lines.append(
        f"  After:  {report.after_label} "
        f"(score: {report.after_score})"
    )
    delta = report.improvement_score
    sign = "+" if delta >= 0 else ""
    lines.append(f"  Delta:  {sign}{delta}")
    lines.append("")
    lines.append(f"  New:         {report.total_new}")
    lines.append(f"  Resolved:    {report.total_resolved}")
    lines.append(f"  Persistent:  {report.total_persistent}")
    lines.append(f"  Regressions: {report.total_regressions}")
    lines.append("=" * 55)

    if report.new_findings:
        lines.append(f"\n  [+] NEW ({report.total_new}):")
        for f in report.new_findings:
            lines.append(
                f"    + [{f.severity}] {f.rule_id}: {f.title}"
            )

    if report.regression_findings:
        lines.append(f"\n  [!] REGRESSIONS ({report.total_regressions}):")
        for f in report.regression_findings:
            lines.append(
                f"    ! [{f.severity}] {f.rule_id}: {f.title}"
            )

    if report.resolved_findings:
        lines.append(f"\n  [-] RESOLVED ({report.total_resolved}):")
        for f in report.resolved_findings:
            lines.append(
                f"    - [{f.severity}] {f.rule_id}: {f.title}"
            )

    lines.append("")
    return "\n".join(lines)


def format_comparison_json(report: ComparisonReport) -> str:
    """Format comparison report as JSON.

    Args:
        report: ComparisonReport.

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