"""MCP Security Metrics — Aggregated security metrics for dashboards.

Collects and aggregates metrics from scans, fixes, and compliance checks.
Supports time-series views (day/week/month), threshold-based alerts,
and multiple output formats.

Zero external dependencies — pure Python stdlib.
"""

from __future__ import annotations

import json
import math
import time
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


# ---------- Data Models ----------

@dataclass
class MetricPoint:
    """A single metric data point."""

    name: str
    value: float
    timestamp: str  # ISO 8601
    labels: dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {
            "name": self.name,
            "value": self.value,
            "timestamp": self.timestamp,
        }
        if self.labels:
            result["labels"] = self.labels
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MetricPoint:
        return cls(
            name=data.get("name", ""),
            value=data.get("value", 0.0),
            timestamp=data.get("timestamp", ""),
            labels=data.get("labels", {}),
        )


@dataclass
class Alert:
    """A threshold-based alert."""

    metric_name: str
    threshold: float
    direction: str  # above, below
    current_value: float
    message: str
    severity: str = "warning"  # warning, critical
    timestamp: str = ""

    def __post_init__(self) -> None:
        if not self.timestamp:
            self.timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

    @property
    def is_triggered(self) -> bool:
        if self.direction == "below":
            return self.current_value < self.threshold
        return self.current_value > self.threshold

    def to_dict(self) -> dict[str, Any]:
        return {
            "metric_name": self.metric_name,
            "threshold": self.threshold,
            "direction": self.direction,
            "current_value": self.current_value,
            "message": self.message,
            "severity": self.severity,
            "is_triggered": self.is_triggered,
            "timestamp": self.timestamp,
        }


@dataclass
class AlertRule:
    """Definition of a threshold alert rule."""

    metric_name: str
    threshold: float
    direction: str = "above"  # above, below
    severity: str = "warning"
    message: str = ""

    def evaluate(self, value: float) -> Alert | None:
        """Evaluate the alert rule against a value.

        Returns an Alert if triggered, None otherwise.
        """
        alert = Alert(
            metric_name=self.metric_name,
            threshold=self.threshold,
            direction=self.direction,
            current_value=value,
            message=self.message or f"{self.metric_name} is {self.direction} threshold {self.threshold}",
            severity=self.severity,
        )
        if alert.is_triggered:
            return alert
        return None

    def to_dict(self) -> dict[str, Any]:
        return {
            "metric_name": self.metric_name,
            "threshold": self.threshold,
            "direction": self.direction,
            "severity": self.severity,
            "message": self.message,
        }


@dataclass
class TimeSeries:
    """Time series of metric points."""

    name: str
    points: list[MetricPoint] = field(default_factory=list)

    @property
    def latest(self) -> float:
        if not self.points:
            return 0.0
        return self.points[-1].value

    @property
    def average(self) -> float:
        if not self.points:
            return 0.0
        return sum(p.value for p in self.points) / len(self.points)

    @property
    def minimum(self) -> float:
        if not self.points:
            return 0.0
        return min(p.value for p in self.points)

    @property
    def maximum(self) -> float:
        if not self.points:
            return 0.0
        return max(p.value for p in self.points)

    @property
    def trend(self) -> str:
        """Calculate trend direction: improving, stable, declining."""
        if len(self.points) < 2:
            return "stable"
        first_half = self.points[:len(self.points) // 2]
        second_half = self.points[len(self.points) // 2:]
        avg_first = sum(p.value for p in first_half) / len(first_half)
        avg_second = sum(p.value for p in second_half) / len(second_half)
        diff = avg_second - avg_first
        threshold = 0.05 * abs(avg_first) if avg_first != 0 else 0.1
        if diff > threshold:
            return "improving"
        if diff < -threshold:
            return "declining"
        return "stable"

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "latest": self.latest,
            "average": round(self.average, 2),
            "min": self.minimum,
            "max": self.maximum,
            "trend": self.trend,
            "points_count": len(self.points),
            "points": [p.to_dict() for p in self.points],
        }


@dataclass
class MetricsSnapshot:
    """A snapshot of all metrics at a point in time."""

    timestamp: str
    scan_count: int = 0
    findings_total: int = 0
    findings_per_scan: float = 0.0
    fix_rate: float = 0.0  # percentage of fixed findings
    compliance_score: float = 0.0  # 0-100
    trust_score: float = 0.0  # 0-100
    critical_count: int = 0
    high_count: int = 0
    medium_count: int = 0
    low_count: int = 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "timestamp": self.timestamp,
            "scan_count": self.scan_count,
            "findings_total": self.findings_total,
            "findings_per_scan": round(self.findings_per_scan, 2),
            "fix_rate": round(self.fix_rate, 2),
            "compliance_score": round(self.compliance_score, 2),
            "trust_score": round(self.trust_score, 2),
            "critical_count": self.critical_count,
            "high_count": self.high_count,
            "medium_count": self.medium_count,
            "low_count": self.low_count,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MetricsSnapshot:
        return cls(
            timestamp=data.get("timestamp", ""),
            scan_count=data.get("scan_count", 0),
            findings_total=data.get("findings_total", 0),
            findings_per_scan=data.get("findings_per_scan", 0.0),
            fix_rate=data.get("fix_rate", 0.0),
            compliance_score=data.get("compliance_score", 0.0),
            trust_score=data.get("trust_score", 0.0),
            critical_count=data.get("critical_count", 0),
            high_count=data.get("high_count", 0),
            medium_count=data.get("medium_count", 0),
            low_count=data.get("low_count", 0),
        )


@dataclass
class MetricsReport:
    """Aggregated metrics report."""

    snapshots: list[MetricsSnapshot] = field(default_factory=list)
    alerts: list[Alert] = field(default_factory=list)
    period: str = "day"  # day, week, month
    timestamp: str = ""

    def __post_init__(self) -> None:
        if not self.timestamp:
            self.timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

    @property
    def total_scans(self) -> int:
        return sum(s.scan_count for s in self.snapshots)

    @property
    def total_findings(self) -> int:
        return sum(s.findings_total for s in self.snapshots)

    @property
    def avg_findings_per_scan(self) -> float:
        if self.total_scans == 0:
            return 0.0
        return self.total_findings / self.total_scans

    @property
    def avg_trust_score(self) -> float:
        scores = [s.trust_score for s in self.snapshots if s.trust_score > 0]
        if not scores:
            return 0.0
        return sum(scores) / len(scores)

    @property
    def avg_compliance_score(self) -> float:
        scores = [s.compliance_score for s in self.snapshots if s.compliance_score > 0]
        if not scores:
            return 0.0
        return sum(scores) / len(scores)

    @property
    def avg_fix_rate(self) -> float:
        rates = [s.fix_rate for s in self.snapshots if s.fix_rate > 0]
        if not rates:
            return 0.0
        return sum(rates) / len(rates)

    @property
    def trust_trend(self) -> str:
        """Trust score trend: improving, stable, declining."""
        scores = [s.trust_score for s in self.snapshots if s.trust_score > 0]
        if len(scores) < 2:
            return "stable"
        first = scores[:len(scores) // 2]
        second = scores[len(scores) // 2:]
        avg_first = sum(first) / len(first)
        avg_second = sum(second) / len(second)
        diff = avg_second - avg_first
        if diff > 2:
            return "improving"
        if diff < -2:
            return "declining"
        return "stable"

    @property
    def triggered_alerts(self) -> list[Alert]:
        return [a for a in self.alerts if a.is_triggered]

    def to_dict(self) -> dict[str, Any]:
        return {
            "period": self.period,
            "timestamp": self.timestamp,
            "total_scans": self.total_scans,
            "total_findings": self.total_findings,
            "avg_findings_per_scan": round(self.avg_findings_per_scan, 2),
            "avg_trust_score": round(self.avg_trust_score, 2),
            "avg_compliance_score": round(self.avg_compliance_score, 2),
            "avg_fix_rate": round(self.avg_fix_rate, 2),
            "trust_trend": self.trust_trend,
            "alerts_triggered": len(self.triggered_alerts),
            "snapshots": [s.to_dict() for s in self.snapshots],
            "alerts": [a.to_dict() for a in self.alerts],
        }


# ---------- Metric Collector ----------

class MetricCollector:
    """Collects and aggregates security metrics from scan data."""

    def __init__(self) -> None:
        self.snapshots: list[MetricsSnapshot] = []
        self.alert_rules: list[AlertRule] = []
        self._init_default_alerts()

    def _init_default_alerts(self) -> None:
        """Set up default alert rules."""
        self.alert_rules = [
            AlertRule(
                metric_name="trust_score",
                threshold=50.0,
                direction="below",
                severity="critical",
                message="Trust score below 50 — critical security posture",
            ),
            AlertRule(
                metric_name="trust_score",
                threshold=70.0,
                direction="below",
                severity="warning",
                message="Trust score below 70 — needs attention",
            ),
            AlertRule(
                metric_name="critical_count",
                threshold=0,
                direction="above",
                severity="critical",
                message="Critical findings detected",
            ),
            AlertRule(
                metric_name="findings_per_scan",
                threshold=10.0,
                direction="above",
                severity="warning",
                message="High findings density (>10 per scan)",
            ),
            AlertRule(
                metric_name="fix_rate",
                threshold=50.0,
                direction="below",
                severity="warning",
                message="Fix rate below 50% — many issues unfixed",
            ),
            AlertRule(
                metric_name="compliance_score",
                threshold=60.0,
                direction="below",
                severity="warning",
                message="Compliance score below 60%",
            ),
        ]

    def add_alert_rule(self, rule: AlertRule) -> None:
        """Add a custom alert rule."""
        self.alert_rules.append(rule)

    def remove_alert_rule(self, metric_name: str) -> None:
        """Remove all alert rules for a metric."""
        self.alert_rules = [r for r in self.alert_rules if r.metric_name != metric_name]

    def record_scan(
        self,
        *,
        findings_total: int = 0,
        critical: int = 0,
        high: int = 0,
        medium: int = 0,
        low: int = 0,
        trust_score: float = 0.0,
        compliance_score: float = 0.0,
        fix_rate: float = 0.0,
        timestamp: str = "",
    ) -> MetricsSnapshot:
        """Record metrics from a single scan.

        Args:
            findings_total: Total findings count.
            critical: Critical findings.
            high: High findings.
            medium: Medium findings.
            low: Low findings.
            trust_score: Trust score (0-100).
            compliance_score: Compliance score (0-100).
            fix_rate: Fix rate percentage.
            timestamp: ISO timestamp (default: now).

        Returns:
            The recorded MetricsSnapshot.
        """
        ts = timestamp or time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        snapshot = MetricsSnapshot(
            timestamp=ts,
            scan_count=1,
            findings_total=findings_total,
            findings_per_scan=float(findings_total),
            fix_rate=fix_rate,
            compliance_score=compliance_score,
            trust_score=trust_score,
            critical_count=critical,
            high_count=high,
            medium_count=medium,
            low_count=low,
        )
        self.snapshots.append(snapshot)
        return snapshot

    def get_alerts(self) -> list[Alert]:
        """Evaluate all alert rules against the latest snapshot.

        Returns:
            List of triggered alerts.
        """
        if not self.snapshots:
            return []

        latest = self.snapshots[-1]
        alerts: list[Alert] = []

        metric_values: dict[str, float] = {
            "trust_score": latest.trust_score,
            "compliance_score": latest.compliance_score,
            "fix_rate": latest.fix_rate,
            "findings_per_scan": latest.findings_per_scan,
            "critical_count": float(latest.critical_count),
            "high_count": float(latest.high_count),
            "scan_count": float(latest.scan_count),
            "findings_total": float(latest.findings_total),
        }

        for rule in self.alert_rules:
            if rule.metric_name in metric_values:
                alert = rule.evaluate(metric_values[rule.metric_name])
                if alert is not None:
                    alerts.append(alert)

        return alerts

    def aggregate(
        self,
        period: str = "day",
    ) -> MetricsReport:
        """Aggregate metrics by period.

        Args:
            period: Aggregation period (day, week, month).

        Returns:
            MetricsReport with aggregated data.
        """
        if not self.snapshots:
            return MetricsReport(period=period)

        buckets = _bucket_snapshots(self.snapshots, period)
        aggregated: list[MetricsSnapshot] = []

        for bucket_key, bucket_snapshots in sorted(buckets.items()):
            agg = _aggregate_bucket(bucket_snapshots, bucket_key)
            aggregated.append(agg)

        alerts = self.get_alerts()

        return MetricsReport(
            snapshots=aggregated,
            alerts=alerts,
            period=period,
        )

    def build_time_series(self, metric: str) -> TimeSeries:
        """Build a time series for a specific metric.

        Args:
            metric: Metric name (trust_score, compliance_score, etc.).

        Returns:
            TimeSeries with metric points.
        """
        ts = TimeSeries(name=metric)

        for snapshot in self.snapshots:
            value = getattr(snapshot, metric, None)
            if value is not None:
                ts.points.append(MetricPoint(
                    name=metric,
                    value=float(value),
                    timestamp=snapshot.timestamp,
                ))

        return ts

    def to_dict(self) -> dict[str, Any]:
        return {
            "snapshots_count": len(self.snapshots),
            "alert_rules_count": len(self.alert_rules),
            "snapshots": [s.to_dict() for s in self.snapshots],
            "alert_rules": [r.to_dict() for r in self.alert_rules],
        }


# ---------- Aggregation Helpers ----------

def _get_bucket_key(timestamp: str, period: str) -> str:
    """Extract bucket key from timestamp based on period."""
    # Timestamps are ISO 8601: "2024-01-15T10:30:00Z"
    date_part = timestamp[:10]  # "2024-01-15"

    if period == "day":
        return date_part
    elif period == "week":
        # Use ISO week number
        parts = date_part.split("-")
        if len(parts) == 3:
            year, month, day = int(parts[0]), int(parts[1]), int(parts[2])
            # Approximate week: use day of year / 7
            day_of_year = _approximate_day_of_year(year, month, day)
            week_num = day_of_year // 7 + 1
            return f"{year}-W{week_num:02d}"
        return date_part
    elif period == "month":
        return date_part[:7]  # "2024-01"
    return date_part


def _approximate_day_of_year(year: int, month: int, day: int) -> int:
    """Approximate day of year without datetime."""
    days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    # Leap year check
    if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
        days_in_month[2] = 29
    return sum(days_in_month[:month]) + day


def _bucket_snapshots(
    snapshots: list[MetricsSnapshot], period: str
) -> dict[str, list[MetricsSnapshot]]:
    """Group snapshots into time buckets."""
    buckets: dict[str, list[MetricsSnapshot]] = defaultdict(list)
    for snapshot in snapshots:
        key = _get_bucket_key(snapshot.timestamp, period)
        buckets[key].append(snapshot)
    return dict(buckets)


def _aggregate_bucket(
    snapshots: list[MetricsSnapshot], bucket_key: str
) -> MetricsSnapshot:
    """Aggregate multiple snapshots into one."""
    total_scans = sum(s.scan_count for s in snapshots)
    total_findings = sum(s.findings_total for s in snapshots)
    fps = total_findings / total_scans if total_scans > 0 else 0.0

    trust_scores = [s.trust_score for s in snapshots if s.trust_score > 0]
    compliance_scores = [s.compliance_score for s in snapshots if s.compliance_score > 0]
    fix_rates = [s.fix_rate for s in snapshots if s.fix_rate > 0]

    return MetricsSnapshot(
        timestamp=bucket_key,
        scan_count=total_scans,
        findings_total=total_findings,
        findings_per_scan=fps,
        fix_rate=sum(fix_rates) / len(fix_rates) if fix_rates else 0.0,
        compliance_score=sum(compliance_scores) / len(compliance_scores) if compliance_scores else 0.0,
        trust_score=sum(trust_scores) / len(trust_scores) if trust_scores else 0.0,
        critical_count=sum(s.critical_count for s in snapshots),
        high_count=sum(s.high_count for s in snapshots),
        medium_count=sum(s.medium_count for s in snapshots),
        low_count=sum(s.low_count for s in snapshots),
    )


# ---------- Persistence ----------

def save_metrics(snapshots: list[MetricsSnapshot], path: Path | str) -> None:
    """Save metrics snapshots to a JSON file.

    Args:
        snapshots: List of snapshots.
        path: File path.
    """
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    data = [s.to_dict() for s in snapshots]
    p.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")


def load_metrics(path: Path | str) -> list[MetricsSnapshot]:
    """Load metrics snapshots from a JSON file.

    Args:
        path: File path.

    Returns:
        List of MetricsSnapshot.
    """
    p = Path(path)
    if not p.exists():
        return []
    data = json.loads(p.read_text(encoding="utf-8"))
    return [MetricsSnapshot.from_dict(d) for d in data]


# ---------- Format Functions ----------

def format_metrics_text(report: MetricsReport) -> str:
    """Format metrics report as human-readable text.

    Args:
        report: MetricsReport.

    Returns:
        Formatted text.
    """
    lines: list[str] = []
    lines.append("")
    lines.append("MCP Security Metrics Report")
    lines.append("=" * 50)
    lines.append(f"Period: {report.period}")
    lines.append(f"Total scans: {report.total_scans}")
    lines.append(f"Total findings: {report.total_findings}")
    lines.append(f"Avg findings/scan: {report.avg_findings_per_scan:.1f}")
    lines.append(f"Avg trust score: {report.avg_trust_score:.1f}")
    lines.append(f"Avg compliance: {report.avg_compliance_score:.1f}%")
    lines.append(f"Avg fix rate: {report.avg_fix_rate:.1f}%")
    lines.append(f"Trust trend: {report.trust_trend}")
    lines.append("")

    if report.snapshots:
        lines.append("Time Series:")
        lines.append(f"  {'Period':<16} {'Scans':<8} {'Findings':<10} {'Trust':<8} {'Compliance':<12}")
        lines.append(f"  {'─' * 16} {'─' * 8} {'─' * 10} {'─' * 8} {'─' * 12}")
        for s in report.snapshots:
            lines.append(
                f"  {s.timestamp:<16} {s.scan_count:<8} "
                f"{s.findings_total:<10} {s.trust_score:<8.1f} "
                f"{s.compliance_score:<12.1f}"
            )
        lines.append("")

    triggered = report.triggered_alerts
    if triggered:
        lines.append(f"Alerts ({len(triggered)}):")
        for alert in triggered:
            marker = "[CRIT]" if alert.severity == "critical" else "[WARN]"
            lines.append(f"  {marker} {alert.message} (current: {alert.current_value:.1f})")
        lines.append("")
    else:
        lines.append("No alerts triggered.")
        lines.append("")

    return "\n".join(lines)


def format_metrics_json(report: MetricsReport) -> str:
    """Format metrics report as JSON.

    Args:
        report: MetricsReport.

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


def format_metrics_html(report: MetricsReport) -> str:
    """Format metrics report as HTML dashboard.

    Args:
        report: MetricsReport.

    Returns:
        HTML string.
    """
    rows: list[str] = []
    for s in report.snapshots:
        trust_color = "#2ecc71" if s.trust_score >= 70 else "#e67e22" if s.trust_score >= 50 else "#e74c3c"
        rows.append(f"""
        <tr>
            <td>{s.timestamp}</td>
            <td>{s.scan_count}</td>
            <td>{s.findings_total}</td>
            <td style="color:{trust_color};font-weight:bold">{s.trust_score:.1f}</td>
            <td>{s.compliance_score:.1f}%</td>
            <td>{s.fix_rate:.1f}%</td>
            <td>{s.critical_count}</td>
            <td>{s.high_count}</td>
        </tr>""")

    alert_rows: list[str] = []
    for a in report.triggered_alerts:
        color = "#e74c3c" if a.severity == "critical" else "#e67e22"
        alert_rows.append(f"""
        <tr>
            <td style="color:{color};font-weight:bold">{a.severity.upper()}</td>
            <td>{a.message}</td>
            <td>{a.current_value:.1f}</td>
            <td>{a.threshold}</td>
        </tr>""")

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MCP Security Metrics</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2em; background: #f8f9fa; }}
        h1, h2 {{ color: #2c3e50; }}
        table {{ border-collapse: collapse; width: 100%; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 1em 0; }}
        th {{ background: #2c3e50; color: white; padding: 12px; text-align: left; }}
        td {{ padding: 10px 12px; border-bottom: 1px solid #ecf0f1; }}
        tr:hover {{ background: #f5f5f5; }}
        .card {{ display: inline-block; margin: 0.5em; padding: 1.5em; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); min-width: 150px; text-align: center; }}
        .card .value {{ font-size: 2em; font-weight: bold; color: #2c3e50; }}
        .card .label {{ color: #7f8c8d; font-size: 0.9em; }}
    </style>
</head>
<body>
    <h1>MCP Security Metrics Dashboard</h1>
    <div>
        <div class="card"><div class="value">{report.total_scans}</div><div class="label">Total Scans</div></div>
        <div class="card"><div class="value">{report.total_findings}</div><div class="label">Total Findings</div></div>
        <div class="card"><div class="value">{report.avg_trust_score:.1f}</div><div class="label">Avg Trust Score</div></div>
        <div class="card"><div class="value">{report.avg_compliance_score:.1f}%</div><div class="label">Avg Compliance</div></div>
        <div class="card"><div class="value">{report.avg_fix_rate:.1f}%</div><div class="label">Avg Fix Rate</div></div>
        <div class="card"><div class="value">{report.trust_trend}</div><div class="label">Trust Trend</div></div>
    </div>

    <h2>Time Series ({report.period})</h2>
    <table>
        <thead>
            <tr>
                <th>Period</th><th>Scans</th><th>Findings</th><th>Trust</th>
                <th>Compliance</th><th>Fix Rate</th><th>Critical</th><th>High</th>
            </tr>
        </thead>
        <tbody>{''.join(rows)}</tbody>
    </table>

    <h2>Alerts ({len(report.triggered_alerts)})</h2>
    {'<table><thead><tr><th>Severity</th><th>Message</th><th>Current</th><th>Threshold</th></tr></thead><tbody>' + ''.join(alert_rows) + '</tbody></table>' if alert_rows else '<p>No alerts triggered.</p>'}
</body>
</html>"""
