"""Subprocess worker that builds the media index.

The FastAPI app starts this worker as a separate Python process so the build can
be cooperatively canceled or force-killed without taking down the API server.
"""

from __future__ import annotations

import argparse
import logging
import os
import time
from pathlib import Path
from typing import Any

from media_library_viewer_api.config import get_settings
from media_library_viewer_api.logging_utils import configure_logging, describe_settings
from media_library_viewer_api.services.media_index import (
    MediaIndex,
    MediaIndexBuildCancelled,
    build_media_index,
)

logger = logging.getLogger(__name__)


def _set_build_metadata(index: MediaIndex, state: dict[str, Any]) -> None:
    for key, value in state.items():
        index.set_metadata(key, "" if value is None else value)


def _cancel_requested(index: MediaIndex) -> bool:
    return index.status().build_cancel_requested


def _start_state(index: MediaIndex, pid: int, library_count: int) -> None:
    _set_build_metadata(
        index,
        {
            "build_running": True,
            "build_stage": "starting",
            "build_message": "Starting media index build",
            "build_progress": None,
            "build_items_processed": 0,
            "build_items_total": 0,
            "build_current_library": "",
            "build_library_index": 0,
            "build_libraries_total": library_count,
            "build_library_progress": None,
            "build_library_items_processed": 0,
            "build_library_items_total": 0,
            "build_elapsed_seconds": None,
            "build_eta_seconds": None,
            "build_library_elapsed_seconds": None,
            "build_library_eta_seconds": None,
            "build_cancel_requested": False,
            "build_pid": pid,
            "build_error": "",
        },
    )


def _progress_callback(index: MediaIndex, pid: int, state: dict[str, Any]) -> None:
    _set_build_metadata(
        index,
        {
            "build_running": True,
            "build_stage": state.get("stage", ""),
            "build_message": state.get("message", ""),
            "build_progress": state.get("progress"),
            "build_items_processed": state.get("processed", 0),
            "build_items_total": state.get("total", 0),
            "build_current_library": state.get("library", ""),
            "build_library_index": state.get("library_index", 0),
            "build_libraries_total": state.get("libraries_total", 0),
            "build_library_progress": state.get("library_progress"),
            "build_library_items_processed": state.get("library_processed", 0),
            "build_library_items_total": state.get("library_total", 0),
            "build_elapsed_seconds": state.get("elapsed_seconds"),
            "build_eta_seconds": state.get("eta_seconds"),
            "build_library_elapsed_seconds": state.get("library_elapsed_seconds"),
            "build_library_eta_seconds": state.get("library_eta_seconds"),
            "build_cancel_requested": False,
            "build_pid": pid,
            "build_error": "",
        },
    )


def _resolve_jellyfin(service_id: str) -> tuple[Any, str]:
    """Resolve the Jellyfin client + user_id from the settings store.

    In a subprocess we cannot use the FastAPI dependency layer (no request),
    so we query the settings store directly. When ``service_id`` is given,
    resolve that specific instance; otherwise fall back to first-enabled.
    """
    from media_library_viewer_api.clients.jellyfin import JellyfinClient
    from media_library_viewer_api.dependencies import _service_record, get_settings_store

    store = get_settings_store()
    service = _service_record(store, "jellyfin", service_id or None)
    if service is None:
        raise RuntimeError("No Jellyfin service is configured. Add a Jellyfin service on the Services page.")
    base_url = str(service.get("config", {}).get("base_url") or "")
    api_key = str(service.get("secrets", {}).get("api_key") or "")
    if not base_url or not api_key:
        raise RuntimeError("Jellyfin service is missing base_url or api_key. Edit it on the Services page.")
    timeout = int(service.get("config", {}).get("timeout_seconds", 10))
    client = JellyfinClient(base_url, api_key, timeout)
    user_id = str(service.get("config", {}).get("user_id") or "")
    if not user_id:
        users = client.users()
        if not users:
            raise RuntimeError("No Jellyfin users found and no user_id configured on the service")
        user_id = users[0]["Id"]
    else:
        # Try the configured user_id directly. It might be the internal
        # Jellyfin Id (a long hash) — in that case libraries() succeeds
        # without an extra users() round-trip. Only if it fails do we
        # resolve it via the users API (the config field accepts usernames
        # like 'admin' too, but Jellyfin's API rejects them on /Users/<id>).
        try:
            client.libraries(user_id)
        except Exception:
            users = client.users()
            match = next((u for u in users if str(u.get("Name", "")) == user_id), None)
            if match:
                resolved = match["Id"]
                logger.info(
                    "Resolved username '%s' to Jellyfin Id '%s'",
                    user_id,
                    resolved,
                )
                user_id = resolved
            elif users:
                user_id = users[0]["Id"]
                logger.warning(
                    "user_id '%s' not found; falling back to first user",
                    service.get("config", {}).get("user_id"),
                )
    return client, user_id


def run_build(final_index_path: str | Path, staging_index_path: str | Path, service_id: str = "") -> int:
    """Run the media index build in a subprocess."""
    settings = get_settings()
    configure_logging(settings.log_level)
    logger.info("Media index worker starting: %s", describe_settings(settings))

    # Create the indexes BEFORE the try block so the except handler can write
    # error state to the DB. If _resolve_jellyfin or client.libraries fails,
    # the worker needs to record the error — otherwise the status stays
    # "queued" forever with no feedback.
    final_index = MediaIndex(final_index_path)
    staging_index = MediaIndex(staging_index_path)
    pid = os.getpid()
    started_at = time.perf_counter()

    staging_path = Path(staging_index.db_path)
    staging_path.unlink(missing_ok=True)

    try:
        client, user_id = _resolve_jellyfin(service_id)
        libraries = client.libraries(user_id)
        logger.info("Media index worker pid=%s libraries=%s", pid, len(libraries))
        _start_state(final_index, pid, len(libraries))

        count = build_media_index(
            client,
            user_id,
            libraries,
            index=staging_index,
            media_root=settings.media_root,
            fallback_prefix=settings.path_prefix,
            progress_callback=lambda state: _progress_callback(final_index, pid, state),
            should_cancel=lambda: _cancel_requested(final_index),
            service_id=service_id,
        )
        # Swap the staging database into place atomically.
        os.replace(staging_index.db_path, final_index.db_path)
        completed_index = MediaIndex(final_index.db_path)
        logger.info("Media index worker completed count=%s", count)
        elapsed = time.perf_counter() - started_at
        completed_status = completed_index.status()
        _set_build_metadata(
            completed_index,
            {
                "build_running": False,
                "build_stage": "completed",
                "build_message": "Media index build complete",
                "build_progress": 1.0,
                "build_items_processed": count,
                "build_items_total": count,
                "build_current_library": "",
                "build_library_index": len(libraries),
                "build_libraries_total": len(libraries),
                "build_library_progress": 1.0,
                "build_library_items_processed": 0,
                "build_library_items_total": 0,
                "build_elapsed_seconds": elapsed,
                "build_eta_seconds": 0.0,
                "build_library_elapsed_seconds": 0.0,
                "build_library_eta_seconds": 0.0,
                "build_cancel_requested": False,
                "build_pid": "",
                "build_error": "",
                # Keep the duration reported by the staging build.
                "build_duration_seconds": completed_status.build_duration_seconds or elapsed,
            },
        )
        return 0
    except MediaIndexBuildCancelled:
        logger.info("Media index worker canceled")
        _set_build_metadata(
            final_index,
            {
                "build_running": False,
                "build_stage": "canceled",
                "build_message": "Media index build canceled",
                "build_cancel_requested": False,
                "build_pid": "",
                "build_error": "",
            },
        )
        return 130
    except Exception as exc:  # pragma: no cover - defensive subprocess error handling
        logger.exception("Media index worker failed")
        _set_build_metadata(
            final_index,
            {
                "build_running": False,
                "build_stage": "error",
                "build_message": "Media index build failed",
                "build_cancel_requested": False,
                "build_pid": "",
                "build_error": str(exc),
            },
        )
        return 1
    finally:
        # If the build did not complete successfully, the staging DB is disposable.
        if staging_path.exists() and staging_path != Path(final_index.db_path):
            try:
                staging_path.unlink()
            except OSError:
                pass


def main() -> int:
    parser = argparse.ArgumentParser(description="Build the media index in a worker process")
    parser.add_argument("--index-path", required=True)
    parser.add_argument("--staging-path", required=True)
    parser.add_argument("--service-id", default="", help="Jellyfin service instance id")
    args = parser.parse_args()
    return run_build(args.index_path, args.staging_path, args.service_id)


if __name__ == "__main__":  # pragma: no cover
    raise SystemExit(main())
