"""In-memory job registry for video renders.

Each call to ``POST /api/video`` (and each agent-tool invocation)
registers a ``VideoJobState`` in this module's process-level registry.
Callers poll ``GET /api/video/jobs/{id}`` for progress + result. State
lives in-process only — restarts lose job history, but each render's
output mp4 is the real durable artifact on disk, so "correctness" is
unaffected by lost job metadata.

Pattern modeled after plugins/bundled/onenote-import/jobs.py.
"""

from __future__ import annotations

import asyncio
import secrets
from collections import deque
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from typing import Any


def new_job_id() -> str:
    return secrets.token_urlsafe(6)


def _iso_now() -> str:
    return datetime.now(UTC).isoformat()


@dataclass
class VideoJobState:
    """Live state of one video render. Mutated by the engine as it works."""

    job_id: str
    state: str = "pending"        # pending | running | completed | failed | cancelled
    phase: str = ""               # "adapter" / "tts" / "recording" / "composing"
    started_at: str = ""
    finished_at: str = ""

    # Request parameters
    target: str = ""
    duration_seconds: int = 60
    voice: str = "liam"
    style: str = "walkthrough"

    # Originating conversation (for chat follow-up; empty if non-chat)
    origin_conversation_id: str = ""

    # Progress counters — updated live
    scenes_total: int = 0
    scenes_tts_done: int = 0
    scenes_recorded: int = 0

    # Result fields — populated on success
    path: str = ""
    duration_actual: float = 0.0
    narration_word_count: int = 0
    tts_provider_used: str = ""
    warnings: list[str] = field(default_factory=list)

    # Error
    error: str = ""

    def to_public_dict(self) -> dict[str, Any]:
        d = asdict(self)
        # Normalize mutable defaults
        d["warnings"] = list(self.warnings)
        # progress_ratio: 0.0 during adapter, 0.33 after tts, 0.75 after recording, 1.0 on completed
        if self.state == "completed":
            d["progress_ratio"] = 1.0
        elif self.state in ("failed", "cancelled"):
            d["progress_ratio"] = 0.0
        elif self.phase == "composing":
            d["progress_ratio"] = 0.9
        elif self.phase == "recording":
            d["progress_ratio"] = 0.75 if not self.scenes_total else 0.4 + 0.4 * (self.scenes_recorded / max(self.scenes_total, 1))
        elif self.phase == "tts":
            d["progress_ratio"] = 0.1 + 0.2 * (self.scenes_tts_done / max(self.scenes_total, 1))
        elif self.phase == "adapter":
            d["progress_ratio"] = 0.05
        else:
            d["progress_ratio"] = 0.0
        return d


class VideoJobRegistry:
    """Per-process job registry. Bounded at 50 finished jobs."""

    def __init__(self, *, max_finished: int = 50) -> None:
        self._jobs: dict[str, VideoJobState] = {}
        # FIFO of finished job_ids, oldest first. Bounded manually in finish()
        # because deque's auto-eviction drops the id but leaves _jobs behind.
        self._finished_order: deque[str] = deque()
        self._max_finished = max_finished
        self._lock = asyncio.Lock()

    async def register(self, job: VideoJobState) -> None:
        async with self._lock:
            self._jobs[job.job_id] = job

    async def update(self, job_id: str, **fields: Any) -> VideoJobState | None:
        async with self._lock:
            job = self._jobs.get(job_id)
            if job is None:
                return None
            for k, v in fields.items():
                if hasattr(job, k):
                    setattr(job, k, v)
            return job

    async def finish(self, job_id: str, *, state: str, **fields: Any) -> VideoJobState | None:
        async with self._lock:
            job = self._jobs.get(job_id)
            if job is None:
                return None
            job.state = state
            job.finished_at = _iso_now()
            for k, v in fields.items():
                if hasattr(job, k):
                    setattr(job, k, v)
            # Track finished-job LRU
            if job_id not in self._finished_order:
                self._finished_order.append(job_id)
            # Evict oldest finished beyond cap (retains running jobs always)
            while len(self._finished_order) > self._max_finished:
                old = self._finished_order.popleft()
                self._jobs.pop(old, None)
            return job

    async def get(self, job_id: str) -> VideoJobState | None:
        async with self._lock:
            return self._jobs.get(job_id)

    async def list(self, *, limit: int = 20) -> list[VideoJobState]:
        async with self._lock:
            # Running jobs first (newest), then finished (newest)
            running = [j for j in self._jobs.values() if j.state in ("pending", "running")]
            finished = [j for j in self._jobs.values() if j.state in ("completed", "failed", "cancelled")]
            running.sort(key=lambda j: j.started_at or j.job_id, reverse=True)
            finished.sort(key=lambda j: j.finished_at or j.job_id, reverse=True)
            return (running + finished)[:limit]


# Module-level singleton — initialized by main.py on startup
_registry: VideoJobRegistry | None = None


def get_registry() -> VideoJobRegistry:
    """Return the process-wide singleton, creating it lazily."""
    global _registry
    if _registry is None:
        _registry = VideoJobRegistry()
    return _registry
