"""Security Metrics Dashboard — Consolidated KPI dashboard with sparklines.

Generates a self-contained HTML dashboard showing:
- KPIs: total findings, MTTR, resolution rate, new findings/week, compliance %
- CSS sparklines for trend visualization
- Trend arrows (up/down/stable)
- RAG status indicators (red/amber/green)
- Auto-refresh meta tag with configurable interval

Zero external dependencies — pure Python stdlib.
"""

from __future__ import annotations

import html
import hashlib
import json
import time
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any


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

@dataclass
class ScanEntry:
    """A single scan record for dashboard input."""

    timestamp: str  # ISO 8601
    findings_total: int = 0
    findings_new: int = 0
    findings_resolved: int = 0
    critical: int = 0
    high: int = 0
    medium: int = 0
    low: int = 0
    compliance_pct: float = 0.0
    mttr_hours: float = 0.0  # mean time to remediate

    def to_dict(self) -> dict[str, Any]:
        return {
            "timestamp": self.timestamp,
            "findings_total": self.findings_total,
            "findings_new": self.findings_new,
            "findings_resolved": self.findings_resolved,
            "critical": self.critical,
            "high": self.high,
            "medium": self.medium,
            "low": self.low,
            "compliance_pct": round(self.compliance_pct, 2),
            "mttr_hours": round(self.mttr_hours, 2),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ScanEntry:
        return cls(
            timestamp=data.get("timestamp", ""),
            findings_total=data.get("findings_total", 0),
            findings_new=data.get("findings_new", 0),
            findings_resolved=data.get("findings_resolved", 0),
            critical=data.get("critical", 0),
            high=data.get("high", 0),
            medium=data.get("medium", 0),
            low=data.get("low", 0),
            compliance_pct=data.get("compliance_pct", 0.0),
            mttr_hours=data.get("mttr_hours", 0.0),
        )


@dataclass
class KPI:
    """A single KPI value with trend info."""

    name: str
    value: float
    unit: str = ""
    trend: str = "stable"  # improving, stable, declining
    rag: str = "green"  # red, amber, green
    sparkline: list[float] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "value": round(self.value, 2),
            "unit": self.unit,
            "trend": self.trend,
            "rag": self.rag,
            "sparkline": [round(v, 2) for v in self.sparkline],
        }


@dataclass
class DashboardOptions:
    """Configuration options for the dashboard."""

    title: str = "Security Metrics Dashboard"
    refresh_interval: int = 300  # seconds, 0 = no auto-refresh
    show_sparklines: bool = True
    rag_thresholds: dict[str, tuple[float, float]] | None = None
    # Default RAG thresholds: (amber_threshold, red_threshold) for "higher is worse" metrics
    # For compliance_pct: (amber_below, red_below) — reversed logic

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "refresh_interval": self.refresh_interval,
            "show_sparklines": self.show_sparklines,
            "rag_thresholds": self.rag_thresholds,
        }


@dataclass
class DashboardResult:
    """Result of dashboard generation."""

    html: str
    kpis: list[KPI] = field(default_factory=list)
    scan_count: int = 0
    timestamp: str = ""

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

    def to_dict(self) -> dict[str, Any]:
        return {
            "scan_count": self.scan_count,
            "timestamp": self.timestamp,
            "kpis": [k.to_dict() for k in self.kpis],
        }


# ---------- RAG Classification ----------

_DEFAULT_RAG_THRESHOLDS: dict[str, tuple[float, float]] = {
    # (amber_threshold, red_threshold) — higher value is worse
    "total_findings": (20.0, 50.0),
    "mttr_hours": (48.0, 168.0),  # 2 days amber, 7 days red
    "new_findings_week": (5.0, 15.0),
    # Reversed: lower is worse
    "resolution_rate": (70.0, 50.0),  # below 70 amber, below 50 red
    "compliance_pct": (80.0, 60.0),  # below 80 amber, below 60 red
}


def _classify_rag(
    metric_name: str,
    value: float,
    thresholds: dict[str, tuple[float, float]] | None = None,
) -> str:
    """Classify a metric value into RAG status.

    Args:
        metric_name: Name of the metric.
        value: Current metric value.
        thresholds: Custom thresholds override.

    Returns:
        'red', 'amber', or 'green'.
    """
    t = (thresholds or _DEFAULT_RAG_THRESHOLDS).get(metric_name)
    if t is None:
        return "green"

    amber_t, red_t = t

    # Reversed metrics (higher is better)
    reversed_metrics = {"resolution_rate", "compliance_pct"}
    if metric_name in reversed_metrics:
        if value < red_t:
            return "red"
        if value < amber_t:
            return "amber"
        return "green"

    # Normal metrics (lower is better)
    if value >= red_t:
        return "red"
    if value >= amber_t:
        return "amber"
    return "green"


# ---------- Trend Calculation ----------

def _compute_trend(values: list[float]) -> str:
    """Compute trend direction from a series of values.

    For 'findings' type metrics, declining values = improving.
    Returns 'improving', 'stable', or 'declining'.
    """
    if len(values) < 2:
        return "stable"

    mid = len(values) // 2
    first_half = values[:mid] if mid > 0 else values[:1]
    second_half = values[mid:]

    avg_first = sum(first_half) / len(first_half)
    avg_second = sum(second_half) / len(second_half)

    threshold = max(0.05 * abs(avg_first), 0.5) if avg_first != 0 else 0.5
    diff = avg_second - avg_first

    if abs(diff) < threshold:
        return "stable"
    if diff > 0:
        return "increasing"
    return "decreasing"


def _compute_findings_trend(values: list[float]) -> str:
    """For findings, decreasing = improving."""
    raw = _compute_trend(values)
    if raw == "decreasing":
        return "improving"
    if raw == "increasing":
        return "declining"
    return "stable"


def _compute_rate_trend(values: list[float]) -> str:
    """For rates/compliance, increasing = improving."""
    raw = _compute_trend(values)
    if raw == "increasing":
        return "improving"
    if raw == "decreasing":
        return "declining"
    return "stable"


# ---------- KPI Computation ----------

def _compute_kpis(
    scans: list[ScanEntry],
    thresholds: dict[str, tuple[float, float]] | None = None,
) -> list[KPI]:
    """Compute KPIs from scan entries."""
    if not scans:
        return []

    kpis: list[KPI] = []

    # 1. Total Findings (latest)
    total_values = [s.findings_total for s in scans]
    latest_total = total_values[-1] if total_values else 0
    kpis.append(KPI(
        name="Total Findings",
        value=float(latest_total),
        unit="findings",
        trend=_compute_findings_trend(total_values),
        rag=_classify_rag("total_findings", float(latest_total), thresholds),
        sparkline=total_values[-20:],  # last 20 points
    ))

    # 2. MTTR (Mean Time to Remediate)
    mttr_values = [s.mttr_hours for s in scans if s.mttr_hours > 0]
    avg_mttr = sum(mttr_values) / len(mttr_values) if mttr_values else 0.0
    kpis.append(KPI(
        name="MTTR",
        value=avg_mttr,
        unit="hours",
        trend=_compute_findings_trend(mttr_values) if mttr_values else "stable",
        rag=_classify_rag("mttr_hours", avg_mttr, thresholds),
        sparkline=mttr_values[-20:],
    ))

    # 3. Resolution Rate
    total_resolved = sum(s.findings_resolved for s in scans)
    total_new = sum(s.findings_new for s in scans)
    resolution_rate = (total_resolved / total_new * 100) if total_new > 0 else 100.0
    rate_values = []
    for s in scans:
        if s.findings_new > 0:
            rate_values.append(s.findings_resolved / s.findings_new * 100)
        else:
            rate_values.append(100.0)
    kpis.append(KPI(
        name="Resolution Rate",
        value=resolution_rate,
        unit="%",
        trend=_compute_rate_trend(rate_values),
        rag=_classify_rag("resolution_rate", resolution_rate, thresholds),
        sparkline=rate_values[-20:],
    ))

    # 4. New Findings/Week (approximate from latest data)
    new_values = [float(s.findings_new) for s in scans]
    # Use last 7 entries as proxy for a week
    week_slice = new_values[-7:] if len(new_values) >= 7 else new_values
    new_per_week = sum(week_slice)
    kpis.append(KPI(
        name="New Findings/Week",
        value=new_per_week,
        unit="findings",
        trend=_compute_findings_trend(new_values),
        rag=_classify_rag("new_findings_week", new_per_week, thresholds),
        sparkline=new_values[-20:],
    ))

    # 5. Compliance %
    compliance_values = [s.compliance_pct for s in scans if s.compliance_pct > 0]
    avg_compliance = sum(compliance_values) / len(compliance_values) if compliance_values else 0.0
    kpis.append(KPI(
        name="Compliance",
        value=avg_compliance,
        unit="%",
        trend=_compute_rate_trend(compliance_values) if compliance_values else "stable",
        rag=_classify_rag("compliance_pct", avg_compliance, thresholds),
        sparkline=compliance_values[-20:],
    ))

    return kpis


# ---------- Trend Arrow ----------

def _trend_arrow(trend: str) -> str:
    """Return an HTML arrow for a trend direction."""
    arrows = {
        "improving": '<span class="trend-arrow trend-up">&#9650;</span>',
        "declining": '<span class="trend-arrow trend-down">&#9660;</span>',
        "stable": '<span class="trend-arrow trend-stable">&#9654;</span>',
    }
    return arrows.get(trend, arrows["stable"])


# ---------- Sparkline CSS ----------

def _sparkline_css(values: list[float], height: int = 30) -> str:
    """Generate a CSS-only sparkline bar chart.

    Args:
        values: Data points.
        height: Max height in pixels.

    Returns:
        HTML string for sparkline.
    """
    if not values:
        return '<div class="sparkline"></div>'

    max_val = max(values) if max(values) > 0 else 1.0
    bars: list[str] = []
    bar_width = max(2, min(8, 160 // max(len(values), 1)))

    for v in values:
        h = max(1, int((v / max_val) * height))
        bars.append(
            f'<span class="spark-bar" style="height:{h}px;width:{bar_width}px"></span>'
        )

    return f'<div class="sparkline">{"".join(bars)}</div>'


# ---------- RAG Badge ----------

def _rag_badge(rag: str) -> str:
    """Generate RAG status badge HTML."""
    colors = {
        "red": "#dc2626",
        "amber": "#f59e0b",
        "green": "#22c55e",
    }
    color = colors.get(rag, colors["green"])
    label = rag.upper()
    return (
        f'<span class="rag-badge" style="background:{color}">{label}</span>'
    )


# ---------- HTML Generation ----------

def _generate_html(
    kpis: list[KPI],
    scans: list[ScanEntry],
    options: DashboardOptions,
) -> str:
    """Generate the full HTML dashboard."""
    title = html.escape(options.title)

    refresh_meta = ""
    if options.refresh_interval > 0:
        refresh_meta = f'<meta http-equiv="refresh" content="{options.refresh_interval}">'

    # KPI cards
    kpi_cards: list[str] = []
    for kpi in kpis:
        sparkline_html = ""
        if options.show_sparklines and kpi.sparkline:
            sparkline_html = _sparkline_css(kpi.sparkline)

        value_str = f"{kpi.value:.1f}" if kpi.value != int(kpi.value) else str(int(kpi.value))

        kpi_cards.append(f"""
        <div class="kpi-card">
            <div class="kpi-header">
                <span class="kpi-name">{html.escape(kpi.name)}</span>
                {_rag_badge(kpi.rag)}
            </div>
            <div class="kpi-value">{value_str}<span class="kpi-unit">{html.escape(kpi.unit)}</span></div>
            <div class="kpi-trend">{_trend_arrow(kpi.trend)} {html.escape(kpi.trend)}</div>
            {sparkline_html}
        </div>""")

    # Severity breakdown
    severity_rows: list[str] = []
    if scans:
        latest = scans[-1]
        for sev, count in [
            ("Critical", latest.critical),
            ("High", latest.high),
            ("Medium", latest.medium),
            ("Low", latest.low),
        ]:
            severity_rows.append(f"<tr><td>{sev}</td><td>{count}</td></tr>")

    # Scan history table
    history_rows: list[str] = []
    for s in scans[-10:]:  # last 10 scans
        history_rows.append(f"""
        <tr>
            <td>{html.escape(s.timestamp)}</td>
            <td>{s.findings_total}</td>
            <td>{s.findings_new}</td>
            <td>{s.findings_resolved}</td>
            <td>{s.compliance_pct:.1f}%</td>
        </tr>""")

    timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    {refresh_meta}
    <title>{title}</title>
    <style>
        * {{ margin: 0; padding: 0; box-sizing: border-box; }}
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2em; }}
        h1 {{ color: #f1f5f9; margin-bottom: 0.5em; font-size: 1.8em; }}
        h2 {{ color: #94a3b8; margin: 1.5em 0 0.5em; font-size: 1.2em; }}
        .meta {{ color: #64748b; font-size: 0.85em; margin-bottom: 1.5em; }}
        .kpi-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1em; margin: 1em 0; }}
        .kpi-card {{ background: #1e293b; border-radius: 12px; padding: 1.2em; box-shadow: 0 2px 8px rgba(0,0,0,0.3); }}
        .kpi-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5em; }}
        .kpi-name {{ color: #94a3b8; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.5px; }}
        .kpi-value {{ font-size: 2em; font-weight: 700; color: #f1f5f9; }}
        .kpi-unit {{ font-size: 0.4em; color: #64748b; margin-left: 4px; }}
        .kpi-trend {{ font-size: 0.85em; color: #94a3b8; margin-top: 0.3em; }}
        .trend-arrow {{ font-size: 0.8em; margin-right: 4px; }}
        .trend-up {{ color: #22c55e; }}
        .trend-down {{ color: #ef4444; }}
        .trend-stable {{ color: #64748b; }}
        .rag-badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.7em; font-weight: 600; text-transform: uppercase; }}
        .sparkline {{ display: flex; align-items: flex-end; gap: 1px; margin-top: 0.5em; height: 30px; }}
        .spark-bar {{ background: #3b82f6; border-radius: 1px; display: inline-block; min-height: 1px; }}
        table {{ border-collapse: collapse; width: 100%; background: #1e293b; border-radius: 8px; overflow: hidden; margin: 0.5em 0; }}
        th {{ background: #334155; color: #e2e8f0; padding: 10px 14px; text-align: left; font-size: 0.85em; text-transform: uppercase; }}
        td {{ padding: 8px 14px; border-bottom: 1px solid #334155; font-size: 0.9em; }}
        tr:hover {{ background: #263548; }}
        .grid-2 {{ display: grid; grid-template-columns: 1fr 2fr; gap: 1em; }}
        @media (max-width: 768px) {{ .grid-2 {{ grid-template-columns: 1fr; }} .kpi-grid {{ grid-template-columns: 1fr 1fr; }} }}
    </style>
</head>
<body>
    <h1>{title}</h1>
    <div class="meta">Generated: {timestamp} | Scans: {len(scans)}{f' | Auto-refresh: {options.refresh_interval}s' if options.refresh_interval > 0 else ''}</div>

    <div class="kpi-grid">
        {''.join(kpi_cards)}
    </div>

    <div class="grid-2">
        <div>
            <h2>Severity Breakdown</h2>
            <table>
                <thead><tr><th>Severity</th><th>Count</th></tr></thead>
                <tbody>{''.join(severity_rows)}</tbody>
            </table>
        </div>
        <div>
            <h2>Recent Scans</h2>
            <table>
                <thead><tr><th>Timestamp</th><th>Total</th><th>New</th><th>Resolved</th><th>Compliance</th></tr></thead>
                <tbody>{''.join(history_rows)}</tbody>
            </table>
        </div>
    </div>
</body>
</html>"""


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

def generate_metrics_dashboard(
    scans: list[dict[str, Any] | ScanEntry],
    options: dict[str, Any] | DashboardOptions | None = None,
) -> str:
    """Generate a consolidated security metrics dashboard as HTML.

    Args:
        scans: List of scan entries (dicts or ScanEntry objects).
        options: Dashboard configuration (dict or DashboardOptions).

    Returns:
        Complete HTML string.
    """
    # Normalize scans
    entries: list[ScanEntry] = []
    for s in scans:
        if isinstance(s, ScanEntry):
            entries.append(s)
        elif isinstance(s, dict):
            entries.append(ScanEntry.from_dict(s))

    # Normalize options
    opts: DashboardOptions
    if options is None:
        opts = DashboardOptions()
    elif isinstance(options, DashboardOptions):
        opts = options
    else:
        opts = DashboardOptions(
            title=options.get("title", "Security Metrics Dashboard"),
            refresh_interval=options.get("refresh_interval", 300),
            show_sparklines=options.get("show_sparklines", True),
            rag_thresholds=options.get("rag_thresholds"),
        )

    kpis = _compute_kpis(entries, opts.rag_thresholds)
    html_out = _generate_html(kpis, entries, opts)

    return html_out


def compute_kpis(
    scans: list[dict[str, Any] | ScanEntry],
    thresholds: dict[str, tuple[float, float]] | None = None,
) -> list[KPI]:
    """Compute KPIs without generating HTML.

    Args:
        scans: List of scan entries.
        thresholds: Custom RAG thresholds.

    Returns:
        List of KPI objects.
    """
    entries: list[ScanEntry] = []
    for s in scans:
        if isinstance(s, ScanEntry):
            entries.append(s)
        elif isinstance(s, dict):
            entries.append(ScanEntry.from_dict(s))
    return _compute_kpis(entries, thresholds)


def format_dashboard_result_json(kpis: list[KPI]) -> str:
    """Format KPIs as JSON.

    Args:
        kpis: List of KPI objects.

    Returns:
        JSON string.
    """
    return json.dumps(
        {"kpis": [k.to_dict() for k in kpis]},
        indent=2,
        ensure_ascii=False,
    )
