from __future__ import annotations

import math
from typing import Dict, List

from .simulator import Snapshot, build_network


def animate_history(
    history: List[Snapshot], profile_name: str, output_file: str, fps: int
) -> str:
    try:
        import matplotlib.pyplot as plt
        import numpy as np
        from matplotlib import patches
        from matplotlib.animation import FuncAnimation
    except ImportError as exc:
        raise RuntimeError(
            "Install animation deps: pip install matplotlib pillow"
        ) from exc

    if not history:
        raise ValueError("No history recorded")

    fig, ax = plt.subplots(figsize=(11.2, 6.4))
    fig.patch.set_facecolor("#f7f7f2")
    ax.set_facecolor("#f7f7f2")
    ax.set_xlim(-4.4, 4.4)
    ax.set_ylim(-2.8, 2.8)
    ax.set_aspect("equal")
    ax.set_axis_off()

    ring_centers = {
        0: (-3.0, 1.0),
        1: (0.0, 1.4),
        2: (-1.5, -1.0),
        3: (3.0, 0.8),
        4: (1.5, -1.2),
    }
    fallback_centers = [
        (-3.0, 1.2),
        (0.0, 1.4),
        (3.0, 1.2),
        (-1.5, -1.2),
        (1.5, -1.2),
        (0.0, -0.2),
    ]
    ring_ids = sorted(history[0].rings)
    for idx, rb_id in enumerate(ring_ids):
        if rb_id not in ring_centers:
            ring_centers[rb_id] = fallback_centers[idx % len(fallback_centers)]

    ring_radius = 0.78
    inner_radius = 0.30
    ring_scatters: Dict[int, object] = {}

    for rb_id in ring_ids:
        cx, cy = ring_centers[rb_id]
        outer = patches.Circle(
            (cx, cy),
            radius=ring_radius,
            fill=False,
            linewidth=2.2,
            edgecolor="#4b5563",
        )
        inner = patches.Circle(
            (cx, cy),
            radius=inner_radius,
            color="#d1d5db",
            alpha=0.65,
            linewidth=0,
        )
        ax.add_patch(outer)
        ax.add_patch(inner)
        ax.text(
            cx,
            cy,
            f"RB{rb_id}",
            ha="center",
            va="center",
            fontsize=9,
            color="#374151",
            weight="bold",
        )
        scatter = ax.scatter([], [], s=165, edgecolors="#111827", linewidths=0.8)
        ring_scatters[rb_id] = scatter

    arrow_style = dict(arrowstyle="->", color="#9ca3af", lw=1.4)
    rbs_topo, _ = build_network()
    flow_edges_set: set = set()
    for src_id, rb in enumerate(rbs_topo):
        for ex in rb.exits:
            if ex.dst_roundabout is not None:
                flow_edges_set.add((src_id, ex.dst_roundabout))
    flow_edges = sorted(flow_edges_set)
    for src, dst in flow_edges:
        if src not in ring_centers or dst not in ring_centers:
            continue
        x0, y0 = ring_centers[src]
        x1, y1 = ring_centers[dst]
        dx = x1 - x0
        dy = y1 - y0
        norm = math.hypot(dx, dy) or 1.0
        ux = dx / norm
        uy = dy / norm
        start = (x0 + ux * ring_radius, y0 + uy * ring_radius)
        end = (x1 - ux * ring_radius, y1 - uy * ring_radius)
        ax.annotate("", xy=end, xytext=start, arrowprops=arrow_style)

    title = ax.text(
        0.02,
        0.98,
        "",
        va="top",
        ha="left",
        transform=ax.transAxes,
        fontsize=12,
        color="#1f2937",
        weight="bold",
    )
    queue_text = ax.text(
        0.02,
        0.06,
        "",
        va="bottom",
        ha="left",
        transform=ax.transAxes,
        fontsize=10,
        family="monospace",
        color="#374151",
    )
    sink_text = ax.text(
        0.52,
        0.06,
        "",
        va="bottom",
        ha="left",
        transform=ax.transAxes,
        fontsize=10,
        family="monospace",
        color="#1f2937",
    )

    def vehicle_color(vid: int) -> str:
        palette = [
            "#0f766e",
            "#b45309",
            "#334155",
            "#be123c",
            "#1d4ed8",
            "#4d7c0f",
            "#7c3aed",
            "#0369a1",
            "#a16207",
            "#166534",
        ]
        return palette[vid % len(palette)]

    def update(i: int):
        snapshot = history[i]
        title.set_text(
            f"Roundabout Network | profile={profile_name} | step={snapshot.step}"
        )

        queue_preview = snapshot.queue[:10]
        queue_tail = "..." if len(snapshot.queue) > 10 else ""
        queue_text.set_text(
            f"queue({len(snapshot.queue)}): {queue_preview}{queue_tail}"
        )

        sink_preview = snapshot.sink[-12:]
        sink_text.set_text(f"sink({len(snapshot.sink)}): {sink_preview}")

        artists = [title, queue_text, sink_text]
        for rb_id in ring_ids:
            cells = snapshot.rings[rb_id]
            cx, cy = ring_centers[rb_id]
            points: List[List[float]] = []
            colors: List[str] = []
            length = len(cells)
            for cell_idx, vid in enumerate(cells):
                if vid is None:
                    continue
                angle = (2.0 * math.pi * cell_idx / length) - (math.pi / 2.0)
                x = cx + ring_radius * math.cos(angle)
                y = cy + ring_radius * math.sin(angle)
                points.append([x, y])
                colors.append(vehicle_color(vid))
            scatter = ring_scatters[rb_id]
            if points:
                scatter.set_offsets(np.array(points))
                scatter.set_facecolors(colors)
            else:
                scatter.set_offsets(np.empty((0, 2)))
                scatter.set_facecolors([])
            artists.append(scatter)
        return artists

    animation = FuncAnimation(
        fig,
        update,
        frames=len(history),
        interval=max(40, int(1000 / max(1, fps))),
        blit=True,
        repeat=False,
    )
    animation.save(output_file, writer="pillow", fps=fps)
    plt.close(fig)
    return output_file
