"""Interactive playback of recorder traces."""

from __future__ import annotations

from dataclasses import dataclass
import json
import os
import subprocess
import sys
import tempfile
import webbrowser
from pathlib import Path
from typing import Any

from .api.build import (
    SuspiciousOffsetEncodingError,
    _apply_edit,
    _normalize_newlines,
    _ordered_edit_events,
    _uses_crlf_offsets,
)
from .api.document import filter_events_by_document_with_rename_handling
from .api.load import filter_edit_events
from .timeutil import parse_timestamp


@dataclass(frozen=True)
class PlaybackFrame:
    """Document snapshot after a number of edits have been applied."""

    applied_events: int
    content: str
    event: dict[str, Any] | None


@dataclass(frozen=True)
class PlaybackTimeline:
    """Precomputed states and ordered events for playback navigation."""

    document: str | None
    frames: tuple[PlaybackFrame, ...]
    events: tuple[dict[str, Any], ...]

    @property
    def total_events(self) -> int:
        return len(self.events)


def _advance_state(
    current_state: str,
    event: dict[str, Any],
    *,
    crlf_mode: bool,
) -> str:
    old_fragment = _normalize_newlines(str(event.get("oldFragment", "")))
    new_fragment = _normalize_newlines(str(event.get("newFragment", "")))

    try:
        offset = int(event.get("offset", 0) or 0)
    except (TypeError, ValueError):
        offset = 0

    if old_fragment == new_fragment and offset == 0:
        return old_fragment or current_state
    if old_fragment == new_fragment:
        return current_state

    try:
        return _apply_edit(
            current_state,
            old_fragment=old_fragment,
            new_fragment=new_fragment,
            offset=offset,
            window=200,
            utf16_mode=False,
            crlf_mode=crlf_mode,
        )
    except ValueError as exc:
        if isinstance(exc, SuspiciousOffsetEncodingError):
            raise
        return current_state


def build_playback_timeline(
    json_data: tuple[dict[str, Any], ...],
    document: str | None,
    template: str,
) -> PlaybackTimeline:
    """Build a replay timeline so playback can move forward and backward."""
    edit_events = tuple(filter_edit_events(json_data))
    doc_events = tuple(
        filter_events_by_document_with_rename_handling(edit_events, document)
    )
    ordered_events = tuple(_ordered_edit_events(doc_events))
    crlf_mode = _uses_crlf_offsets(ordered_events)

    current_state = _normalize_newlines(template)
    frames = [PlaybackFrame(applied_events=0, content=current_state, event=None)]

    for event_index, event in enumerate(ordered_events, start=1):
        current_state = _advance_state(current_state, event, crlf_mode=crlf_mode)
        frames.append(
            PlaybackFrame(
                applied_events=event_index,
                content=current_state,
                event=event,
            )
        )

    return PlaybackTimeline(
        document=document,
        frames=tuple(frames),
        events=ordered_events,
    )


def _validate_start_event(start_event: int, total_events: int) -> None:
    if start_event < 0 or start_event > total_events:
        raise ValueError(
            f"Invalid start_event: {start_event} (valid range: 0-{total_events})"
        )


def _playback_delay_seconds(
    events: tuple[dict[str, Any], ...],
    current_index: int,
    speed: float,
) -> float:
    safe_speed = max(speed, 0.1)
    if current_index <= 0:
        return 0.5 / safe_speed
    if current_index >= len(events):
        return 0.0

    previous_timestamp = events[current_index - 1].get("timestamp")
    next_timestamp = events[current_index].get("timestamp")
    if previous_timestamp and next_timestamp:
        try:
            delay = (
                parse_timestamp(str(next_timestamp))
                - parse_timestamp(str(previous_timestamp))
            ).total_seconds()
            return min(max(delay / safe_speed, 0.0), 2.0)
        except ValueError:
            pass

    return 0.05 / safe_speed


def _summarize_fragment(fragment: str, limit: int = 80) -> str:
    text = _normalize_newlines(fragment).strip("\n")
    if not text:
        return "<empty>"

    condensed = text.replace("\n", "\\n")
    if len(condensed) <= limit:
        return condensed
    return f"{condensed[: max(limit - 3, 1)]}..."


def _describe_event(event: dict[str, Any]) -> str:
    old_fragment = _normalize_newlines(str(event.get("oldFragment", "")))
    new_fragment = _normalize_newlines(str(event.get("newFragment", "")))
    timestamp = str(event.get("timestamp", "unknown"))

    try:
        offset = int(event.get("offset", 0) or 0)
    except (TypeError, ValueError):
        offset = 0

    if old_fragment == new_fragment and offset == 0:
        change = f"snapshot load ({len(new_fragment)} chars)"
    elif not old_fragment:
        change = f"insert {len(new_fragment)} chars"
    elif not new_fragment:
        change = f"delete {len(old_fragment)} chars"
    else:
        change = f"replace {len(old_fragment)} with {len(new_fragment)} chars"

    return (
        f"{timestamp} | offset {offset} | {change} | "
        f"old: {_summarize_fragment(old_fragment)} | "
        f"new: {_summarize_fragment(new_fragment)}"
    )


class _PlaybackViewer:
    """Browser-based viewer payload writer."""

    def __init__(
        self,
        timeline: PlaybackTimeline,
        *,
        speed: float,
        start_event: int,
    ) -> None:
        safe_speed = max(speed, 0.1)
        self._payload = {
            "document": timeline.document or "recording",
            "startEvent": start_event,
            "speed": safe_speed,
            "totalEvents": timeline.total_events,
            "delaysMs": [
                max(int(_playback_delay_seconds(timeline.events, index, safe_speed) * 1000), 1)
                for index in range(timeline.total_events)
            ],
            "frames": [
                {
                    "appliedEvents": frame.applied_events,
                    "content": frame.content,
                    "description": (
                        "Template state before the first edit."
                        if frame.event is None
                        else _describe_event(frame.event)
                    ),
                }
                for frame in timeline.frames
            ],
        }

    def run(self) -> None:
        output_path = self._write_html_file()
        if self._open_in_browser(output_path):
            return

        print(
            "Warning: Unable to open the playback viewer automatically. "
            f"Open this file manually: {output_path}",
            file=sys.stderr,
        )

    def _write_html_file(self) -> Path:
        with tempfile.NamedTemporaryFile(
            mode="w",
            suffix=".html",
            prefix="cr_proc_playback_",
            delete=False,
            encoding="utf-8",
        ) as handle:
            handle.write(self._render_html())
            return Path(handle.name)

    def _render_html(self) -> str:
        payload_json = json.dumps(self._payload).replace("</", "<\\/")
        return f"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>cr_proc Playback</title>
  <style>
    :root {{
      color-scheme: light;
      --bg: #f4f0e8;
      --panel: rgba(255, 252, 247, 0.92);
      --ink: #14213d;
      --muted: #5e6a73;
      --accent: #d26a3d;
      --accent-strong: #8f2d13;
      --border: rgba(20, 33, 61, 0.14);
      --shadow: 0 18px 48px rgba(20, 33, 61, 0.16);
      --code-bg: #fbfaf7;
    }}

    * {{
      box-sizing: border-box;
    }}

    body {{
      margin: 0;
      min-height: 100vh;
      background:
        radial-gradient(circle at top left, rgba(210, 106, 61, 0.18), transparent 34%),
        linear-gradient(155deg, #f5efe4 0%, #efe7da 48%, #f8f4eb 100%);
      color: var(--ink);
      font-family: "Avenir Next", "Segoe UI", sans-serif;
    }}

    .shell {{
      width: min(1200px, calc(100vw - 32px));
      margin: 16px auto;
      padding: 18px;
      border: 1px solid var(--border);
      border-radius: 24px;
      background: var(--panel);
      box-shadow: var(--shadow);
      backdrop-filter: blur(10px);
    }}

    .header {{
      display: grid;
      gap: 14px;
      margin-bottom: 16px;
    }}

    .status {{
      font-size: 1rem;
      font-weight: 700;
      letter-spacing: 0.02em;
    }}

    .meta {{
      color: var(--muted);
      line-height: 1.45;
    }}

    .controls {{
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      align-items: center;
    }}

    button {{
      border: 1px solid rgba(143, 45, 19, 0.16);
      background: #fff7f1;
      color: var(--accent-strong);
      border-radius: 999px;
      padding: 10px 16px;
      font: inherit;
      font-weight: 700;
      cursor: pointer;
      transition: transform 120ms ease, background 120ms ease, box-shadow 120ms ease;
    }}

    button:hover {{
      transform: translateY(-1px);
      background: #fff1e7;
      box-shadow: 0 10px 20px rgba(143, 45, 19, 0.12);
    }}

    button:active {{
      transform: translateY(0);
    }}

    .hint {{
      color: var(--muted);
      font-size: 0.95rem;
    }}

    .editor {{
      min-height: min(72vh, 900px);
      border: 1px solid var(--border);
      border-radius: 18px;
      background: var(--code-bg);
      overflow: auto;
    }}

    pre {{
      margin: 0;
      min-height: 100%;
      padding: 18px 20px 28px;
      color: #1c2329;
      font: 14px/1.5 "SFMono-Regular", Menlo, Consolas, monospace;
      white-space: pre;
    }}

    @media (max-width: 800px) {{
      .shell {{
        width: calc(100vw - 18px);
        margin: 9px auto;
        padding: 12px;
        border-radius: 18px;
      }}

      .editor {{
        min-height: 68vh;
      }}
    }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="header">
      <div class="status" id="status"></div>
      <div class="controls">
        <button id="start" type="button">|&lt;</button>
        <button id="prev" type="button">&lt;</button>
        <button id="play" type="button">Pause</button>
        <button id="next" type="button">&gt;</button>
        <button id="end" type="button">&gt;|</button>
        <span class="hint" id="speed"></span>
      </div>
      <div class="meta" id="event"></div>
      <div class="hint">Keys: Left/Right step, Space play/pause, Home/End jump</div>
    </section>
    <section class="editor" id="editor">
      <pre id="code"></pre>
    </section>
  </main>

  <script>
    const playback = {payload_json};

    const statusEl = document.getElementById("status");
    const eventEl = document.getElementById("event");
    const codeEl = document.getElementById("code");
    const editorEl = document.getElementById("editor");
    const playButton = document.getElementById("play");
    const speedEl = document.getElementById("speed");

    let currentIndex = playback.startEvent;
    let playing = currentIndex < playback.totalEvents;
    let timerId = null;

    function cancelScheduledStep() {{
      if (timerId === null) {{
        return;
      }}
      window.clearTimeout(timerId);
      timerId = null;
    }}

    function renderFrame() {{
      const frame = playback.frames[currentIndex];
      const scrollTop = editorEl.scrollTop;
      const scrollLeft = editorEl.scrollLeft;

      statusEl.textContent = `${{playback.document}}    Applied events: ${{frame.appliedEvents}}/${{playback.totalEvents}}`;
      eventEl.textContent = frame.description;
      codeEl.textContent = frame.content;
      speedEl.textContent = `Speed: ${{playback.speed}}x`;
      playButton.textContent = playing ? "Pause" : "Play";

      editorEl.scrollTop = scrollTop;
      editorEl.scrollLeft = scrollLeft;
      document.title = `cr_proc Playback - ${{playback.document}}`;
    }}

    function scheduleNextFrame() {{
      cancelScheduledStep();
      if (!playing || currentIndex >= playback.totalEvents) {{
        playing = false;
        renderFrame();
        return;
      }}

      const delayMs = playback.delaysMs[currentIndex] ?? 50;
      timerId = window.setTimeout(() => {{
        timerId = null;
        if (!playing) {{
          return;
        }}
        currentIndex = Math.min(currentIndex + 1, playback.totalEvents);
        renderFrame();
        scheduleNextFrame();
      }}, delayMs);
    }}

    function setPlaying(nextPlaying) {{
      playing = Boolean(nextPlaying) && currentIndex < playback.totalEvents;
      renderFrame();
      if (playing) {{
        scheduleNextFrame();
      }} else {{
        cancelScheduledStep();
      }}
    }}

    function stepBackward() {{
      cancelScheduledStep();
      playing = false;
      currentIndex = Math.max(currentIndex - 1, 0);
      renderFrame();
    }}

    function stepForward() {{
      cancelScheduledStep();
      playing = false;
      currentIndex = Math.min(currentIndex + 1, playback.totalEvents);
      renderFrame();
    }}

    function jumpToStart() {{
      cancelScheduledStep();
      playing = false;
      currentIndex = 0;
      renderFrame();
    }}

    function jumpToEnd() {{
      cancelScheduledStep();
      playing = false;
      currentIndex = playback.totalEvents;
      renderFrame();
    }}

    document.getElementById("start").addEventListener("click", jumpToStart);
    document.getElementById("prev").addEventListener("click", stepBackward);
    document.getElementById("play").addEventListener("click", () => setPlaying(!playing));
    document.getElementById("next").addEventListener("click", stepForward);
    document.getElementById("end").addEventListener("click", jumpToEnd);

    document.addEventListener("keydown", (event) => {{
      if (event.key === "ArrowLeft") {{
        event.preventDefault();
        stepBackward();
      }} else if (event.key === "ArrowRight") {{
        event.preventDefault();
        stepForward();
      }} else if (event.key === " ") {{
        event.preventDefault();
        setPlaying(!playing);
      }} else if (event.key === "Home") {{
        event.preventDefault();
        jumpToStart();
      }} else if (event.key === "End") {{
        event.preventDefault();
        jumpToEnd();
      }}
    }});

    renderFrame();
    if (playing) {{
      scheduleNextFrame();
    }}
  </script>
</body>
</html>
"""

    def _open_in_browser(self, output_path: Path) -> bool:
        try:
            if webbrowser.open(output_path.as_uri(), new=1):
                return True
        except Exception:
            pass

        if hasattr(os, "startfile"):
            try:
                os.startfile(str(output_path))  # type: ignore[attr-defined]
                return True
            except OSError:
                pass

        command: list[str] | None = None
        if sys.platform == "darwin":
            command = ["open", str(output_path)]
        elif os.name != "nt":
            command = ["xdg-open", str(output_path)]

        if command is None:
            return False

        try:
            subprocess.run(
                command,
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
            return True
        except (OSError, subprocess.CalledProcessError):
            return False


def playback_recording(
    json_data: tuple[dict[str, Any], ...],
    document: str | None,
    template: str,
    speed: float = 1.0,
    start_event: int = 0,
) -> None:
    """Open a windowed playback viewer for a recording."""
    timeline = build_playback_timeline(json_data, document, template)
    if not timeline.events:
        raise ValueError(f"No events found for document: {document or '<unknown>'}")

    _validate_start_event(start_event, timeline.total_events)
    _PlaybackViewer(
        timeline,
        speed=speed,
        start_event=start_event,
    ).run()
