"""SQLite-backed media inventory service.

This is the main step toward a frontend-agnostic architecture. The Streamlit UI
asks this service to build/query an index, but the same class could be exposed
through FastAPI to a React frontend without rewriting Jellyfin indexing logic.
"""

from __future__ import annotations

import sqlite3
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable

from media_library_viewer.clients.jellyfin import JellyfinClient
from media_library_viewer.domain.media import display_media_row, normalize_media_item

# Local generated database. It is ignored by git and can be rebuilt from
# Jellyfin metadata whenever needed.
DEFAULT_INDEX_PATH = Path(".cache/media_library_viewer/media_index.sqlite")
MEDIA_TYPES = "Movie,Episode,Video"

# Only values from this whitelist are interpolated into ORDER BY. User-selected
# sort keys map to these known SQL snippets to avoid SQL injection.
SORT_COLUMNS = {
    "title": "title COLLATE NOCASE",
    "series": "series COLLATE NOCASE",
    "season": "season_number",
    "episode": "episode",
    "type": "type COLLATE NOCASE",
    "year": "year",
    "runtime": "runtime_min",
    "size": "size_bytes",
    "bitrate": "bitrate_bps",
    "hdr": "hdr",
    "video": "video COLLATE NOCASE",
    "resolution": "height",
    "date_added": "date_added_ts",
    "library": "library_name COLLATE NOCASE",
    "path": "path COLLATE NOCASE",
}


@dataclass(frozen=True)
class MediaIndexStatus:
    """Lightweight status object displayed by the Media tab."""

    exists: bool
    item_count: int = 0
    updated_at: int | None = None
    updated_at_label: str = ""
    build_duration_seconds: float | None = None


class MediaIndex:
    """SQLite-backed media inventory.

    This class is UI-framework independent. Streamlit, a future FastAPI backend,
    or a React-facing API can all use this service.
    """

    def __init__(self, db_path: Path | str = DEFAULT_INDEX_PATH):
        self.db_path = Path(db_path)
        self.db_path.parent.mkdir(parents=True, exist_ok=True)

    def connect(self) -> sqlite3.Connection:
        """Open a sqlite connection configured to return Row objects."""
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        return conn

    def init_schema(self) -> None:
        """Create tables/indexes if this is the first use of the index."""
        with self.connect() as conn:
            conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS media_items (
                    id TEXT PRIMARY KEY,
                    title TEXT,
                    series TEXT,
                    season TEXT,
                    season_number INTEGER,
                    episode INTEGER,
                    type TEXT,
                    year INTEGER,
                    runtime_ticks INTEGER,
                    runtime_min INTEGER,
                    size_bytes INTEGER,
                    bitrate_bps INTEGER,
                    hdr INTEGER,
                    video TEXT,
                    width INTEGER,
                    height INTEGER,
                    resolution TEXT,
                    date_added TEXT,
                    date_added_ts INTEGER,
                    path TEXT,
                    library_id TEXT,
                    library_name TEXT
                );
                CREATE TABLE IF NOT EXISTS index_metadata (
                    key TEXT PRIMARY KEY,
                    value TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_media_type ON media_items(type);
                CREATE INDEX IF NOT EXISTS idx_media_library ON media_items(library_id);
                CREATE INDEX IF NOT EXISTS idx_media_title ON media_items(title COLLATE NOCASE);
                CREATE INDEX IF NOT EXISTS idx_media_series ON media_items(series COLLATE NOCASE);
                CREATE INDEX IF NOT EXISTS idx_media_date_added ON media_items(date_added_ts);
                CREATE INDEX IF NOT EXISTS idx_media_size ON media_items(size_bytes);
                CREATE INDEX IF NOT EXISTS idx_media_bitrate ON media_items(bitrate_bps);
                """
            )

    def set_metadata(self, key: str, value: str | int | float) -> None:
        """Store a small string metadata value, e.g. build duration."""
        self.init_schema()
        with self.connect() as conn:
            conn.execute(
                "INSERT OR REPLACE INTO index_metadata (key, value) VALUES (?, ?)",
                (key, str(value)),
            )

    def replace_items(self, rows: Iterable[dict[str, Any]]) -> int:
        """Atomically replace indexed media rows with a freshly built set."""
        self.init_schema()
        row_list = list(rows)
        columns = [
            "id",
            "title",
            "series",
            "season",
            "season_number",
            "episode",
            "type",
            "year",
            "runtime_ticks",
            "runtime_min",
            "size_bytes",
            "bitrate_bps",
            "hdr",
            "video",
            "width",
            "height",
            "resolution",
            "date_added",
            "date_added_ts",
            "path",
            "library_id",
            "library_name",
        ]
        placeholders = ",".join(["?"] * len(columns))
        with self.connect() as conn:
            conn.execute("DELETE FROM media_items")
            conn.executemany(
                f"INSERT OR REPLACE INTO media_items ({','.join(columns)}) VALUES ({placeholders})",
                [[row.get(column) for column in columns] for row in row_list],
            )
            conn.execute(
                "INSERT OR REPLACE INTO index_metadata (key, value) VALUES ('updated_at', ?)",
                (str(int(time.time())),),
            )
        return len(row_list)

    def status(self) -> MediaIndexStatus:
        """Return existence, count, update time, and last build duration."""
        if not self.db_path.exists():
            return MediaIndexStatus(exists=False)
        try:
            with self.connect() as conn:
                item_count = int(conn.execute("SELECT COUNT(*) FROM media_items").fetchone()[0])
                updated_row = conn.execute("SELECT value FROM index_metadata WHERE key='updated_at'").fetchone()
                duration_row = conn.execute("SELECT value FROM index_metadata WHERE key='build_duration_seconds'").fetchone()
        except sqlite3.Error:
            return MediaIndexStatus(exists=False)
        updated_at = int(updated_row[0]) if updated_row and str(updated_row[0]).isdigit() else None
        label = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(updated_at)) if updated_at else ""
        build_duration = None
        if duration_row:
            try:
                build_duration = float(duration_row[0])
            except (TypeError, ValueError):
                build_duration = None
        return MediaIndexStatus(
            exists=True,
            item_count=item_count,
            updated_at=updated_at,
            updated_at_label=label,
            build_duration_seconds=build_duration,
        )

    def query(
        self,
        library_id: str | None = None,
        library_ids: list[str] | None = None,
        media_types: list[str] | None = None,
        search: str = "",
        hdr_filter: str = "All",
        sort_key: str = "title",
        sort_order: str = "Ascending",
        limit: int = 100,
        offset: int = 0,
    ) -> tuple[list[dict[str, Any]], int]:
        """Query indexed media with full-index filters, sorting, and pagination."""
        self.init_schema()
        where = []
        params: list[Any] = []
        if library_ids:
            where.append("library_id IN (" + ",".join(["?"] * len(library_ids)) + ")")
            params.extend(library_ids)
        elif library_id:
            where.append("library_id = ?")
            params.append(library_id)
        if media_types:
            where.append("type IN (" + ",".join(["?"] * len(media_types)) + ")")
            params.extend(media_types)
        if search:
            needle = f"%{search.lower()}%"
            where.append("(LOWER(title) LIKE ? OR LOWER(series) LIKE ? OR LOWER(path) LIKE ?)")
            params.extend([needle, needle, needle])
        if hdr_filter == "HDR only":
            where.append("hdr = 1")
        elif hdr_filter == "SDR/unknown only":
            where.append("(hdr IS NULL OR hdr = 0)")

        where_sql = " WHERE " + " AND ".join(where) if where else ""
        sort_sql = SORT_COLUMNS.get(sort_key, SORT_COLUMNS["title"])
        direction = "DESC" if sort_order == "Descending" else "ASC"
        # Always add stable tie-breakers.
        order_sql = f" ORDER BY {sort_sql} {direction}, series COLLATE NOCASE ASC, season_number ASC, episode ASC, title COLLATE NOCASE ASC"

        with self.connect() as conn:
            total = int(conn.execute("SELECT COUNT(*) FROM media_items" + where_sql, params).fetchone()[0])
            rows = conn.execute(
                "SELECT * FROM media_items" + where_sql + order_sql + " LIMIT ? OFFSET ?",
                [*params, int(limit), int(offset)],
            ).fetchall()
        return [display_media_row(dict(row)) for row in rows], total


def build_media_index(
    client: JellyfinClient,
    user_id: str,
    libraries: list[dict[str, Any]],
    index: MediaIndex | None = None,
    page_size: int = 500,
) -> int:
    """Fetch Jellyfin pages for all selected libraries and rebuild the index."""
    index = index or MediaIndex()
    started_at = time.perf_counter()
    normalized_rows: list[dict[str, Any]] = []
    for library in libraries:
        library_id = library.get("Id")
        library_name = library.get("Name", "")
        if not library_id:
            continue
        start = 0
        while True:
            response = client.items(
                user_id=user_id,
                parent_id=library_id,
                start_index=start,
                limit=page_size,
                include_item_types=MEDIA_TYPES,
                recursive=True,
                sort_by="SortName",
                sort_order="Ascending",
            )
            items = response.get("Items", [])
            normalized_rows.extend(normalize_media_item(item, library_id, library_name) for item in items)
            start += len(items)
            total = int(response.get("TotalRecordCount", start))
            if not items or start >= total:
                break
    count = index.replace_items(normalized_rows)
    index.set_metadata("build_duration_seconds", f"{time.perf_counter() - started_at:.3f}")
    return count
