"""Thread-safe play queue with prefetch."""

from __future__ import annotations

import threading
from collections import deque
from dataclasses import dataclass

from .log import get_logger
from .resolve.ytdlp import ResolvedURL, resolve_source

log = get_logger("queue")


@dataclass
class QueueItem:
    """A queued item — either a URL or a capture source."""
    url: str | None = None                      # URL to resolve via yt-dlp
    capture: str | None = None                   # "screen" | "window" | "webcam" | "browser"
    window_title: str | None = None              # for capture="window"
    duration: float | None = None                # per-item duration limit
    show_placeholder: bool = True                # per-item placeholder toggle
    title: str | None = None                     # override title (for display)
    cursor: bool = True                          # show cursor in screen/window capture


class PlayQueue:
    """Thread-safe queue with background prefetch.

    Items are added as raw URL strings or QueueItem objects.
    URL items are resolved via yt-dlp; capture items pass through directly.
    """

    def __init__(self, loop: bool = False, cookies_from_browser: str | None = None) -> None:
        self._pending: deque[QueueItem] = deque()
        self._resolved: deque[ResolvedURL] = deque()  # ready to play
        self._all_items: list[QueueItem] = []         # original items for looping
        self._lock = threading.Lock()
        self._item_available = threading.Condition(self._lock)
        self._prefetch_thread: threading.Thread | None = None
        self._prefetching = False
        self._closed = False
        self._loop = loop
        self._loop_count: int = 0
        self._cookies_from_browser = cookies_from_browser

    def add(self, url: str) -> None:
        """Add a raw URL to the queue (backward-compatible)."""
        self.add_item(QueueItem(url=url))

    def add_item(self, item: QueueItem) -> None:
        """Add a QueueItem to the queue."""
        with self._item_available:
            self._pending.append(item)
            self._all_items.append(item)
            label = item.url or item.capture or "item"
            log.info("Queued: %s (%d pending)", label, len(self._pending))
            self._item_available.notify_all()

    def next(self) -> ResolvedURL | None:
        """Get the next resolved item. Blocks until available. Returns None when done."""
        qi: QueueItem | None = None
        with self._item_available:
            # Wait until we have a resolved item, a pending item to resolve, or are closed
            # Stay alive while a prefetch is in-flight — it will deliver a resolved item
            while not self._resolved and not self._pending:
                if self._prefetching:
                    log.debug("Waiting for prefetch to complete...")
                if self._closed and not self._prefetching:
                    if self._loop and self._all_items:
                        self._pending.extend(self._all_items)
                        self._loop_count += 1
                        log.info("Looping queue (%d items, loop %d)", len(self._pending), self._loop_count)
                        continue
                    break
                self._item_available.wait()

            if self._resolved:
                item = self._resolved.popleft()
                log.info("Dequeued: %s", item.title)
                return item

            if self._pending:
                qi = self._pending.popleft()

        # Resolve outside the lock (this is slow)
        if qi is not None:
            return self._resolve_item(qi)

        return None

    def _resolve_item(self, qi: QueueItem) -> ResolvedURL:
        """Resolve a QueueItem into a ResolvedURL."""
        if qi.capture:
            # Capture items don't need yt-dlp resolution
            # Browser items pass their URL through source_urls
            source_urls = [qi.url] if qi.capture == "browser" and qi.url else []
            return ResolvedURL(
                title=qi.title or qi.capture.title(),
                duration=qi.duration,
                is_live=False,
                source_urls=source_urls,
                capture=qi.capture,
                window_title=qi.window_title,
                show_placeholder=qi.show_placeholder,
                cursor=qi.cursor,
            )

        assert qi.url is not None
        resolved = resolve_source(
            qi.url,
            duration=qi.duration,
            title=qi.title,
            cookies_from_browser=self._cookies_from_browser,
        )
        resolved.show_placeholder = qi.show_placeholder
        # Cache the resolved title for display in queue status
        if not qi.title and resolved.title:
            qi.title = resolved.title
        return resolved

    def resolve_next(self) -> ResolvedURL | None:
        """Resolve the next pending item synchronously (for eager first-item resolution)."""
        with self._lock:
            if not self._pending:
                return None
            qi = self._pending.popleft()

        resolved = self._resolve_item(qi)
        with self._item_available:
            self._resolved.append(resolved)
            self._item_available.notify_all()
        log.info("Resolved: %s", resolved.title)
        return resolved

    def start_prefetch(self) -> None:
        """Start background resolution of the next pending item."""
        with self._lock:
            if not self._pending or self._resolved:
                return
            # Skip capture items (nothing to prefetch)
            if self._pending[0].capture:
                return
            # Don't start another if one is already running
            if self._prefetch_thread and self._prefetch_thread.is_alive():
                return

        self._prefetch_thread = threading.Thread(target=self._prefetch, daemon=True)
        self._prefetch_thread.start()

    def _prefetch(self) -> None:
        with self._lock:
            if not self._pending:
                return
            qi = self._pending.popleft()
            self._prefetching = True

        try:
            resolved = self._resolve_item(qi)
            with self._item_available:
                self._resolved.append(resolved)
                self._item_available.notify_all()
            log.info("Prefetched: %s", resolved.title)
        except Exception:
            log.exception("Prefetch failed for %s", qi.url or qi.capture)
        finally:
            with self._item_available:
                self._prefetching = False
                self._item_available.notify_all()

    def remove(self, index: int) -> str | None:
        """Remove an item by rotation index. Returns a label or None."""
        with self._lock:
            if self._loop and self._all_items:
                if 0 <= index < len(self._all_items):
                    qi = self._all_items[index]
                    del self._all_items[index]
                    # Also remove from pending if present
                    try:
                        self._pending.remove(qi)
                    except ValueError:
                        pass
                    return qi.title or qi.url or qi.capture
            else:
                if 0 <= index < len(self._pending):
                    qi = self._pending[index]
                    del self._pending[index]
                    return qi.title or qi.url or qi.capture
        return None

    def close(self) -> None:
        """Signal no more items will be added."""
        with self._item_available:
            self._closed = True
            self._item_available.notify_all()

    @property
    def loop_count(self) -> int:
        """Number of times the queue has looped back to the beginning."""
        return self._loop_count

    def set_loop(self, enabled: bool) -> None:
        """Enable or disable queue looping."""
        self._loop = enabled

    @property
    def has_capture_items(self) -> bool:
        """True if any queued item is a capture source."""
        return any(qi.capture for qi in self._all_items)

    def pending_labels(self) -> list[str]:
        """Return labels for all pending items (thread-safe)."""
        with self._lock:
            return [qi.url or qi.title or qi.capture or "item" for qi in self._pending]

    def is_next_ready(self) -> bool:
        """True if next() would return quickly (resolved item or instant-resolve capture)."""
        with self._lock:
            if self._resolved:
                return True
            if self._pending and self._pending[0].capture:
                return True
            return False

    def has_more(self) -> bool:
        """True if more items may become available (pending, prefetching, or loop)."""
        with self._lock:
            if self._resolved or self._pending or self._prefetching:
                return True
            if self._loop and self._all_items and self._closed:
                return True
            return False

    def peek_next_label(self) -> str | None:
        """Best-effort label for the next item (for placeholder text)."""
        with self._lock:
            if self._resolved:
                return self._resolved[0].title
            if self._pending:
                qi = self._pending[0]
                return qi.title or qi.url or qi.capture
            # Loop case: next item will be first in _all_items
            if self._loop and self._all_items:
                qi = self._all_items[0]
                return qi.title or qi.url or qi.capture
            return None

    def peek_next(self) -> tuple[int, str | None]:
        """Return (remaining_count, next_resolved_title) under lock.

        Only returns a title if the next item has been resolved (has a real
        title from yt-dlp). Unresolved pending items are counted but not named.
        """
        with self._lock:
            count = len(self._pending) + len(self._resolved)
            title = self._resolved[0].title if self._resolved else None
            return count, title

    @staticmethod
    def _item_label(qi: QueueItem) -> str:
        label = qi.title or qi.url or qi.capture or "item"
        if qi.duration:
            mins, secs = divmod(int(qi.duration), 60)
            label += f"  ({mins}:{secs:02d})"
        return label

    def rotation_labels(self, current_title: str | None = None) -> list[str]:
        """Return item labels in play order.

        In repeat mode: full _all_items list, rotated so the item after
        the currently playing one is first.
        In non-repeat mode: pending items only.
        """
        with self._lock:
            if not self._loop:
                return [self._item_label(qi) for qi in self._pending]

            if not self._all_items:
                return []

            labels = [self._item_label(qi) for qi in self._all_items]

            if not current_title:
                return labels

            # Find current item and rotate so next-up is index 0
            current_idx = -1
            for i, qi in enumerate(self._all_items):
                if qi.title == current_title:
                    current_idx = i
                    break

            if current_idx < 0:
                return labels

            n = len(labels)
            start = (current_idx + 1) % n
            return labels[start:] + labels[:start]

    @property
    def status(self) -> str:
        with self._lock:
            return (
                f"Queue: {len(self._resolved)} resolved, "
                f"{len(self._pending)} pending"
            )
