from typing import Any, Dict, Sequence, List, Optional
import os
import sys
import json
import socket
import re
import subprocess
from urllib.parse import urlparse, parse_qs
from pathlib import Path
from cmdlet._shared import Cmdlet, CmdletArg, parse_cmdlet_args, resolve_tidal_manifest_path
from SYS.logger import debug, get_thread_stream, is_debug_enabled, set_debug, set_thread_stream
from SYS.result_table import ResultTable
from MPV.mpv_ipc import MPV
from SYS import pipeline as ctx
from SYS.models import PipeObject

from API.folder import LocalLibrarySearchOptimizer
from SYS.config import get_local_storage_path, get_hydrus_access_key, get_hydrus_url

_ALLDEBRID_UNLOCK_CACHE: Dict[str,
                              str] = {}


def _repo_root() -> Path:
    try:
        return Path(__file__).resolve().parent.parent
    except Exception:
        return Path(os.getcwd())


def _repo_log_dir() -> Path:
    d = _repo_root() / "Log"
    try:
        d.mkdir(parents=True, exist_ok=True)
    except Exception:
        pass
    return d


def _helper_log_file() -> Path:
    return _repo_log_dir() / "medeia-mpv-helper.log"


def _lua_log_file() -> Path:
    return _repo_log_dir() / "medeia-mpv-lua.log"


def _try_enable_mpv_file_logging(mpv_log_path: str, *, attempts: int = 3) -> bool:
    """Best-effort enable mpv log-file + verbose level on a running instance.

    Note: mpv may not honor changing log-file at runtime on all builds/platforms.
    We still try; if it fails, callers can fall back to restart-on-demand.
    """
    if not isinstance(mpv_log_path, str) or not mpv_log_path.strip():
        return False
    mpv_log_path = mpv_log_path.strip()

    try:
        Path(mpv_log_path).parent.mkdir(parents=True, exist_ok=True)
        with open(mpv_log_path, "a", encoding="utf-8", errors="replace"):
            pass
    except Exception:
        pass

    ok = False
    for _ in range(max(1, int(attempts))):
        try:
            # Try to set log-file and verbose level.
            r1 = _send_ipc_command(
                {
                    "command": ["set_property",
                                "options/log-file",
                                mpv_log_path]
                }
            )
            r2 = _send_ipc_command(
                {
                    "command": ["set_property",
                                "options/msg-level",
                                "all=v"]
                }
            )
            ok = bool(
                (r1 and r1.get("error") == "success")
                or (r2 and r2.get("error") == "success")
            )

            # Emit a predictable line so the file isn't empty if logging is active.
            _send_ipc_command(
                {
                    "command": ["print-text",
                                f"medeia: log enabled -> {mpv_log_path}"]
                },
                silent=True
            )
        except Exception:
            ok = False

        # If mpv has opened the log file, it should have content shortly.
        try:
            p = Path(mpv_log_path)
            if p.exists() and p.is_file() and p.stat().st_size > 0:
                return True
        except Exception:
            pass

        try:
            import time

            time.sleep(0.15)
        except Exception:
            break

    return bool(ok)


def _get_alldebrid_api_key(config: Optional[Dict[str, Any]]) -> Optional[str]:
    try:
        if not isinstance(config, dict):
            return None
        provider_cfg = config.get("provider")
        if not isinstance(provider_cfg, dict):
            return None
        ad_cfg = provider_cfg.get("alldebrid")
        if not isinstance(ad_cfg, dict):
            return None
        key = ad_cfg.get("api_key")
        if not isinstance(key, str):
            return None
        key = key.strip()
        return key or None
    except Exception:
        return None


def _is_alldebrid_protected_url(url: str) -> bool:
    try:
        if not isinstance(url, str):
            return False
        u = url.strip()
        if not u.startswith(("http://", "https://")):
            return False
        p = urlparse(u)
        host = (p.netloc or "").lower()
        path = p.path or ""
        # AllDebrid file page links (require auth; not directly streamable by mpv)
        return host == "alldebrid.com" and path.startswith("/f/")
    except Exception:
        return False


def _maybe_unlock_alldebrid_url(url: str, config: Optional[Dict[str, Any]]) -> str:
    """Convert AllDebrid protected file URLs into direct streamable links.

    When AllDebrid returns `https://alldebrid.com/f/...`, that URL typically requires
    authentication. MPV cannot access it without credentials. We transparently call
    the AllDebrid API `link/unlock` (using the configured API key) to obtain a direct
    URL that MPV can stream.
    """
    if not _is_alldebrid_protected_url(url):
        return url

    cached = _ALLDEBRID_UNLOCK_CACHE.get(url)
    if isinstance(cached, str) and cached:
        return cached

    api_key = _get_alldebrid_api_key(config)
    if not api_key:
        return url

    try:
        from API.alldebrid import AllDebridClient

        client = AllDebridClient(api_key)
        unlocked = client.unlock_link(url)
        if isinstance(unlocked, str) and unlocked.strip():
            unlocked = unlocked.strip()
            _ALLDEBRID_UNLOCK_CACHE[url] = unlocked
            return unlocked
    except Exception as e:
        debug(f"AllDebrid unlock failed for MPV target: {e}", file=sys.stderr)

    return url


def _ensure_lyric_overlay(mpv: MPV) -> None:
    try:
        mpv.ensure_lyric_loader_running()
    except Exception:
        pass


def _send_ipc_command(command: Dict[str, Any], silent: bool = False) -> Optional[Any]:
    """Send a command to the MPV IPC pipe and return the response."""
    try:
        mpv = MPV()
        return mpv.send(command, silent=silent)
    except Exception as e:
        if not silent:
            debug(f"IPC Error: {e}", file=sys.stderr)
        return None


def _extract_store_and_hash(item: Any) -> tuple[Optional[str], Optional[str]]:
    store: Optional[str] = None
    file_hash: Optional[str] = None

    try:
        if isinstance(item, dict):
            store = item.get("store")
            file_hash = item.get("hash") or item.get("file_hash")
        else:
            store = getattr(item, "store", None)
            file_hash = getattr(item, "hash", None) or getattr(item, "file_hash", None)
    except Exception:
        store = None
        file_hash = None

    try:
        store = str(store).strip() if store else None
    except Exception:
        store = None

    try:
        file_hash = str(file_hash).strip().lower() if file_hash else None
    except Exception:
        file_hash = None

    if not file_hash:
        try:
            text = None
            if isinstance(item, dict):
                text = item.get("path") or item.get("url") or item.get("filename")
            else:
                text = getattr(item, "path", None) or getattr(item, "url", None)
            if text:
                m = re.search(r"[0-9a-f]{64}", str(text).lower())
                if m:
                    file_hash = m.group(0)
        except Exception:
            pass

    return store, file_hash


def _set_mpv_item_context(store: Optional[str], file_hash: Optional[str]) -> None:
    # Properties consumed by MPV.lyric
    try:
        _send_ipc_command(
            {
                "command": ["set_property", "user-data/medeia-item-store", store or ""],
                "request_id": 901,
            },
            silent=True,
        )
        _send_ipc_command(
            {
                "command": ["set_property", "user-data/medeia-item-hash", file_hash or ""],
                "request_id": 902,
            },
            silent=True,
        )
    except Exception:
        pass


def _get_playlist(silent: bool = False) -> Optional[List[Dict[str, Any]]]:
    """Get the current playlist from MPV. Returns None if MPV is not running."""
    cmd = {
        "command": ["get_property",
                    "playlist"],
        "request_id": 100
    }
    resp = _send_ipc_command(cmd, silent=silent)
    if resp is None:
        return None
    if resp.get("error") == "success":
        return resp.get("data", [])
    return []


def _extract_title_from_item(item: Dict[str, Any]) -> str:
    """Extract a clean title from an MPV playlist item, handling memory:// M3U hacks."""
    title = item.get("title")
    filename = item.get("filename") or ""

    # Special handling for memory:// M3U playlists (used to pass titles via IPC)
    if "memory://" in filename and "#EXTINF:" in filename:
        try:
            # Extract title from #EXTINF:-1,Title
            # Use regex to find title between #EXTINF:-1, and newline
            match = re.search(r"#EXTINF:-1,(.*?)(?:\n|\r|$)", filename)
            if match:
                extracted_title = match.group(1).strip()
                if not title or title == "memory://":
                    title = extracted_title

            # If we still don't have a title, try to find the URL in the M3U content
            if not title:
                lines = filename.splitlines()
                for line in lines:
                    line = line.strip()
                    if line and not line.startswith("#") and not line.startswith(
                            "memory://"):
                        # Found the URL, use it as title
                        return line
        except Exception:
            pass

    return title or filename or "Unknown"


def _extract_target_from_memory_uri(text: str) -> Optional[str]:
    """Extract the real target URL/path from a memory:// M3U payload."""
    if not isinstance(text, str) or not text.startswith("memory://"):
        return None
    for line in text.splitlines():
        line = line.strip()
        if not line or line.startswith("#") or line.startswith("memory://"):
            continue
        return line
    return None


def _find_hydrus_instance_for_hash(hash_str: str, file_storage: Any) -> Optional[str]:
    """Find which Hydrus instance serves a specific file hash.

    Args:
        hash_str: SHA256 hash (64 hex chars)
        file_storage: FileStorage instance with Hydrus backends

    Returns:
        Instance name (e.g., 'home') or None if not found
    """
    # Query each Hydrus backend to see if it has this file
    for backend_name in file_storage.list_backends():
        backend = file_storage[backend_name]
        # Check if this is a Hydrus backend by checking class name
        backend_class = type(backend).__name__
        if backend_class != "HydrusNetwork":
            continue

        try:
            # Query metadata to see if this instance has the file
            metadata = backend.get_metadata(hash_str)
            if metadata:
                return backend_name
        except Exception:
            # This instance doesn't have the file or had an error
            continue

    return None


def _find_hydrus_instance_by_url(url: str, file_storage: Any) -> Optional[str]:
    """Find which Hydrus instance matches a given URL.

    Args:
        url: Full URL (e.g., http://localhost:45869/get_files/file?hash=...)
        file_storage: FileStorage instance with Hydrus backends

    Returns:
        Instance name (e.g., 'home') or None if not found
    """
    from urllib.parse import urlparse

    parsed_target = urlparse(url)
    target_netloc = parsed_target.netloc.lower()

    # Check each Hydrus backend's URL
    for backend_name in file_storage.list_backends():
        backend = file_storage[backend_name]
        backend_class = type(backend).__name__
        if backend_class != "HydrusNetwork":
            continue

        # Get the backend's base URL from its client
        try:
            backend_url = backend._client.base_url
            parsed_backend = urlparse(backend_url)
            backend_netloc = parsed_backend.netloc.lower()

            # Match by netloc (host:port)
            if target_netloc == backend_netloc:
                return backend_name
        except Exception:
            continue

    return None


def _normalize_playlist_path(text: Optional[str]) -> Optional[str]:
    """Normalize playlist entry paths for dedupe comparisons."""
    if not text:
        return None
    real = _extract_target_from_memory_uri(text) or text
    real = real.strip()
    if not real:
        return None
    # If it's already a bare hydrus hash, use it directly
    lower_real = real.lower()
    if re.fullmatch(r"[0-9a-f]{64}", lower_real):
        return lower_real

    # If it's a hydrus file URL, normalize to the hash for dedupe
    try:
        parsed = urlparse(real)
        if parsed.scheme in {"http",
                             "https",
                             "hydrus"}:
            if parsed.path.endswith("/get_files/file"):
                qs = parse_qs(parsed.query)
                h = qs.get("hash", [None])[0]
                if h and re.fullmatch(r"[0-9a-f]{64}", h.lower()):
                    return h.lower()
    except Exception:
        pass

    # Normalize slashes for Windows paths and lowercase for comparison
    real = real.replace("\\", "/")
    return real.lower()


def _infer_store_from_playlist_item(
    item: Dict[str,
               Any],
    file_storage: Optional[Any] = None
) -> str:
    """Infer a friendly store label from an MPV playlist entry.

    Args:
        item: MPV playlist item dict
        file_storage: Optional FileStorage instance for querying specific backend instances

    Returns:
        Store label (e.g., 'home', 'work', 'local', 'youtube', etc.)
    """
    name = item.get("filename") if isinstance(item, dict) else None
    target = str(name or "")

    # Unwrap memory:// M3U wrapper
    memory_target = _extract_target_from_memory_uri(target)
    if memory_target:
        target = memory_target

    # Hydrus hashes: bare 64-hex entries
    if re.fullmatch(r"[0-9a-f]{64}", target.lower()):
        # If we have file_storage, query each Hydrus instance to find which one has this hash
        if file_storage:
            hash_str = target.lower()
            hydrus_instance = _find_hydrus_instance_for_hash(hash_str, file_storage)
            if hydrus_instance:
                return hydrus_instance
        return "hydrus"

    lower = target.lower()
    if lower.startswith("magnet:"):
        return "magnet"
    if lower.startswith("hydrus://"):
        # Extract hash from hydrus:// URL if possible
        if file_storage:
            hash_match = re.search(r"[0-9a-f]{64}", target.lower())
            if hash_match:
                hash_str = hash_match.group(0)
                hydrus_instance = _find_hydrus_instance_for_hash(hash_str, file_storage)
                if hydrus_instance:
                    return hydrus_instance
        return "hydrus"

    # Windows / UNC paths
    if re.match(r"^[a-z]:[\\/]",
                target,
                flags=re.IGNORECASE) or target.startswith("\\\\"):
        return "local"

    # file:// url
    if lower.startswith("file://"):
        return "local"

    parsed = urlparse(target)
    host = (parsed.netloc or "").lower()
    path = parsed.path or ""

    if not host:
        return ""

    host_no_port = host.split(":", 1)[0]
    host_stripped = host_no_port[4:] if host_no_port.startswith(
        "www."
    ) else host_no_port

    if "youtube" in host_stripped or "youtu.be" in target.lower():
        return "youtube"
    if "soundcloud" in host_stripped:
        return "soundcloud"
    if "bandcamp" in host_stripped:
        return "bandcamp"
    if "get_files" in path or "file?hash=" in path or host_stripped in {"127.0.0.1",
                                                                        "localhost"}:
        # Hydrus API URL - try to extract hash and find instance
        if file_storage:
            # Try to extract hash from URL parameters
            hash_match = re.search(r"hash=([0-9a-f]{64})", target.lower())
            if hash_match:
                hash_str = hash_match.group(1)
                hydrus_instance = _find_hydrus_instance_for_hash(hash_str, file_storage)
                if hydrus_instance:
                    return hydrus_instance
            # If no hash in URL, try matching the base URL to configured instances
            hydrus_instance = _find_hydrus_instance_by_url(target, file_storage)
            if hydrus_instance:
                return hydrus_instance
        return "hydrus"
    if re.match(r"^\d+\.\d+\.\d+\.\d+$", host_stripped) and "get_files" in path:
        # IP-based Hydrus URL
        if file_storage:
            hash_match = re.search(r"hash=([0-9a-f]{64})", target.lower())
            if hash_match:
                hash_str = hash_match.group(1)
                hydrus_instance = _find_hydrus_instance_for_hash(hash_str, file_storage)
                if hydrus_instance:
                    return hydrus_instance
            hydrus_instance = _find_hydrus_instance_by_url(target, file_storage)
            if hydrus_instance:
                return hydrus_instance
        return "hydrus"

    parts = host_stripped.split(".")
    if len(parts) >= 2:
        return parts[-2] or host_stripped
    return host_stripped


def _build_hydrus_header(config: Dict[str, Any]) -> Optional[str]:
    """Return header string for Hydrus auth if configured."""
    try:
        key = get_hydrus_access_key(config)
    except Exception:
        key = None
    if not key:
        return None
    return f"Hydrus-Client-API-Access-Key: {key}"


def _build_ytdl_options(config: Optional[Dict[str,
                                              Any]],
                        hydrus_header: Optional[str]) -> Optional[str]:
    """Compose ytdl-raw-options string including cookies and optional Hydrus header."""
    opts: List[str] = []
    cookies_path = None
    try:
        from tool.ytdlp import YtDlpTool

        cookiefile = YtDlpTool(config or {}).resolve_cookiefile()
        if cookiefile is not None:
            cookies_path = str(cookiefile)
    except Exception:
        cookies_path = None

    if cookies_path:
        opts.append(f"cookies={cookies_path.replace('\\', '/')}")
    else:
        opts.append("cookies-from-browser=chrome")
    if hydrus_header:
        opts.append(f"add-header={hydrus_header}")
    return ",".join(opts) if opts else None


def _is_hydrus_path(path: str, hydrus_url: Optional[str]) -> bool:
    if not path:
        return False
    lower = path.lower()
    if "hydrus://" in lower:
        return True
    parsed = urlparse(path)
    host = (parsed.netloc or "").lower()
    path_part = parsed.path or ""
    if hydrus_url:
        try:
            hydrus_host = urlparse(hydrus_url).netloc.lower()
            if hydrus_host and hydrus_host in host:
                return True
        except Exception:
            pass
    if "get_files" in path_part or "file?hash=" in path_part:
        return True
    if re.match(r"^\d+\.\d+\.\d+\.\d+$", host) and "get_files" in path_part:
        return True
    return False


def _ensure_ytdl_cookies(config: Optional[Dict[str, Any]] = None) -> None:
    """Ensure yt-dlp options are set correctly for this session."""
    from pathlib import Path

    cookies_path = None
    try:
        from tool.ytdlp import YtDlpTool

        cookiefile = YtDlpTool(config or {}).resolve_cookiefile()
        if cookiefile is not None:
            cookies_path = str(cookiefile)
    except Exception:
        cookies_path = None
    if cookies_path:
        # Check if file exists and has content (use forward slashes for path checking)
        check_path = cookies_path.replace("\\", "/")
        file_obj = Path(cookies_path)
        if file_obj.exists():
            file_size = file_obj.stat().st_size
            debug(f"Cookies file verified: {check_path} ({file_size} bytes)")
        else:
            debug(
                f"WARNING: Cookies file does not exist: {check_path}",
                file=sys.stderr
            )
    else:
        debug("No cookies file configured")


def _monitor_mpv_logs(duration: float = 3.0) -> None:
    """Monitor MPV logs for a short duration to capture errors."""
    try:
        mpv = MPV()
        client = mpv.client()
        if not client.connect():
            debug("Failed to connect to MPV for log monitoring", file=sys.stderr)
            return

        # Request log messages
        client.send_command({
            "command": ["request_log_messages",
                        "warn"]
        })

        # On Windows named pipes, avoid blocking the CLI; skip log read entirely
        if client.is_windows:
            client.disconnect()
            return

        import time

        start_time = time.time()

        # Unix sockets already have timeouts set; read until duration expires
        sock_obj = client.sock
        if not isinstance(sock_obj, socket.socket):
            client.disconnect()
            return

        while time.time() - start_time < duration:
            try:
                chunk = sock_obj.recv(4096)
            except socket.timeout:
                continue
            except Exception:
                break
            if not chunk:
                break
            for line in chunk.decode("utf-8", errors="ignore").splitlines():
                try:
                    msg = json.loads(line)
                    if msg.get("event") == "log-message":
                        text = msg.get("text", "").strip()
                        prefix = msg.get("prefix", "")
                        level = msg.get("level", "")
                        if "ytdl" in prefix or level == "error":
                            debug(f"[MPV {prefix}] {text}", file=sys.stderr)
                except json.JSONDecodeError:
                    continue

        client.disconnect()
    except Exception:
        pass


def _tail_text_file(path: str,
                    *,
                    max_lines: int = 120,
                    max_bytes: int = 65536) -> List[str]:
    try:
        p = Path(str(path))
        if not p.exists() or not p.is_file():
            return []
    except Exception:
        return []

    try:
        with open(p, "rb") as f:
            try:
                f.seek(0, os.SEEK_END)
                end = f.tell()
                start = max(0, end - int(max_bytes))
                f.seek(start, os.SEEK_SET)
            except Exception:
                pass
            data = f.read()
        text = data.decode("utf-8", errors="replace")
        lines = text.splitlines()
        if len(lines) > max_lines:
            return lines[-max_lines:]
        return lines
    except Exception:
        return []


def _get_playable_path(
    item: Any,
    file_storage: Optional[Any],
    config: Optional[Dict[str,
                          Any]]
) -> Optional[tuple[str,
                    Optional[str]]]:
    """Extract a playable path/URL from an item, handling different store types.

    Args:
        item: Item to extract path from (dict, PipeObject, or string)
        file_storage: FileStorage instance for querying backends
        config: Config dict for Hydrus URL

    Returns:
        Tuple of (path, title) or None if no valid path found
    """
    path: Optional[str] = None
    title: Optional[str] = None
    store: Optional[str] = None
    file_hash: Optional[str] = None

    # Extract fields from item - prefer a disk path ('path'), but accept 'url' as fallback for providers
    if isinstance(item, dict):
        path = item.get("path")
        # Fallbacks for provider-style entries where URL is stored in 'url' or 'source_url' or 'target'
        if not path:
            path = item.get("url") or item.get("source_url") or item.get("target")
            if not path:
                known = item.get("url") or item.get("url") or []
                if known and isinstance(known, list):
                    path = known[0]
        title = item.get("title") or item.get("file_title")
        store = item.get("store")
        file_hash = item.get("hash")
    elif (hasattr(item,
                  "path") or hasattr(item,
                                     "url") or hasattr(item,
                                                       "source_url") or hasattr(item,
                                                                                "store")
          or hasattr(item,
                     "hash")):
        # Handle PipeObject / dataclass objects - prefer path, but fall back to url/source_url attributes
        path = getattr(item, "path", None)
        if not path:
            path = (
                getattr(item,
                        "url",
                        None) or getattr(item,
                                         "source_url",
                                         None) or getattr(item,
                                                          "target",
                                                          None)
            )
            if not path:
                known = getattr(item,
                                "url",
                                None) or (getattr(item,
                                                  "extra",
                                                  None) or {}).get("url")
                if known and isinstance(known, list):
                    path = known[0]
        title = getattr(item, "title", None) or getattr(item, "file_title", None)
        store = getattr(item, "store", None)
        file_hash = getattr(item, "hash", None)
    elif isinstance(item, str):
        path = item

    # Debug: show incoming values
    try:
        debug(f"_get_playable_path: store={store}, path={path}, hash={file_hash}")
    except Exception:
        pass

    # Treat common placeholders as missing.
    if isinstance(path,
                  str) and path.strip().lower() in {"",
                                                    "n/a",
                                                    "na",
                                                    "none"}:
        path = None

    manifest_path = resolve_tidal_manifest_path(item)
    if manifest_path:
        path = manifest_path
    else:
        # If this is a hifi:// placeholder and we couldn't resolve a manifest, do not fall back.
        try:
            if isinstance(path, str) and path.strip().lower().startswith("hifi:"):
                try:
                    meta = None
                    if isinstance(item, dict):
                        meta = item.get("full_metadata") or item.get("metadata")
                    else:
                        meta = getattr(item, "full_metadata", None) or getattr(item, "metadata", None)
                    if isinstance(meta, dict) and meta.get("_tidal_manifest_error"):
                        print(str(meta.get("_tidal_manifest_error")), file=sys.stderr)
                except Exception:
                    pass
                print("HIFI selection has no playable DASH MPD manifest.", file=sys.stderr)
                return None
        except Exception:
            pass

    if title is not None and not isinstance(title, str):
        title = str(title)

    if isinstance(file_hash, str):
        file_hash = file_hash.strip().lower()

    # Resolve hash+store into a playable target (file path or URL).
    # This is unrelated to MPV's IPC pipe and keeps "pipe" terminology reserved for:
    # - MPV IPC pipe (transport)
    # - PipeObject (pipeline data)
    if store and file_hash and file_hash != "unknown" and file_storage:
        # If it's already a URL, MPV can usually play it directly.
        if isinstance(path, str) and path.startswith(("http://", "https://")):
            return (path, title)

        try:
            backend = file_storage[store]
        except Exception:
            backend = None

        if backend is not None:
            backend_class = type(backend).__name__

            # Folder stores: resolve to an on-disk file path.
            if (hasattr(backend,
                        "get_file") and callable(getattr(backend,
                                                         "get_file"))
                    and backend_class == "Folder"):
                try:
                    resolved = backend.get_file(file_hash)
                    if isinstance(resolved, Path):
                        path = str(resolved)
                    elif resolved is not None:
                        path = str(resolved)
                except Exception as e:
                    debug(
                        f"Error resolving file path from store '{store}': {e}",
                        file=sys.stderr
                    )

            # HydrusNetwork: build a playable API file URL without browser side-effects.
            elif backend_class == "HydrusNetwork":
                try:
                    client = getattr(backend, "_client", None)
                    base_url = getattr(client, "url", None)
                    if base_url:
                        base_url = str(base_url).rstrip("/")
                        # Auth is provided via http-header-fields (set in _queue_items).
                        path = f"{base_url}/get_files/file?hash={file_hash}"
                except Exception as e:
                    debug(
                        f"Error building Hydrus URL from store '{store}': {e}",
                        file=sys.stderr
                    )

    if not path:
        # As a last resort, if we have a hash and no path/url, return the hash.
        # _queue_items will convert it to a Hydrus file URL when possible.
        if store and file_hash and file_hash != "unknown":
            return (str(file_hash), title)
        return None

    if not isinstance(path, str):
        path = str(path)

    return (path, title)


def _queue_items(
    items: List[Any],
    clear_first: bool = False,
    config: Optional[Dict[str,
                          Any]] = None,
    start_opts: Optional[Dict[str,
                              Any]] = None,
) -> bool:
    """Queue items to MPV, starting it if necessary.

    Args:
        items: List of items to queue
        clear_first: If True, the first item will replace the current playlist

    Returns:
        True if MPV was started, False if items were queued via IPC.
    """
    # Debug: print incoming items
    try:
        debug(
            f"_queue_items: count={len(items)} types={[type(i).__name__ for i in items]}"
        )
    except Exception:
        pass

    # Just verify cookies are configured, don't try to set via IPC
    _ensure_ytdl_cookies(config)

    hydrus_header = _build_hydrus_header(config or {})
    ytdl_opts = _build_ytdl_options(config, hydrus_header)
    hydrus_url = None
    try:
        hydrus_url = get_hydrus_url(config) if config is not None else None
    except Exception:
        hydrus_url = None

    # Initialize Store registry for path resolution
    file_storage = None
    try:
        from Store import Store

        file_storage = Store(config or {})
    except Exception as e:
        debug(f"Warning: Could not initialize Store registry: {e}", file=sys.stderr)

    # Dedupe existing playlist before adding more (unless we're replacing it)
    existing_targets: set[str] = set()
    if not clear_first:
        playlist = _get_playlist(silent=True) or []
        dup_indexes: List[int] = []
        for idx, pl_item in enumerate(playlist):
            fname = pl_item.get("filename") if isinstance(pl_item,
                                                          dict) else str(pl_item)
            alt = pl_item.get("playlist-path") if isinstance(pl_item, dict) else None
            norm = _normalize_playlist_path(fname) or _normalize_playlist_path(alt)
            if not norm:
                continue
            if norm in existing_targets:
                dup_indexes.append(idx)
            else:
                existing_targets.add(norm)

        # Remove duplicates from playlist starting from the end to keep indices valid
        for idx in reversed(dup_indexes):
            try:
                _send_ipc_command(
                    {
                        "command": ["playlist-remove",
                                    idx],
                        "request_id": 106
                    },
                    silent=True
                )
            except Exception:
                pass

    new_targets: set[str] = set()

    for i, item in enumerate(items):
        # Debug: show the item being processed
        try:
            debug(
                f"_queue_items: processing idx={i} type={type(item)} repr={repr(item)[:200]}"
            )
        except Exception:
            pass
        # Extract URL/Path using store-aware logic
        result = _get_playable_path(item, file_storage, config)
        if not result:
            debug(f"_queue_items: item idx={i} produced no playable path")
            continue

        target, title = result

        # MPD/DASH playback requires ffmpeg protocol whitelist (file + https + crypto etc).
        # Set it via IPC before loadfile so the currently running MPV can play the manifest.
        try:
            target_str = str(target or "")
            if re.search(r"\.mpd($|\?)", target_str.lower()):
                _send_ipc_command(
                    {
                        "command": [
                            "set_property",
                            "options/demuxer-lavf-o",
                            "protocol_whitelist=file,https,tcp,tls,crypto,data",
                        ],
                        "request_id": 198,
                    },
                    silent=True,
                )
        except Exception:
            pass

        # If the target is an AllDebrid protected file URL, unlock it to a direct link for MPV.
        try:
            if isinstance(target, str):
                target = _maybe_unlock_alldebrid_url(target, config)
        except Exception:
            pass

        # Prefer per-item Hydrus instance credentials when the item belongs to a Hydrus store.
        effective_hydrus_url = hydrus_url
        effective_hydrus_header = hydrus_header
        effective_ytdl_opts = ytdl_opts
        item_store_name: Optional[str] = None
        try:
            item_store = None
            if isinstance(item, dict):
                item_store = item.get("store")
            else:
                item_store = getattr(item, "store", None)

            if item_store:
                item_store_name = str(item_store).strip() or None

            if item_store and file_storage:
                try:
                    backend = file_storage[str(item_store)]
                except Exception:
                    backend = None

                if backend is not None and type(backend).__name__ == "HydrusNetwork":
                    client = getattr(backend, "_client", None)
                    base_url = getattr(client, "url", None)
                    key = getattr(client, "access_key", None)
                    if base_url:
                        effective_hydrus_url = str(base_url).rstrip("/")
                    if key:
                        effective_hydrus_header = (
                            f"Hydrus-Client-API-Access-Key: {str(key).strip()}"
                        )
                        effective_ytdl_opts = _build_ytdl_options(
                            config,
                            effective_hydrus_header
                        )
        except Exception:
            pass

        if target:
            # If we just have a hydrus hash, build a direct file URL for MPV
            if re.fullmatch(r"[0-9a-f]{64}",
                            str(target).strip().lower()) and effective_hydrus_url:
                target = (
                    f"{effective_hydrus_url.rstrip('/')}/get_files/file?hash={str(target).strip()}"
                )

            norm_key = _normalize_playlist_path(target) or str(target).strip().lower()
            if norm_key in existing_targets or norm_key in new_targets:
                debug(f"Skipping duplicate playlist entry: {title or target}")
                continue
            new_targets.add(norm_key)

            # Use memory:// M3U hack to pass title to MPV.
            # This is especially important for remote URLs (e.g., YouTube) where MPV may otherwise
            # show the raw URL as the playlist title.
            if title:
                # Sanitize title for M3U (remove newlines)
                safe_title = title.replace("\n", " ").replace("\r", "")

                # Carry the store name for hash URLs so MPV.lyric can resolve the backend.
                # This is especially important for local file-server URLs like /get_files/file?hash=...
                target_for_m3u = target
                try:
                    if (item_store_name and isinstance(target_for_m3u,
                                                       str)
                            and target_for_m3u.startswith("http")):
                        if "get_files/file" in target_for_m3u and "store=" not in target_for_m3u:
                            sep = "&" if "?" in target_for_m3u else "?"
                            target_for_m3u = f"{target_for_m3u}{sep}store={item_store_name}"
                except Exception:
                    target_for_m3u = target

                m3u_content = f"#EXTM3U\n#EXTINF:-1,{safe_title}\n{target_for_m3u}"
                target_to_send = f"memory://{m3u_content}"
            else:
                target_to_send = target

            mode = "append"
            if clear_first and i == 0:
                mode = "replace"

            # If we're replacing, this will start playing immediately: set store/hash context
            # so MPV.lyric can resolve the correct backend for notes.
            if mode == "replace":
                try:
                    s, h = _extract_store_and_hash(item)
                    _set_mpv_item_context(s, h)
                except Exception:
                    pass

            # If this is a Hydrus path, set header property and yt-dlp headers before loading.
            # Use the real target (not the memory:// wrapper) for detection.
            if effective_hydrus_header and _is_hydrus_path(str(target),
                                                           effective_hydrus_url):
                header_cmd = {
                    "command":
                    ["set_property",
                     "http-header-fields",
                     effective_hydrus_header],
                    "request_id":
                    199,
                }
                _send_ipc_command(header_cmd, silent=True)
                if effective_ytdl_opts:
                    ytdl_cmd = {
                        "command":
                        ["set_property",
                         "ytdl-raw-options",
                         effective_ytdl_opts],
                        "request_id": 197,
                    }
                    _send_ipc_command(ytdl_cmd, silent=True)

            # For memory:// M3U payloads (used to carry titles), use loadlist so mpv parses
            # the content as a playlist and does not expose #EXTINF lines as entries.
            command_name = "loadfile"
            try:
                if isinstance(target_to_send, str) and target_to_send.startswith("memory://") and "#EXTM3U" in target_to_send:
                    command_name = "loadlist"
            except Exception:
                pass

            cmd = {
                "command": [command_name,
                            target_to_send,
                            mode],
                "request_id": 200
            }
            try:
                debug(f"Sending MPV {command_name}: {target_to_send} mode={mode}")
                resp = _send_ipc_command(cmd, silent=True)
                debug(f"MPV {command_name} response: {resp}")
            except Exception as e:
                debug(f"Exception sending {command_name} to MPV: {e}", file=sys.stderr)
                resp = None

            if resp is None:
                # MPV not running (or died)
                # Start MPV with remaining items
                debug(
                    f"MPV not running/died while queuing, starting MPV with remaining items: {items[i:]}"
                )
                _start_mpv(items[i:], config=config, start_opts=start_opts)
                return True
            elif resp.get("error") == "success":
                # Do not set `force-media-title` when queueing items. It's a global property and
                # would change the MPV window title even if the item isn't currently playing.
                debug(f"Queued: {title or target}")
            else:
                error_msg = str(resp.get("error"))
                debug(f"Failed to queue item: {error_msg}", file=sys.stderr)
    return False


def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
    """Manage and play items in the MPV playlist via IPC."""

    parsed = parse_cmdlet_args(args, CMDLET)

    log_requested = bool(parsed.get("log"))
    borderless = bool(parsed.get("borderless"))

    prev_debug = is_debug_enabled()
    prev_stream = get_thread_stream()
    devnull_fh = None

    mpv_log_path: Optional[str] = None

    try:
        # Default: keep `.pipe` quiet even if debug is enabled.
        # With -log: enable debug and route it to stdout (pipeable), plus enable mpv log-file.
        if log_requested:
            set_debug(True)
            set_thread_stream(sys.stdout)
            try:
                log_dir = _repo_log_dir()
                mpv_log_path = str((log_dir / "medeia-mpv.log").resolve())
            except Exception:
                mpv_log_path = str(
                    (
                        Path(os.environ.get("TEMP") or os.environ.get("TMP") or ".") /
                        "medeia-mpv.log"
                    ).resolve()
                )
            # Ensure file exists early so we can tail it even if mpv writes later.
            try:
                Path(mpv_log_path).parent.mkdir(parents=True, exist_ok=True)
                with open(mpv_log_path, "a", encoding="utf-8", errors="replace"):
                    pass
            except Exception:
                pass
            debug(f"MPV log file: {mpv_log_path}")

            # Try to enable mpv file logging on the currently running instance.
            # (If mpv wasn't started with --log-file, this may not work everywhere.)
            try:
                _try_enable_mpv_file_logging(mpv_log_path, attempts=3)
            except Exception:
                pass

            # If mpv is already running, set log options live via IPC.
            try:
                mpv_live = MPV()
                if mpv_live.is_running():
                    mpv_live.set_property("options/log-file", mpv_log_path)
                    mpv_live.set_property("options/msg-level", "all=v")
            except Exception:
                pass
        else:
            if prev_debug:
                try:
                    devnull_fh = open(
                        os.devnull,
                        "w",
                        encoding="utf-8",
                        errors="replace"
                    )
                    set_thread_stream(devnull_fh)
                except Exception:
                    pass

        start_opts: Dict[str,
                         Any] = {
                             "borderless": borderless,
                             "mpv_log_path": mpv_log_path
                         }

        # Store registry is only needed for certain playlist listing/inference paths.
        # Keep it lazy so a simple `.pipe <url> -play` doesn't trigger Hydrus/API calls.
        file_storage = None

        # Initialize mpv_started flag
        mpv_started = False

        # Handle positional index argument if provided
        index_arg = parsed.get("index")
        url_arg = parsed.get("url")

        # If index_arg is provided but is not an integer, treat it as a URL
        # This allows .pipe "http://..." without -url flag
        if index_arg is not None:
            try:
                int(index_arg)
            except ValueError:
                # Not an integer, treat as URL if url_arg is not set
                if not url_arg:
                    url_arg = index_arg
                    index_arg = None

        clear_mode = parsed.get("clear")
        list_mode = parsed.get("list")
        play_mode = parsed.get("play")
        pause_mode = parsed.get("pause")
        save_mode = parsed.get("save")
        load_mode = parsed.get("load")
        current_mode = parsed.get("current")

        # Pure log mode: `.pipe -log` should not run any playlist actions and
        # should not print the playlist table. It should only enable/tail logs
        # (handled in the `finally` block).
        only_log = bool(
            log_requested and not url_arg and index_arg is None and not clear_mode
            and not list_mode and not play_mode and not pause_mode and not save_mode
            and not load_mode and not current_mode
        )
        if only_log:
            return 0

        # Handle --current flag: emit currently playing item to pipeline
        if current_mode:
            items = _get_playlist()
            if items is None:
                debug("MPV is not running or not accessible.", file=sys.stderr)
                return 1

            # Find the currently playing item
            current_item = None
            for item in items:
                if item.get("current", False):
                    current_item = item
                    break

            if current_item is None:
                debug("No item is currently playing.", file=sys.stderr)
                return 1

            # Build result object with file info
            title = _extract_title_from_item(current_item)
            filename = current_item.get("filename", "")

            # Emit the current item to pipeline
            result_obj = {
                "path": filename,
                "title": title,
                "cmdlet_name": ".mpv",
                "source": "pipe",
                "__pipe_index": items.index(current_item),
            }

            ctx.emit(result_obj)
            debug(f"Emitted current item: {title}")
            return 0

        # Handle URL queuing
        mpv_started = False
        if url_arg:
            mpv_started = _queue_items([url_arg], config=config, start_opts=start_opts)
            # Auto-play the URL when it's queued via .pipe "url" (without explicit flags)
            # unless other flags are present
            if not (clear_mode or play_mode or pause_mode or save_mode or load_mode):
                if mpv_started:
                    # MPV was just started, wait a moment for it to be ready, then play first item
                    import time

                    time.sleep(0.5)
                    index_arg = "1"  # 1-based index for first item
                    play_mode = True
                else:
                    # MPV was already running, get playlist and play the newly added item
                    playlist = _get_playlist(silent=True)
                    if playlist and len(playlist) > 0:
                        # Auto-play the last item in the playlist (the one we just added)
                        # Use 1-based indexing
                        index_arg = str(len(playlist))
                        play_mode = True
                    else:
                        # Fallback: just list the playlist if we can't determine index
                        list_mode = True

            # If the user explicitly requested -play while queueing a URL, interpret that
            # as "play the URL I just queued" (not merely "unpause whatever is currently playing").
            if play_mode and index_arg is None:
                if mpv_started:
                    # MPV was just started; give it a moment, then play first item.
                    import time

                    time.sleep(0.5)
                    index_arg = "1"
                else:
                    playlist = _get_playlist(silent=True)
                    if playlist and len(playlist) > 0:
                        index_arg = str(len(playlist))

            # Ensure lyric overlay is running (auto-discovery handled by MPV.lyric).
            try:
                mpv = MPV()
                _ensure_lyric_overlay(mpv)
            except Exception:
                pass

        # Handle Save Playlist
        if save_mode:
            # Avoid `shell=True` / `date /t` on Windows (can flash a cmd.exe window).
            # Use Python's datetime instead.
            from datetime import datetime

            playlist_name = index_arg or f"Playlist {datetime.now().strftime('%Y-%m-%d')}"
            # If index_arg was used for name, clear it so it doesn't trigger index logic
            if index_arg:
                index_arg = None

            items = _get_playlist()
            if not items:
                debug("Cannot save: MPV playlist is empty or MPV is not running.")
                return 1

            # Clean up items for saving (remove current flag, etc)
            clean_items = []
            for item in items:
                # If title was extracted from memory://, we should probably save the original filename
                # if it's a URL, or reconstruct a clean object.
                # Actually, _extract_title_from_item handles the display title.
                # But for playback, we need the 'filename' (which might be memory://...)
                # If we save 'memory://...', it will work when loaded back.
                clean_items.append(item)

            # Use config from context or load it
            config_data = config if config else {}

            storage_path = get_local_storage_path(config_data) or _default_state_dir()
            try:
                Path(storage_path).mkdir(parents=True, exist_ok=True)
            except Exception:
                pass

            with LocalLibrarySearchOptimizer(storage_path) as db:
                if db.save_playlist(playlist_name, clean_items):
                    debug(f"Playlist saved as '{playlist_name}'")
                    return 0
                else:
                    debug(f"Failed to save playlist '{playlist_name}'")
                    return 1

        # Handle Load Playlist
        current_playlist_name = None
        if load_mode:
            # Use config from context or load it
            config_data = config if config else {}

            storage_path = get_local_storage_path(config_data)
            if not storage_path:
                debug("Local storage path not configured.")
                return 1

            with LocalLibrarySearchOptimizer(storage_path) as db:
                if index_arg:
                    try:
                        pl_id = int(index_arg)

                        # Handle Delete Playlist (if -clear is also passed)
                        if clear_mode:
                            if db.delete_playlist(pl_id):
                                debug(f"Playlist ID {pl_id} deleted.")
                                # Clear index_arg so we fall through to list mode and show updated list
                                index_arg = None
                                # Don't return, let it list the remaining playlists
                            else:
                                debug(f"Failed to delete playlist ID {pl_id}.")
                                return 1
                        else:
                            # Handle Load Playlist
                            result = db.get_playlist_by_id(pl_id)
                            if result is None:
                                debug(f"Playlist ID {pl_id} not found.")
                                return 1

                            name, items = result
                            current_playlist_name = name

                            # Queue items (replacing current playlist)
                            if items:
                                _queue_items(
                                    items,
                                    clear_first=True,
                                    config=config,
                                    start_opts=start_opts
                                )
                            else:
                                # Empty playlist, just clear
                                _send_ipc_command(
                                    {
                                        "command": ["playlist-clear"]
                                    },
                                    silent=True
                                )

                            # Switch to list mode to show the result
                            list_mode = True
                            index_arg = None
                            # Fall through to list logic

                    except ValueError:
                        debug(f"Invalid playlist ID: {index_arg}")
                        return 1

                # If we deleted or didn't have an index, list playlists
                if not index_arg:
                    playlists = db.get_playlists()

                    if not playlists:
                        debug("No saved playlists found.")
                        return 0

                    table = ResultTable("Saved Playlists")
                    for i, pl in enumerate(playlists):
                        item_count = len(pl.get("items", []))
                        row = table.add_row()
                        # row.add_column("ID", str(pl['id'])) # Hidden as per user request
                        row.add_column("Name", pl["name"])
                        row.add_column("Items", str(item_count))
                        row.add_column("Updated", pl["updated_at"])

                        # Set the playlist items as the result object for this row
                        # When user selects @N, they get the list of items
                        # We also set the source command to .pipe -load <ID> so it loads it
                        table.set_row_selection_args(i, ["-load", str(pl["id"])])

                    table.set_source_command(".mpv")

                    # Register results
                    ctx.set_last_result_table_overlay(
                        table,
                        [p["items"] for p in playlists]
                    )
                    ctx.set_current_stage_table(table)

                    # Do not print directly here.
                    # Both CmdletExecutor and PipelineExecutor render the current-stage/overlay table,
                    # so printing here would duplicate output.
                    return 0

        # Everything below was originally outside a try block; keep it inside so `start_opts` is in scope.

        # Handle Play/Pause commands (but skip if we have index_arg to play a specific item)
        if play_mode and index_arg is None:
            cmd = {
                "command": ["set_property",
                            "pause",
                            False],
                "request_id": 103
            }
            resp = _send_ipc_command(cmd)
            if resp and resp.get("error") == "success":
                debug("Resumed playback")
                return 0
            else:
                debug("Failed to resume playback (MPV not running?)", file=sys.stderr)
                return 1

        if pause_mode:
            cmd = {
                "command": ["set_property",
                            "pause",
                            True],
                "request_id": 104
            }
            resp = _send_ipc_command(cmd)
            if resp and resp.get("error") == "success":
                debug("Paused playback")
                return 0
            else:
                debug("Failed to pause playback (MPV not running?)", file=sys.stderr)
                return 1

        # Handle Clear All command (no index provided)
        if clear_mode and index_arg is None:
            cmd = {
                "command": ["playlist-clear"],
                "request_id": 105
            }
            resp = _send_ipc_command(cmd)
            if resp and resp.get("error") == "success":
                debug("Playlist cleared")
                return 0
            else:
                debug("Failed to clear playlist (MPV not running?)", file=sys.stderr)
                return 1

        # Handle piped input (add to playlist)
        # Skip adding if -list is specified (user just wants to see current playlist)
        if result and not list_mode and not url_arg:
            playlist_before = _get_playlist(silent=True)
            idle_before = None
            try:
                idle_resp = _send_ipc_command(
                    {
                        "command": ["get_property",
                                    "idle-active"],
                        "request_id": 111
                    },
                    silent=True
                )
                if idle_resp and idle_resp.get("error") == "success":
                    idle_before = bool(idle_resp.get("data"))
            except Exception:
                idle_before = None

            # If result is a list of items, add them to playlist
            items_to_add = []
            if isinstance(result, list):
                items_to_add = result
            elif isinstance(result, dict):
                items_to_add = [result]
            else:
                # Handle PipeObject or any other object type
                items_to_add = [result]

            # Debug: inspect incoming result and attributes
            try:
                debug(
                    f"pipe._run: received result type={type(result)} repr={repr(result)[:200]}"
                )
                debug(
                    f"pipe._run: attrs path={getattr(result, 'path', None)} url={getattr(result, 'url', None)} store={getattr(result, 'store', None)} hash={getattr(result, 'hash', None)}"
                )
            except Exception:
                pass

            queued_started_mpv = False
            if items_to_add and _queue_items(items_to_add,
                                             config=config,
                                             start_opts=start_opts):
                mpv_started = True
                queued_started_mpv = True

            # Ensure lyric overlay is running when we queue anything via .pipe.
            if items_to_add and not queued_started_mpv:
                try:
                    mpv = MPV()
                    _ensure_lyric_overlay(mpv)
                except Exception:
                    pass

            # Auto-play when a single item is piped and mpv was idle/empty.
            if items_to_add and len(items_to_add) == 1 and not queued_started_mpv:
                try:
                    playlist_after = _get_playlist(silent=True)
                    before_len = len(playlist_before
                                     ) if isinstance(playlist_before,
                                                     list) else 0
                    after_len = len(playlist_after
                                    ) if isinstance(playlist_after,
                                                    list) else 0

                    should_autoplay = False
                    if idle_before is True:
                        should_autoplay = True
                    elif isinstance(playlist_before,
                                    list) and len(playlist_before) == 0:
                        should_autoplay = True

                    if should_autoplay and after_len > 0:
                        idx_to_play = min(max(0, before_len), after_len - 1)

                        # Prefer the store/hash from the piped item when auto-playing.
                        try:
                            s, h = _extract_store_and_hash(items_to_add[0])
                            _set_mpv_item_context(s, h)
                        except Exception:
                            pass
                        play_resp = _send_ipc_command(
                            {
                                "command": ["playlist-play-index",
                                            idx_to_play],
                                "request_id": 112
                            },
                            silent=True,
                        )
                        _send_ipc_command(
                            {
                                "command": ["set_property",
                                            "pause",
                                            False],
                                "request_id": 113
                            },
                            silent=True,
                        )
                        if play_resp and play_resp.get("error") == "success":
                            debug("Auto-playing piped item")

                        # Start lyric overlay (auto-discovery handled by MPV.lyric).
                        try:
                            mpv = MPV()
                            _ensure_lyric_overlay(mpv)
                        except Exception:
                            pass
                except Exception:
                    pass

        # Get playlist from MPV (silent: we handle MPV-not-running gracefully below)
        items = _get_playlist(silent=True)

        if items is None:
            if mpv_started:
                # MPV was just started, retry getting playlist after a brief delay
                import time

                time.sleep(0.3)
                items = _get_playlist(silent=True)

                if items is None:
                    # Still can't connect, but MPV is starting
                    debug("MPV is starting up...")
                    return 0
            else:
                # Do not auto-launch MPV when no action/inputs were provided; avoid surprise startups
                no_inputs = not any(
                    [
                        result,
                        url_arg,
                        index_arg,
                        clear_mode,
                        play_mode,
                        pause_mode,
                        save_mode,
                        load_mode,
                        current_mode,
                        list_mode,
                    ]
                )

                if no_inputs:
                    # User invoked `.pipe` with no args: treat this as an intent to open MPV.
                    debug("MPV is not running. Starting new instance...")
                    _start_mpv([], config=config, start_opts=start_opts)

                    # Re-check playlist after startup; if IPC still isn't ready, just exit cleanly.
                    try:
                        import time

                        time.sleep(0.3)
                    except Exception:
                        pass
                    items = _get_playlist(silent=True)
                    if items is None:
                        debug("MPV is starting up...")
                        return 0

                    # IPC is ready; continue without restarting MPV again.
                else:
                    debug("MPV is not running. Starting new instance...")
                    _start_mpv([], config=config, start_opts=start_opts)
                    return 0

        if not items:
            debug("MPV playlist is empty.")
            return 0

        # If index is provided, perform action (Play or Clear)
        if index_arg is not None:
            try:
                # Handle 1-based index
                idx = int(index_arg) - 1

                if idx < 0 or idx >= len(items):
                    debug(f"Index {index_arg} out of range (1-{len(items)}).")
                    return 1

                item = items[idx]
                title = _extract_title_from_item(item)
                filename = item.get("filename", "") if isinstance(item, dict) else ""
                hydrus_header = _build_hydrus_header(config or {})
                hydrus_url = None
                try:
                    hydrus_url = get_hydrus_url(config) if config is not None else None
                except Exception:
                    hydrus_url = None

                if clear_mode:
                    # Remove item
                    cmd = {
                        "command": ["playlist-remove",
                                    idx],
                        "request_id": 101
                    }
                    resp = _send_ipc_command(cmd)
                    if resp and resp.get("error") == "success":
                        debug(f"Removed: {title}")
                        # Refresh items for listing
                        items = _get_playlist() or []
                        list_mode = True
                        index_arg = None
                    else:
                        debug(
                            f"Failed to remove item: {resp.get('error') if resp else 'No response'}"
                        )
                        return 1
                else:
                    # Play item
                    try:
                        s, h = _extract_store_and_hash(item)
                        _set_mpv_item_context(s, h)
                    except Exception:
                        pass
                    if hydrus_header and _is_hydrus_path(filename, hydrus_url):
                        header_cmd = {
                            "command":
                            ["set_property",
                             "http-header-fields",
                             hydrus_header],
                            "request_id": 198,
                        }
                        _send_ipc_command(header_cmd, silent=True)
                    cmd = {
                        "command": ["playlist-play-index",
                                    idx],
                        "request_id": 102
                    }
                    resp = _send_ipc_command(cmd)
                    if resp and resp.get("error") == "success":
                        # Ensure playback starts (unpause)
                        unpause_cmd = {
                            "command": ["set_property",
                                        "pause",
                                        False],
                            "request_id": 103,
                        }
                        _send_ipc_command(unpause_cmd)

                        debug(f"Playing: {title}")

                        # Monitor logs briefly for errors (e.g. ytdl failures)
                        _monitor_mpv_logs(3.0)

                        # Refresh playlist view so the user sees the new current item immediately
                        items = _get_playlist(silent=True) or items
                        list_mode = True
                        index_arg = None
                    else:
                        debug(
                            f"Failed to play item: {resp.get('error') if resp else 'No response'}"
                        )
                        return 1
            except ValueError:
                debug(f"Invalid index: {index_arg}")
                return 1

        # List items (Default action or after clear)
        if list_mode or (index_arg is None and not url_arg):
            if not items:
                debug("MPV playlist is empty.")
                return 0

            if file_storage is None:
                try:
                    from Store import Store

                    file_storage = Store(config)
                except Exception as e:
                    debug(
                        f"Warning: Could not initialize Store registry: {e}",
                        file=sys.stderr
                    )

            # Use the loaded playlist name if available, otherwise default
            # Note: current_playlist_name is defined in the load_mode block if a playlist was loaded
            try:
                table_title = current_playlist_name or "MPV Playlist"
            except NameError:
                table_title = "MPV Playlist"

            table = ResultTable(table_title, preserve_order=True)

            # Convert MPV items to PipeObjects with proper hash and store
            pipe_objects = []
            for i, item in enumerate(items):
                is_current = item.get("current", False)
                title = _extract_title_from_item(item)
                filename = item.get("filename", "")

                # Extract the real path/URL from memory:// wrapper if present
                real_path = _extract_target_from_memory_uri(filename) or filename

                # Try to extract hash from the path/URL
                file_hash = None
                store_name = None

                # Check if it's a Hydrus URL
                if "get_files/file" in real_path or "hash=" in real_path:
                    # Extract hash from Hydrus URL
                    hash_match = re.search(r"hash=([0-9a-f]{64})", real_path.lower())
                    if hash_match:
                        file_hash = hash_match.group(1)
                        # Try to find which Hydrus instance has this file
                        if file_storage:
                            store_name = _find_hydrus_instance_for_hash(
                                file_hash,
                                file_storage
                            )
                            if not store_name:
                                store_name = "hydrus"
                # Check if it's a hash-based local file
                elif real_path:
                    # Try to extract hash from filename (e.g., C:\path\1e8c46...a1b2.mp4)
                    path_obj = Path(real_path)
                    stem = path_obj.stem  # filename without extension
                    if len(stem) == 64 and all(c in "0123456789abcdef"
                                               for c in stem.lower()):
                        file_hash = stem.lower()
                        # Find which folder store has this file
                        if file_storage:
                            for backend_name in file_storage.list_backends():
                                backend = file_storage[backend_name]
                                if type(backend).__name__ == "Folder":
                                    # Check if this backend has the file
                                    try:
                                        result_path = backend.get_file(file_hash)
                                        if isinstance(result_path,
                                                      Path) and result_path.exists():
                                            store_name = backend_name
                                            break
                                    except Exception:
                                        pass

                # Fallback to inferred store if we couldn't find it
                if not store_name:
                    store_name = _infer_store_from_playlist_item(
                        item,
                        file_storage=file_storage
                    )

                # Build PipeObject with proper metadata
                pipe_obj = PipeObject(
                    hash=file_hash or "unknown",
                    store=store_name or "unknown",
                    title=title,
                    path=real_path,
                )
                pipe_objects.append(pipe_obj)

                # Truncate title for display
                display_title = title
                if len(display_title) > 80:
                    display_title = display_title[:77] + "..."

                row = table.add_row()
                row.add_column("Current", "*" if is_current else "")
                row.add_column("Store", store_name or "unknown")
                row.add_column("Title", display_title)

                table.set_row_selection_args(i, [str(i + 1)])

            table.set_source_command(".mpv")

            # Register PipeObjects (not raw MPV items) with pipeline context
            ctx.set_last_result_table_overlay(table, pipe_objects)
            ctx.set_current_stage_table(table)

            # Do not print directly here.
            # Both CmdletExecutor and PipelineExecutor render the current-stage/overlay table,
            # so printing here would duplicate output.

        return 0
    finally:
        if log_requested and isinstance(mpv_log_path, str) and mpv_log_path.strip():
            try:
                # Give mpv a short moment to flush logs, then print a tail that is easy to copy.
                print(f"MPV log file: {mpv_log_path}")

                # Best-effort: re-try enabling file logging at the end too (mpv may have
                # been unreachable at the start).
                try:
                    _try_enable_mpv_file_logging(mpv_log_path, attempts=2)
                except Exception:
                    pass

                tail_lines: List[str] = []
                for _ in range(8):
                    tail_lines = _tail_text_file(mpv_log_path, max_lines=200)
                    if tail_lines:
                        break
                    try:
                        import time

                        time.sleep(0.25)
                    except Exception:
                        break

                if tail_lines:
                    print("MPV log (tail):")
                    for ln in tail_lines:
                        print(ln)
                else:
                    print("MPV log (tail): <empty>")
                    print(
                        "Note: On some Windows builds, mpv cannot start writing to --log-file after launch."
                    )
                    print(
                        "If you need full [main2] logs, restart mpv so it starts with --log-file."
                    )

                # Also print the helper log tail (this captures Python helper output that won't
                # necessarily show up in MPV's own log-file).
                try:
                    helper_path = _helper_log_file()
                    helper_tail = _tail_text_file(str(helper_path), max_lines=200)
                    print(f"Helper log file: {str(helper_path)}")
                    if helper_tail:
                        print("Helper log (tail):")
                        for ln in helper_tail:
                            print(ln)
                    else:
                        print("Helper log (tail): <empty>")
                except Exception:
                    pass

                # Also print the Lua-side log tail (mp.msg output isn't always written to mpv's log-file).
                try:
                    lua_path = _lua_log_file()
                    lua_tail = _tail_text_file(str(lua_path), max_lines=200)
                    print(f"Lua log file: {str(lua_path)}")
                    if lua_tail:
                        print("Lua log (tail):")
                        for ln in lua_tail:
                            print(ln)
                    else:
                        print("Lua log (tail): <empty>")
                except Exception:
                    pass
            except Exception:
                pass
        try:
            set_thread_stream(prev_stream)
        except Exception:
            pass
        try:
            set_debug(prev_debug)
        except Exception:
            pass
        try:
            if devnull_fh is not None:
                devnull_fh.close()
        except Exception:
            pass


def _start_mpv(
    items: List[Any],
    config: Optional[Dict[str,
                          Any]] = None,
    start_opts: Optional[Dict[str,
                              Any]] = None,
) -> None:
    """Start MPV with a list of items."""
    import time as _time_module

    mpv = MPV()
    mpv.kill_existing_windows()
    _time_module.sleep(0.5)  # Wait for process to die

    hydrus_header = _build_hydrus_header(config or {})
    ytdl_opts = _build_ytdl_options(config, hydrus_header)

    cookies_path = None
    try:
        from tool.ytdlp import YtDlpTool

        cookiefile = YtDlpTool(config or {}).resolve_cookiefile()
        if cookiefile is not None:
            cookies_path = str(cookiefile)
    except Exception:
        cookies_path = None
    if cookies_path:
        debug(f"Starting MPV with cookies file: {cookies_path.replace('\\', '/')}")
    else:
        debug("Starting MPV with browser cookies: chrome")

    try:
        extra_args: List[str] = [
            "--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]",
        ]

        # If we are going to play a DASH MPD, allow ffmpeg to fetch https segments referenced by the manifest.
        try:
            needs_mpd_whitelist = False
            for it in items or []:
                mpd = resolve_tidal_manifest_path(it)
                candidate = mpd
                if not candidate:
                    if isinstance(it, dict):
                        candidate = it.get("path") or it.get("url")
                    else:
                        candidate = getattr(it, "path", None) or getattr(it, "url", None)
                if candidate and re.search(r"\.mpd($|\?)", str(candidate).lower()):
                    needs_mpd_whitelist = True
                    break
            if needs_mpd_whitelist:
                extra_args.append(
                    "--demuxer-lavf-o=protocol_whitelist=file,https,tcp,tls,crypto,data"
                )
        except Exception:
            pass

        # Optional: borderless window (useful for uosc-like overlay UI without fullscreen).
        if start_opts and start_opts.get("borderless"):
            extra_args.append("--border=no")

        # Optional: mpv logging to file.
        mpv_log_path = (start_opts or {}).get("mpv_log_path")
        if isinstance(mpv_log_path, str) and mpv_log_path.strip():
            extra_args.append(f"--log-file={mpv_log_path}")
            extra_args.append("--msg-level=all=v")

        # Always start MPV with the bundled Lua script via MPV class.
        mpv.start(
            extra_args=extra_args,
            ytdl_raw_options=ytdl_opts,
            http_header_fields=hydrus_header,
            detached=True,
        )
        debug("Started MPV process")

        # Wait for IPC pipe to be ready
        if not mpv.wait_for_ipc(retries=20, delay_seconds=0.2):
            debug("Timed out waiting for MPV IPC connection", file=sys.stderr)
            return

        # Publish context early so the lyric helper can resolve notes on the first
        # target change (the helper may start before playback begins).
        try:
            if items:
                s, h = _extract_store_and_hash(items[0])
                _set_mpv_item_context(s, h)
        except Exception:
            pass

        # main.lua is loaded at startup via --script; don't reload it here.

        # Ensure lyric overlay is running (auto-discovery handled by MPV.lyric).
        _ensure_lyric_overlay(mpv)

        # Queue items via IPC
        if items:
            _queue_items(items, config=config, start_opts=start_opts)

            # Auto-play the first item
            import time

            time.sleep(0.3)  # Give MPV a moment to process the queued items

            # Play the first item (index 0) and unpause
            play_cmd = {
                "command": ["playlist-play-index",
                            0],
                "request_id": 102
            }
            play_resp = _send_ipc_command(play_cmd, silent=True)

            if play_resp and play_resp.get("error") == "success":
                # Ensure playback starts (unpause)
                unpause_cmd = {
                    "command": ["set_property",
                                "pause",
                                False],
                    "request_id": 103
                }
                _send_ipc_command(unpause_cmd, silent=True)
                debug("Auto-playing first item")

            # Overlay already started above; it will follow track changes automatically.

    except Exception as e:
        debug(f"Error starting MPV: {e}", file=sys.stderr)


CMDLET = Cmdlet(
    name=".mpv",
    alias=[".pipe", "pipe", "playlist", "queue", "ls-pipe"],
    summary="Manage and play items in the MPV playlist via IPC",
    usage=".mpv [index|url] [-current] [-clear] [-list] [-url URL] [-log] [-borderless]",
    arg=[
        CmdletArg(
            name="index",
            type="string",  # Changed to string to allow URL detection
            description="Index of item to play/clear, or URL to queue",
            required=False,
        ),
        CmdletArg(name="url", type="string", description="URL to queue", required=False),
        CmdletArg(
            name="clear",
            type="flag",
            description="Remove the selected item, or clear entire playlist if no index provided",
        ),
        CmdletArg(name="list", type="flag", description="List items (default)"),
        CmdletArg(name="play", type="flag", description="Resume playback"),
        CmdletArg(name="pause", type="flag", description="Pause playback"),
        CmdletArg(
            name="save",
            type="flag",
            description="Save current playlist to database",
            requires_db=True,
        ),
        CmdletArg(
            name="load",
            type="flag",
            description="List saved playlists",
            requires_db=True,
        ),
        CmdletArg(
            name="current",
            type="flag",
            description="Emit the currently playing item to pipeline for further processing",
        ),
        CmdletArg(
            name="log",
            type="flag",
            description="Enable pipeable debug output and write an mpv log file",
        ),
        CmdletArg(
            name="borderless",
            type="flag",
            description="Start mpv with no window border (uosc-like overlay feel without fullscreen)",
        ),
    ],
    exec=_run,
)
