"""SVG chart helpers — pure functions returning SVG markup strings.

Four chart types: donut (compliance score), sparkline (trend line),
severity bar (stacked horizontal bar), and score trend (area chart
with drift events). No side effects, no Firestore reads.
"""

from __future__ import annotations

import math
from datetime import datetime


def render_donut_svg(score: float, size: int = 120) -> str:
    """Render a donut chart showing compliance score percentage.

    Color thresholds: <50 red, 50-75 amber, 75-90 blue, 90+ green.
    Uses stroke-dasharray technique for the arc.
    """
    if score < 50:
        color = "#dc2626"
    elif score < 75:
        color = "#ea580c"
    elif score < 90:
        color = "#2563eb"
    else:
        color = "#16a34a"

    clamped = max(0.0, min(100.0, score))
    radius = 40
    circumference = 2 * math.pi * radius
    filled = circumference * clamped / 100.0
    gap = circumference - filled
    cx = size / 2
    cy = size / 2
    font_size = size * 0.22

    return (
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}"'
        f' viewBox="0 0 {size} {size}" role="img"'
        f' aria-label="Compliance score: {clamped:.0f}%">'
        f'<circle cx="{cx}" cy="{cy}" r="{radius}"'
        f' fill="none" stroke="#e5e7eb" stroke-width="8"/>'
        f'<circle cx="{cx}" cy="{cy}" r="{radius}"'
        f' fill="none" stroke="{color}" stroke-width="8"'
        f' stroke-dasharray="{filled:.2f} {gap:.2f}"'
        f' stroke-dashoffset="{circumference * 0.25:.2f}"'
        f' stroke-linecap="round"'
        f' transform="rotate(-90 {cx} {cy})"/>'
        f'<text x="{cx}" y="{cy}" text-anchor="middle"'
        f' dominant-baseline="central"'
        f' font-family="system-ui, sans-serif" font-size="{font_size:.0f}"'
        f' font-weight="600" fill="currentColor">'
        f"{clamped:.0f}%</text>"
        f"</svg>"
    )


def render_sparkline_svg(
    data_points: list[float],
    width: int = 300,
    height: int = 60,
    color: str = "#2563eb",
    fill: bool = False,
) -> str:
    """Render an SVG sparkline from data points.

    Normalizes values to viewBox. Returns empty ``<svg>`` for empty data.
    """
    if not data_points:
        return (
            f'<svg xmlns="http://www.w3.org/2000/svg"'
            f' width="{width}" height="{height}"'
            f' viewBox="0 0 {width} {height}"></svg>'
        )

    min_val = min(data_points)
    max_val = max(data_points)
    val_range = max_val - min_val if max_val != min_val else 1.0

    padding = 2
    usable_h = height - padding * 2
    usable_w = width - padding * 2

    n = len(data_points)
    step = usable_w / max(n - 1, 1)

    points: list[str] = []
    for i, val in enumerate(data_points):
        x = padding + i * step
        y = padding + usable_h - (val - min_val) / val_range * usable_h
        points.append(f"{x:.1f},{y:.1f}")

    points_str = " ".join(points)

    fill_elem = ""
    if fill and len(points) >= 2:
        first_x = padding
        last_x = padding + (n - 1) * step
        bottom_y = padding + usable_h
        fill_points = (
            f"{first_x:.1f},{bottom_y:.1f} "
            + points_str
            + f" {last_x:.1f},{bottom_y:.1f}"
        )
        fill_elem = f'<polygon points="{fill_points}" fill="{color}" opacity="0.15"/>'

    return (
        f'<svg xmlns="http://www.w3.org/2000/svg"'
        f' width="{width}" height="{height}"'
        f' viewBox="0 0 {width} {height}" role="img"'
        f' aria-label="Sparkline chart">'
        f"{fill_elem}"
        f'<polyline points="{points_str}"'
        f' fill="none" stroke="{color}" stroke-width="1.5"/>'
        f"</svg>"
    )


def render_severity_bar_svg(
    severity_counts: dict[str, int],
    width: int = 200,
) -> str:
    """Render a stacked horizontal severity bar.

    Colors: critical #dc2626, high #ea580c, medium #854d0e, low #2563eb.
    Zero-count segments omitted. All zeros returns empty SVG.
    """
    colors = {
        "critical": "#dc2626",
        "high": "#ea580c",
        "medium": "#854d0e",
        "low": "#2563eb",
    }
    height = 20
    total = sum(severity_counts.get(k, 0) for k in colors)

    if total == 0:
        return (
            f'<svg xmlns="http://www.w3.org/2000/svg"'
            f' width="{width}" height="{height}"'
            f' viewBox="0 0 {width} {height}"></svg>'
        )

    rects: list[str] = []
    x = 0.0
    for sev, col in colors.items():
        count = severity_counts.get(sev, 0)
        if count == 0:
            continue
        seg_width = count / total * width
        rects.append(
            f'<rect x="{x:.1f}" y="0" width="{seg_width:.1f}"'
            f' height="{height}" fill="{col}">'
            f"<title>{sev}: {count}</title>"
            f"</rect>"
        )
        x += seg_width

    return (
        f'<svg xmlns="http://www.w3.org/2000/svg"'
        f' width="{width}" height="{height}"'
        f' viewBox="0 0 {width} {height}" role="img"'
        f' aria-label="Severity distribution">' + "".join(rects) + "</svg>"
    )


def render_score_trend_svg(
    score_history: list[dict[str, object]],
    drift_events: list[dict[str, object]],
    width: int = 600,
    height: int = 200,
    *,
    show_events: bool = True,
) -> str:
    """Render an SVG area chart with score trend and drift events.

    Blue gradient fill (opacity 0.15). Scan markers as circles.
    Drift events as red dots below the line (size proportional to
    abs(score_impact)). Zone bands: red (<50), amber (50-75), green (>75).
    X-axis: 90-day range with monthly labels. Y-axis: 0-100%, 25% gridlines.
    """
    if not score_history:
        return (
            f'<svg xmlns="http://www.w3.org/2000/svg"'
            f' width="{width}" height="{height}"'
            f' viewBox="0 0 {width} {height}"></svg>'
        )

    margin_left = 40
    margin_right = 10
    margin_top = 10
    margin_bottom = 30
    plot_w = width - margin_left - margin_right
    plot_h = height - margin_top - margin_bottom

    # Parse timestamps and scores
    parsed: list[tuple[datetime, float]] = []
    for entry in score_history:
        ts_raw = entry.get("scanned_at", "")
        score = float(str(entry.get("score", 0)))
        try:
            ts = datetime.fromisoformat(str(ts_raw))
        except ValueError, TypeError:
            continue
        parsed.append((ts, score))

    if not parsed:
        return (
            f'<svg xmlns="http://www.w3.org/2000/svg"'
            f' width="{width}" height="{height}"'
            f' viewBox="0 0 {width} {height}"></svg>'
        )

    parsed.sort(key=lambda p: p[0])
    ts_min = parsed[0][0]
    ts_max = parsed[-1][0]
    ts_range = (ts_max - ts_min).total_seconds()
    if ts_range == 0:
        ts_range = 1.0

    def x_pos(ts: datetime) -> float:
        return margin_left + (ts - ts_min).total_seconds() / ts_range * plot_w

    def y_pos(score: float) -> float:
        clamped = max(0.0, min(100.0, score))
        return margin_top + plot_h - clamped / 100.0 * plot_h

    parts: list[str] = []

    # Zone bands
    zones = [
        (0, 50, "#dc2626", 0.05),
        (50, 75, "#ea580c", 0.05),
        (75, 100, "#16a34a", 0.05),
    ]
    for low, high, col, opacity in zones:
        ry = y_pos(high)
        rh = y_pos(low) - ry
        parts.append(
            f'<rect x="{margin_left}" y="{ry:.1f}"'
            f' width="{plot_w}" height="{rh:.1f}"'
            f' fill="{col}" opacity="{opacity}"/>'
        )

    # Y-axis gridlines at 25% intervals
    for pct in (0, 25, 50, 75, 100):
        y = y_pos(pct)
        parts.append(
            f'<line x1="{margin_left}" y1="{y:.1f}"'
            f' x2="{margin_left + plot_w}" y2="{y:.1f}"'
            f' stroke="#e5e7eb" stroke-width="0.5"/>'
        )
        parts.append(
            f'<text x="{margin_left - 5}" y="{y:.1f}"'
            f' text-anchor="end" dominant-baseline="central"'
            f' font-size="10" fill="#6b7280">{pct}%</text>'
        )

    # Build line path data for area fill
    line_points: list[str] = []
    for ts, score in parsed:
        line_points.append(f"{x_pos(ts):.1f},{y_pos(score):.1f}")
    points_str = " ".join(line_points)

    # Area fill
    first_x = x_pos(parsed[0][0])
    last_x = x_pos(parsed[-1][0])
    baseline = y_pos(0)
    fill_points = (
        f"{first_x:.1f},{baseline:.1f} " + points_str + f" {last_x:.1f},{baseline:.1f}"
    )
    parts.append(f'<polygon points="{fill_points}" fill="#2563eb" opacity="0.15"/>')

    # Line
    parts.append(
        f'<polyline points="{points_str}"'
        f' fill="none" stroke="#2563eb" stroke-width="2"/>'
    )

    # Scan markers
    for ts, score in parsed:
        cx = x_pos(ts)
        cy = y_pos(score)
        parts.append(
            f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="3"'
            f' fill="#2563eb" stroke="#ffffff" stroke-width="1">'
            f"<title>{score:.1f}% — {ts.strftime('%Y-%m-%d')}</title>"
            f"</circle>"
        )

    # Drift events
    if show_events and drift_events:
        for evt in drift_events:
            det_raw = evt.get("detected_at", "")
            impact = float(str(evt.get("score_impact", 0)))
            try:
                det_ts = datetime.fromisoformat(str(det_raw))
            except ValueError, TypeError:
                continue
            if det_ts < ts_min or det_ts > ts_max:
                continue
            cx = x_pos(det_ts)
            r = max(3.0, min(8.0, abs(impact) * 0.5))
            # Place drift dot below the score line
            cy = y_pos(0) - 10
            parts.append(
                f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{r:.1f}"'
                f' fill="#dc2626" opacity="0.8">'
                f"<title>Drift: {impact:+.1f}pp — "
                f"{det_ts.strftime('%Y-%m-%d')}</title>"
                f"</circle>"
            )

    # X-axis labels (monthly)
    current = ts_min.replace(day=1)
    while current <= ts_max:
        if current >= ts_min:
            lx = x_pos(current)
            parts.append(
                f'<text x="{lx:.1f}"'
                f' y="{margin_top + plot_h + 18}"'
                f' text-anchor="middle" font-size="10"'
                f' fill="#6b7280">{current.strftime("%b")}</text>'
            )
        # Advance to next month
        if current.month == 12:
            current = current.replace(year=current.year + 1, month=1)
        else:
            current = current.replace(month=current.month + 1)

    return (
        f'<svg xmlns="http://www.w3.org/2000/svg"'
        f' width="{width}" height="{height}"'
        f' viewBox="0 0 {width} {height}" role="img"'
        f' aria-label="Score trend chart">' + "".join(parts) + "</svg>"
    )
