"""Report Aggregator for MCP Security Audit.

Merges multiple scan reports into a consolidated view with deduplication,
timeline analysis, trend detection, and executive summary generation.

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


# ---------------------------------------------------------------------------
# Severity ordering for comparisons
# ---------------------------------------------------------------------------

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

_SEVERITY_WEIGHT: dict[str, int] = {
    "CRITICAL": 25,
    "HIGH": 15,
    "MEDIUM": 8,
    "LOW": 3,
    "INFO": 1,
}


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

@dataclass
class AggregatedFinding:
    """A deduplicated finding that may appear in multiple reports."""

    rule_id: str
    severity: str
    title: str
    description: str
    file: str
    line: int
    first_seen: str = ""
    last_seen: str = ""
    occurrence_count: int = 1
    code_snippet: str = ""
    recommendation: str = ""
    reports: list[str] = field(default_factory=list)

    @property
    def dedup_key(self) -> tuple[str, str, int]:
        """Unique key for deduplication: (rule_id, file, line)."""
        return (self.rule_id, self.file, self.line)

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "severity": self.severity,
            "title": self.title,
            "description": self.description,
            "file": self.file,
            "line": self.line,
            "first_seen": self.first_seen,
            "last_seen": self.last_seen,
            "occurrence_count": self.occurrence_count,
            "code_snippet": self.code_snippet,
            "recommendation": self.recommendation,
            "reports": self.reports,
        }


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

    date: str
    total_findings: int
    severity_counts: dict[str, int] = field(default_factory=dict)
    report_name: str = ""
    score: int = 0

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


@dataclass
class TrendAnalysis:
    """Trend analysis across aggregated reports."""

    direction: str  # "improving", "degrading", "stable"
    finding_delta: int = 0
    critical_delta: int = 0
    new_findings: int = 0
    resolved_findings: int = 0
    recurring_findings: int = 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "direction": self.direction,
            "finding_delta": self.finding_delta,
            "critical_delta": self.critical_delta,
            "new_findings": self.new_findings,
            "resolved_findings": self.resolved_findings,
            "recurring_findings": self.recurring_findings,
        }


@dataclass
class AggregatedReport:
    """Consolidated report from multiple scan reports."""

    findings: list[AggregatedFinding] = field(default_factory=list)
    timeline: list[TimelineEntry] = field(default_factory=list)
    trend: TrendAnalysis | None = None
    executive_summary: str = ""
    total_reports: int = 0
    total_unique_findings: int = 0
    severity_counts: dict[str, int] = field(default_factory=dict)
    report_names: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {
            "total_reports": self.total_reports,
            "total_unique_findings": self.total_unique_findings,
            "severity_counts": self.severity_counts,
            "report_names": self.report_names,
            "executive_summary": self.executive_summary,
            "findings": [f.to_dict() for f in self.findings],
            "timeline": [t.to_dict() for t in self.timeline],
        }
        if self.trend:
            result["trend"] = self.trend.to_dict()
        return result


# ---------------------------------------------------------------------------
# Core merging
# ---------------------------------------------------------------------------

def _extract_findings(
    report_data: dict[str, Any],
    report_name: str,
    report_date: str,
) -> list[AggregatedFinding]:
    """Extract findings from a single report JSON.

    Args:
        report_data: Parsed report JSON dict.
        report_name: Name/label for this report.
        report_date: ISO date string for this report.

    Returns:
        List of AggregatedFinding objects.
    """
    findings: list[AggregatedFinding] = []

    # Support both "results" (scan summary) and "findings" (flat list)
    results = report_data.get("results", [])
    for result in results:
        for f in result.get("findings", []):
            findings.append(AggregatedFinding(
                rule_id=f.get("rule_id", ""),
                severity=f.get("severity", "INFO"),
                title=f.get("title", ""),
                description=f.get("description", ""),
                file=f.get("file", ""),
                line=f.get("line", 0),
                first_seen=report_date,
                last_seen=report_date,
                code_snippet=f.get("code_snippet", ""),
                recommendation=f.get("recommendation", ""),
                reports=[report_name],
            ))

    # Flat findings list
    flat_findings = report_data.get("findings", [])
    if isinstance(flat_findings, list):
        for f in flat_findings:
            if isinstance(f, dict):
                findings.append(AggregatedFinding(
                    rule_id=f.get("rule_id", ""),
                    severity=f.get("severity", "INFO"),
                    title=f.get("title", ""),
                    description=f.get("description", ""),
                    file=f.get("file", ""),
                    line=f.get("line", 0),
                    first_seen=report_date,
                    last_seen=report_date,
                    code_snippet=f.get("code_snippet", ""),
                    recommendation=f.get("recommendation", ""),
                    reports=[report_name],
                ))

    return findings


def _dedup_findings(
    findings_list: list[AggregatedFinding],
) -> list[AggregatedFinding]:
    """Deduplicate findings by (rule_id, file, line).

    Merges occurrence counts, first/last seen dates, and report lists.

    Args:
        findings_list: All findings from all reports.

    Returns:
        Deduplicated list sorted by severity then rule_id.
    """
    seen: dict[tuple[str, str, int], AggregatedFinding] = {}

    for f in findings_list:
        key = f.dedup_key
        if key in seen:
            existing = seen[key]
            existing.occurrence_count += 1
            if f.first_seen and (
                not existing.first_seen or f.first_seen < existing.first_seen
            ):
                existing.first_seen = f.first_seen
            if f.last_seen and (
                not existing.last_seen or f.last_seen > existing.last_seen
            ):
                existing.last_seen = f.last_seen
            for rn in f.reports:
                if rn not in existing.reports:
                    existing.reports.append(rn)
        else:
            seen[key] = AggregatedFinding(
                rule_id=f.rule_id,
                severity=f.severity,
                title=f.title,
                description=f.description,
                file=f.file,
                line=f.line,
                first_seen=f.first_seen,
                last_seen=f.last_seen,
                occurrence_count=f.occurrence_count,
                code_snippet=f.code_snippet,
                recommendation=f.recommendation,
                reports=list(f.reports),
            )

    # Sort by severity order, then rule_id
    result = list(seen.values())
    result.sort(key=lambda x: (
        _SEVERITY_ORDER.get(x.severity, 99),
        x.rule_id,
        x.file,
    ))
    return result


def _build_timeline(
    reports: list[tuple[str, str, dict[str, Any]]],
) -> list[TimelineEntry]:
    """Build a timeline from ordered reports.

    Args:
        reports: List of (name, date, data) tuples.

    Returns:
        Chronologically sorted timeline entries.
    """
    entries: list[TimelineEntry] = []
    for name, date, data in reports:
        sev_counts: dict[str, int] = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
        total = 0

        # From summary
        summary = data.get("summary", {})
        if summary.get("severity_counts"):
            sev_counts = dict(summary["severity_counts"])
            total = summary.get("total_findings", 0)
        else:
            # Count from results
            for r in data.get("results", []):
                for f in r.get("findings", []):
                    sev = f.get("severity", "INFO")
                    sev_counts[sev] = sev_counts.get(sev, 0) + 1
                    total += 1
            # Count from flat findings
            for f in data.get("findings", []):
                if isinstance(f, dict):
                    sev = f.get("severity", "INFO")
                    sev_counts[sev] = sev_counts.get(sev, 0) + 1
                    total += 1

        entries.append(TimelineEntry(
            date=date,
            total_findings=total,
            severity_counts=sev_counts,
            report_name=name,
        ))

    entries.sort(key=lambda x: x.date)
    return entries


def _compute_trend(timeline: list[TimelineEntry]) -> TrendAnalysis:
    """Compute trend from timeline entries.

    Args:
        timeline: Chronological timeline entries.

    Returns:
        TrendAnalysis.
    """
    if len(timeline) < 2:
        return TrendAnalysis(direction="stable")

    first = timeline[0]
    last = timeline[-1]

    finding_delta = last.total_findings - first.total_findings
    critical_delta = (
        last.severity_counts.get("CRITICAL", 0)
        - first.severity_counts.get("CRITICAL", 0)
    )

    if abs(finding_delta) <= 1:
        direction = "stable"
    elif finding_delta < 0:
        direction = "improving"
    else:
        direction = "degrading"

    return TrendAnalysis(
        direction=direction,
        finding_delta=finding_delta,
        critical_delta=critical_delta,
    )


def _generate_executive_summary(
    report: AggregatedReport,
) -> str:
    """Generate an auto-generated executive summary.

    Args:
        report: The aggregated report.

    Returns:
        Summary text string.
    """
    lines: list[str] = []
    lines.append(
        f"Aggregated analysis of {report.total_reports} scan report(s) "
        f"found {report.total_unique_findings} unique finding(s)."
    )

    crit = report.severity_counts.get("CRITICAL", 0)
    high = report.severity_counts.get("HIGH", 0)

    if crit > 0:
        lines.append(
            f"CRITICAL: {crit} critical finding(s) require immediate attention."
        )
    if high > 0:
        lines.append(
            f"HIGH: {high} high-severity finding(s) should be addressed promptly."
        )

    if report.trend:
        if report.trend.direction == "improving":
            lines.append("Trend: Security posture is IMPROVING across scans.")
        elif report.trend.direction == "degrading":
            lines.append("Trend: Security posture is DEGRADING — action needed.")
        else:
            lines.append("Trend: Security posture is STABLE across scans.")

    if crit == 0 and high == 0:
        lines.append("No critical or high-severity findings detected.")

    return " ".join(lines)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def merge_reports(
    reports: list[tuple[str, str, dict[str, Any]]],
) -> AggregatedReport:
    """Merge multiple scan reports into a consolidated report.

    Args:
        reports: List of (name, date, data) tuples where:
            - name: Report label/filename
            - date: ISO date string (e.g. "2024-01-15")
            - data: Parsed JSON report dict

    Returns:
        AggregatedReport with deduplicated findings, timeline, and trend.
    """
    all_findings: list[AggregatedFinding] = []
    report_names: list[str] = []

    for name, date, data in reports:
        report_names.append(name)
        extracted = _extract_findings(data, name, date)
        all_findings.extend(extracted)

    deduped = _dedup_findings(all_findings)
    timeline = _build_timeline(reports)
    trend = _compute_trend(timeline)

    # Severity counts
    sev_counts: dict[str, int] = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0}
    for f in deduped:
        sev_counts[f.severity] = sev_counts.get(f.severity, 0) + 1

    agg = AggregatedReport(
        findings=deduped,
        timeline=timeline,
        trend=trend,
        total_reports=len(reports),
        total_unique_findings=len(deduped),
        severity_counts=sev_counts,
        report_names=report_names,
    )
    agg.executive_summary = _generate_executive_summary(agg)
    return agg


def merge_report_files(
    paths: list[str | Path],
) -> AggregatedReport:
    """Merge report files from disk.

    Reads JSON report files and merges them. Uses file modification time
    as the date and filename as the name.

    Args:
        paths: List of paths to JSON report files.

    Returns:
        AggregatedReport.

    Raises:
        FileNotFoundError: If a file does not exist.
    """
    import os
    from datetime import datetime, timezone

    reports: list[tuple[str, str, dict[str, Any]]] = []

    for p in paths:
        p = Path(p)
        if not p.exists():
            raise FileNotFoundError(f"Report file not found: {p}")

        data = json.loads(p.read_text(encoding="utf-8"))
        mtime = os.path.getmtime(p)
        date_str = datetime.fromtimestamp(mtime, tz=timezone.utc).strftime("%Y-%m-%d")
        reports.append((p.name, date_str, data))

    return merge_reports(reports)


def compute_trend_from_reports(
    reports: list[tuple[str, str, dict[str, Any]]],
) -> TrendAnalysis:
    """Compute trend analysis from multiple reports without full merge.

    Args:
        reports: List of (name, date, data) tuples.

    Returns:
        TrendAnalysis.
    """
    timeline = _build_timeline(reports)
    return _compute_trend(timeline)


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

def format_aggregate_text(report: AggregatedReport) -> str:
    """Format an aggregated report as human-readable text.

    Args:
        report: AggregatedReport to format.

    Returns:
        Formatted text string.
    """
    lines: list[str] = []
    lines.append("=" * 60)
    lines.append("MCP Security Audit — Aggregated Report")
    lines.append("=" * 60)
    lines.append(f"Reports merged: {report.total_reports}")
    lines.append(f"Unique findings: {report.total_unique_findings}")
    lines.append("")

    # Severity breakdown
    lines.append("Severity Breakdown:")
    for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
        count = report.severity_counts.get(sev, 0)
        if count > 0:
            lines.append(f"  {sev}: {count}")
    lines.append("")

    # Executive summary
    if report.executive_summary:
        lines.append("Executive Summary:")
        lines.append(f"  {report.executive_summary}")
        lines.append("")

    # Trend
    if report.trend:
        lines.append(f"Trend: {report.trend.direction}")
        if report.trend.finding_delta != 0:
            sign = "+" if report.trend.finding_delta > 0 else ""
            lines.append(f"  Finding delta: {sign}{report.trend.finding_delta}")
        lines.append("")

    # Timeline
    if report.timeline:
        lines.append("Timeline:")
        for entry in report.timeline:
            lines.append(
                f"  {entry.date} [{entry.report_name}] — "
                f"{entry.total_findings} findings"
            )
        lines.append("")

    # Findings
    if report.findings:
        lines.append("Findings:")
        for f in report.findings:
            lines.append(f"  [{f.severity}] {f.rule_id}: {f.title}")
            lines.append(f"    File: {f.file}:{f.line}")
            if f.occurrence_count > 1:
                lines.append(f"    Seen {f.occurrence_count} times across {len(f.reports)} report(s)")
            if f.first_seen and f.last_seen and f.first_seen != f.last_seen:
                lines.append(f"    First seen: {f.first_seen}, Last seen: {f.last_seen}")

    return "\n".join(lines)


def format_aggregate_json(report: AggregatedReport) -> str:
    """Format an aggregated report as JSON.

    Args:
        report: AggregatedReport to format.

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


def format_aggregate_html(report: AggregatedReport) -> str:
    """Format an aggregated report as self-contained HTML.

    Args:
        report: AggregatedReport to format.

    Returns:
        HTML string.
    """
    def esc(text: str) -> str:
        return (
            text.replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace('"', "&quot;")
        )

    sev_colors = {
        "CRITICAL": "#cc0000",
        "HIGH": "#e65100",
        "MEDIUM": "#f9a825",
        "LOW": "#2e7d32",
        "INFO": "#616161",
    }

    finding_rows = ""
    for f in report.findings:
        color = sev_colors.get(f.severity, "#333")
        finding_rows += (
            f"<tr><td style='color:{color};font-weight:bold'>{esc(f.severity)}</td>"
            f"<td>{esc(f.rule_id)}</td>"
            f"<td>{esc(f.title)}</td>"
            f"<td>{esc(f.file)}:{f.line}</td>"
            f"<td>{f.occurrence_count}</td></tr>\n"
        )

    timeline_rows = ""
    for entry in report.timeline:
        timeline_rows += (
            f"<tr><td>{esc(entry.date)}</td>"
            f"<td>{esc(entry.report_name)}</td>"
            f"<td>{entry.total_findings}</td></tr>\n"
        )

    trend_html = ""
    if report.trend:
        trend_html = f"<p>Trend: <strong>{esc(report.trend.direction)}</strong></p>"

    return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Aggregated Security Report</title>
<style>
body {{ font-family: sans-serif; max-width: 1000px; margin: 2em auto; }}
table {{ border-collapse: collapse; width: 100%; margin: 1em 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background: #f4f4f4; }}
.summary {{ background: #f9f9f9; padding: 1em; border-radius: 4px; margin: 1em 0; }}
</style></head><body>
<h1>MCP Security Audit &mdash; Aggregated Report</h1>
<div class="summary">
<p>Reports merged: {report.total_reports} | Unique findings: {report.total_unique_findings}</p>
<p>{esc(report.executive_summary)}</p>
{trend_html}
</div>
<h2>Findings ({report.total_unique_findings})</h2>
<table><tr><th>Severity</th><th>Rule</th><th>Title</th><th>Location</th><th>Count</th></tr>
{finding_rows}</table>
<h2>Timeline</h2>
<table><tr><th>Date</th><th>Report</th><th>Findings</th></tr>
{timeline_rows}</table>
</body></html>"""
