import json
import sqlite3
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from uuid import uuid4


_UNSET = object()


class PlaylistRecord:
    __slots__ = ("id", "name", "cover_image_path", "created_at", "updated_at", "source_ref")

    def __init__(
        self,
        id: str,
        name: str,
        cover_image_path: Optional[str],
        created_at: str,
        updated_at: str,
        source_ref: Optional[str] = None,
    ):
        self.id = id
        self.name = name
        self.cover_image_path = cover_image_path
        self.created_at = created_at
        self.updated_at = updated_at
        self.source_ref = source_ref


class PlaylistSummaryRecord:
    __slots__ = (
        "id", "name", "cover_image_path", "created_at", "updated_at",
        "track_count", "total_duration", "cover_urls", "source_ref",
    )

    def __init__(
        self,
        id: str,
        name: str,
        cover_image_path: Optional[str],
        created_at: str,
        updated_at: str,
        track_count: int,
        total_duration: Optional[int],
        cover_urls: list[str],
        source_ref: Optional[str] = None,
    ):
        self.id = id
        self.name = name
        self.cover_image_path = cover_image_path
        self.created_at = created_at
        self.updated_at = updated_at
        self.track_count = track_count
        self.total_duration = total_duration
        self.cover_urls = cover_urls
        self.source_ref = source_ref


class PlaylistTrackRecord:
    __slots__ = (
        "id", "playlist_id", "position", "track_name", "artist_name",
        "album_name", "album_id", "artist_id", "track_source_id", "cover_url",
        "source_type", "available_sources", "format", "track_number", "disc_number",
        "duration", "created_at", "plex_rating_key",
    )

    def __init__(
        self,
        id: str,
        playlist_id: str,
        position: int,
        track_name: str,
        artist_name: str,
        album_name: str,
        album_id: Optional[str],
        artist_id: Optional[str],
        track_source_id: Optional[str],
        cover_url: Optional[str],
        source_type: str,
        available_sources: Optional[list[str]],
        format: Optional[str],
        track_number: Optional[int],
        disc_number: Optional[int],
        duration: Optional[int],
        created_at: str,
        plex_rating_key: Optional[str] = None,
    ):
        self.id = id
        self.playlist_id = playlist_id
        self.position = position
        self.track_name = track_name
        self.artist_name = artist_name
        self.album_name = album_name
        self.album_id = album_id
        self.artist_id = artist_id
        self.track_source_id = track_source_id
        self.cover_url = cover_url
        self.source_type = source_type
        self.available_sources = available_sources
        self.format = format
        self.track_number = track_number
        self.disc_number = disc_number
        self.duration = duration
        self.created_at = created_at
        self.plex_rating_key = plex_rating_key

def get_cache_dir() -> Path:
      from core.config import get_settings
      return get_settings().library_db_path

class PlaylistRepository:
    def __init__(self, db_path: Path = get_cache_dir()):
        self.db_path = db_path
        self._local = threading.local()
        self._write_lock = threading.Lock()
        self._ensure_tables()

    def _get_connection(self) -> sqlite3.Connection:
        if not hasattr(self._local, "conn") or self._local.conn is None:
            conn = sqlite3.connect(self.db_path, check_same_thread=False)
            conn.execute("PRAGMA journal_mode=WAL")
            conn.execute("PRAGMA synchronous=NORMAL")
            conn.execute("PRAGMA foreign_keys=ON")
            conn.row_factory = sqlite3.Row
            self._local.conn = conn
        return self._local.conn

    def _ensure_tables(self) -> None:
        conn = sqlite3.connect(self.db_path)
        try:
            conn.execute("PRAGMA journal_mode=WAL")
            conn.execute("PRAGMA foreign_keys=ON")
            try:
                conn.execute(
                    "ALTER TABLE playlist_tracks RENAME COLUMN video_id TO track_source_id"
                )
                conn.commit()
            except sqlite3.OperationalError:
                pass
            conn.execute("""
                CREATE TABLE IF NOT EXISTS playlists (
                    id TEXT PRIMARY KEY,
                    name TEXT NOT NULL,
                    cover_image_path TEXT,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS playlist_tracks (
                    id TEXT PRIMARY KEY,
                    playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
                    position INTEGER NOT NULL,
                    track_name TEXT NOT NULL,
                    artist_name TEXT NOT NULL,
                    album_name TEXT NOT NULL,
                    album_id TEXT,
                    artist_id TEXT,
                    track_source_id TEXT,
                    cover_url TEXT,
                    source_type TEXT NOT NULL,
                    available_sources TEXT,
                    format TEXT,
                    track_number INTEGER,
                    duration INTEGER,
                    created_at TEXT NOT NULL,
                    UNIQUE(playlist_id, position)
                )
            """)
            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_playlist_tracks_playlist_position
                ON playlist_tracks(playlist_id, position)
            """)
            try:
                conn.execute("ALTER TABLE playlist_tracks ADD COLUMN disc_number INTEGER")
                conn.commit()
            except sqlite3.OperationalError:
                pass
            try:
                conn.execute("ALTER TABLE playlist_tracks ADD COLUMN plex_rating_key TEXT")
                conn.commit()
            except sqlite3.OperationalError:
                pass
            conn.execute("""
                UPDATE playlist_tracks
                SET cover_url = '/api/v1/covers/' || SUBSTR(cover_url, LENGTH('/api/covers/') + 1)
                WHERE cover_url LIKE '/api/covers/%'
            """)
            try:
                conn.execute("ALTER TABLE playlists ADD COLUMN source_ref TEXT")
                conn.commit()
            except sqlite3.OperationalError:
                pass
            conn.execute("""
                CREATE UNIQUE INDEX IF NOT EXISTS idx_playlists_source_ref
                ON playlists(source_ref) WHERE source_ref IS NOT NULL
            """)
            conn.commit()
        finally:
            conn.close()


    def create_playlist(self, name: str, source_ref: Optional[str] = None) -> PlaylistRecord:
        playlist_id = str(uuid4())
        now = datetime.now(timezone.utc).isoformat()
        with self._write_lock:
            conn = self._get_connection()
            conn.execute(
                "INSERT INTO playlists (id, name, cover_image_path, created_at, updated_at, source_ref) "
                "VALUES (?, ?, NULL, ?, ?, ?)",
                (playlist_id, name, now, now, source_ref),
            )
            conn.commit()
        return PlaylistRecord(
            id=playlist_id, name=name, cover_image_path=None,
            created_at=now, updated_at=now, source_ref=source_ref,
        )

    def get_playlist(self, playlist_id: str) -> Optional[PlaylistRecord]:
        conn = self._get_connection()
        row = conn.execute(
            "SELECT * FROM playlists WHERE id = ?", (playlist_id,)
        ).fetchone()
        return self._row_to_playlist(row) if row else None

    def get_by_source_ref(self, source_ref: str) -> Optional[PlaylistRecord]:
        conn = self._get_connection()
        row = conn.execute(
            "SELECT * FROM playlists WHERE source_ref = ?", (source_ref,)
        ).fetchone()
        return self._row_to_playlist(row) if row else None

    def get_imported_source_ids(self, prefix: str) -> set[str]:
        """Return the set of source IDs imported for a given prefix (e.g. 'plex:')."""
        conn = self._get_connection()
        rows = conn.execute(
            "SELECT source_ref FROM playlists WHERE source_ref LIKE ?",
            (f"{prefix}%",),
        ).fetchall()
        plen = len(prefix)
        return {row["source_ref"][plen:] for row in rows if row["source_ref"]}

    def get_all_playlists(self) -> list[PlaylistSummaryRecord]:
        conn = self._get_connection()
        rows = conn.execute("""
            SELECT p.*,
                   COUNT(pt.id) AS track_count,
                   SUM(pt.duration) AS total_duration
            FROM playlists p
            LEFT JOIN playlist_tracks pt ON pt.playlist_id = p.id
            GROUP BY p.id
            ORDER BY p.updated_at DESC
        """).fetchall()

        results: list[PlaylistSummaryRecord] = []
        for row in rows:
            cover_urls = [
                r["cover_url"] for r in conn.execute(
                    "SELECT DISTINCT cover_url FROM playlist_tracks "
                    "WHERE playlist_id = ? AND cover_url IS NOT NULL LIMIT 4",
                    (row["id"],),
                ).fetchall()
            ]
            results.append(PlaylistSummaryRecord(
                id=row["id"],
                name=row["name"],
                cover_image_path=row["cover_image_path"],
                created_at=row["created_at"],
                updated_at=row["updated_at"],
                track_count=row["track_count"],
                total_duration=row["total_duration"],
                cover_urls=cover_urls,
                source_ref=row["source_ref"],
            ))
        return results

    def update_playlist(
        self,
        playlist_id: str,
        name: Optional[str] = None,
        cover_image_path: Optional[str] = _UNSET,
    ) -> Optional[PlaylistRecord]:
        with self._write_lock:
            conn = self._get_connection()
            existing = conn.execute(
                "SELECT * FROM playlists WHERE id = ?", (playlist_id,)
            ).fetchone()
            if not existing:
                return None

            new_name = name if name is not None else existing["name"]
            new_cover = (
                cover_image_path
                if cover_image_path is not _UNSET
                else existing["cover_image_path"]
            )
            now = datetime.now(timezone.utc).isoformat()

            conn.execute(
                "UPDATE playlists SET name = ?, cover_image_path = ?, updated_at = ? WHERE id = ?",
                (new_name, new_cover, now, playlist_id),
            )
            conn.commit()
        return PlaylistRecord(
            id=playlist_id, name=new_name, cover_image_path=new_cover,
            created_at=existing["created_at"], updated_at=now,
        )

    def delete_playlist(self, playlist_id: str) -> bool:
        with self._write_lock:
            conn = self._get_connection()
            cursor = conn.execute(
                "DELETE FROM playlists WHERE id = ?", (playlist_id,)
            )
            conn.commit()
            return cursor.rowcount > 0

    def add_tracks(
        self,
        playlist_id: str,
        tracks: list[dict],
        position: Optional[int] = None,
    ) -> list[PlaylistTrackRecord]:
        if not tracks:
            return []

        now = datetime.now(timezone.utc).isoformat()
        created_records: list[PlaylistTrackRecord] = []

        with self._write_lock:
            conn = self._get_connection()

            max_row = conn.execute(
                "SELECT MAX(position) FROM playlist_tracks WHERE playlist_id = ?",
                (playlist_id,),
            ).fetchone()
            current_max = max_row[0] if max_row[0] is not None else -1

            if position is None or position > current_max + 1:
                insert_at = current_max + 1
            else:
                insert_at = max(0, position)
                shift_count = len(tracks)
                rows_to_shift = conn.execute(
                    "SELECT id, position FROM playlist_tracks "
                    "WHERE playlist_id = ? AND position >= ? "
                    "ORDER BY position DESC",
                    (playlist_id, insert_at),
                ).fetchall()
                for idx, r in enumerate(rows_to_shift):
                    conn.execute(
                        "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                        (-(idx + 1), r["id"]),
                    )
                for r in rows_to_shift:
                    conn.execute(
                        "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                        (r["position"] + shift_count, r["id"]),
                    )

            for i, track in enumerate(tracks):
                track_id = str(uuid4())
                pos = insert_at + i
                available_sources_json = (
                    json.dumps(track["available_sources"])
                    if track.get("available_sources") else None
                )
                conn.execute(
                    "INSERT INTO playlist_tracks "
                    "(id, playlist_id, position, track_name, artist_name, album_name, "
                    "album_id, artist_id, track_source_id, cover_url, source_type, "
                    "available_sources, format, track_number, disc_number, duration, "
                    "plex_rating_key, created_at) "
                    "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                    (
                        track_id, playlist_id, pos,
                        track["track_name"], track["artist_name"], track["album_name"],
                        track.get("album_id"), track.get("artist_id"),
                        track.get("track_source_id"), track.get("cover_url"),
                        track["source_type"], available_sources_json,
                        track.get("format"), track.get("track_number"), track.get("disc_number"),
                        track.get("duration"), track.get("plex_rating_key"), now,
                    ),
                )
                created_records.append(PlaylistTrackRecord(
                    id=track_id, playlist_id=playlist_id, position=pos,
                    track_name=track["track_name"], artist_name=track["artist_name"],
                    album_name=track["album_name"], album_id=track.get("album_id"),
                    artist_id=track.get("artist_id"), track_source_id=track.get("track_source_id"),
                    cover_url=track.get("cover_url"), source_type=track["source_type"],
                    available_sources=track.get("available_sources"),
                    format=track.get("format"), track_number=track.get("track_number"),
                    disc_number=track.get("disc_number"),
                    duration=track.get("duration"), created_at=now,
                    plex_rating_key=track.get("plex_rating_key"),
                ))

            conn.execute(
                "UPDATE playlists SET updated_at = ? WHERE id = ?",
                (now, playlist_id),
            )
            conn.commit()

        return created_records

    def remove_track(self, playlist_id: str, track_id: str) -> bool:
        with self._write_lock:
            conn = self._get_connection()
            row = conn.execute(
                "SELECT position FROM playlist_tracks WHERE id = ? AND playlist_id = ?",
                (track_id, playlist_id),
            ).fetchone()
            if not row:
                return False

            removed_pos = row["position"]
            conn.execute(
                "DELETE FROM playlist_tracks WHERE id = ? AND playlist_id = ?",
                (track_id, playlist_id),
            )
            rows_to_shift = conn.execute(
                "SELECT id, position FROM playlist_tracks "
                "WHERE playlist_id = ? AND position > ? "
                "ORDER BY position ASC",
                (playlist_id, removed_pos),
            ).fetchall()
            for r in rows_to_shift:
                conn.execute(
                    "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                    (r["position"] - 1, r["id"]),
                )
            now = datetime.now(timezone.utc).isoformat()
            conn.execute(
                "UPDATE playlists SET updated_at = ? WHERE id = ?",
                (now, playlist_id),
            )
            conn.commit()
            return True

    def remove_tracks(self, playlist_id: str, track_ids: list[str]) -> int:
        if not track_ids:
            return 0
        with self._write_lock:
            conn = self._get_connection()
            placeholders = ",".join("?" for _ in track_ids)
            existing = conn.execute(
                f"SELECT id FROM playlist_tracks WHERE playlist_id = ? AND id IN ({placeholders})",
                [playlist_id, *track_ids],
            ).fetchall()
            if not existing:
                return 0
            ids_to_remove = [r["id"] for r in existing]
            rm_placeholders = ",".join("?" for _ in ids_to_remove)
            conn.execute(
                f"DELETE FROM playlist_tracks WHERE playlist_id = ? AND id IN ({rm_placeholders})",
                [playlist_id, *ids_to_remove],
            )
            remaining = conn.execute(
                "SELECT id FROM playlist_tracks WHERE playlist_id = ? ORDER BY position ASC",
                (playlist_id,),
            ).fetchall()
            for new_pos, row in enumerate(remaining):
                conn.execute(
                    "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                    (new_pos, row["id"]),
                )
            now = datetime.now(timezone.utc).isoformat()
            conn.execute(
                "UPDATE playlists SET updated_at = ? WHERE id = ?",
                (now, playlist_id),
            )
            conn.commit()
            return len(ids_to_remove)

    def reorder_track(self, playlist_id: str, track_id: str, new_position: int) -> Optional[int]:
        with self._write_lock:
            conn = self._get_connection()
            row = conn.execute(
                "SELECT position FROM playlist_tracks WHERE id = ? AND playlist_id = ?",
                (track_id, playlist_id),
            ).fetchone()
            if not row:
                return None

            old_position = row["position"]

            max_row = conn.execute(
                "SELECT MAX(position) FROM playlist_tracks WHERE playlist_id = ?",
                (playlist_id,),
            ).fetchone()
            max_pos = max_row[0] if max_row[0] is not None else 0
            actual_position = min(new_position, max_pos)

            if old_position == actual_position:
                return actual_position

            conn.execute(
                "UPDATE playlist_tracks SET position = -1 WHERE id = ?",
                (track_id,),
            )

            if old_position < actual_position:
                rows = conn.execute(
                    "SELECT id, position FROM playlist_tracks "
                    "WHERE playlist_id = ? AND position > ? AND position <= ? "
                    "ORDER BY position ASC",
                    (playlist_id, old_position, actual_position),
                ).fetchall()
                for r in rows:
                    conn.execute(
                        "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                        (r["position"] - 1, r["id"]),
                    )
            else:
                rows = conn.execute(
                    "SELECT id, position FROM playlist_tracks "
                    "WHERE playlist_id = ? AND position >= ? AND position < ? "
                    "ORDER BY position DESC",
                    (playlist_id, actual_position, old_position),
                ).fetchall()
                for r in rows:
                    conn.execute(
                        "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                        (r["position"] + 1, r["id"]),
                    )

            conn.execute(
                "UPDATE playlist_tracks SET position = ? WHERE id = ?",
                (actual_position, track_id),
            )

            now = datetime.now(timezone.utc).isoformat()
            conn.execute(
                "UPDATE playlists SET updated_at = ? WHERE id = ?",
                (now, playlist_id),
            )
            conn.commit()
            return actual_position

    def batch_update_available_sources(
        self,
        playlist_id: str,
        updates: dict[str, list[str]],
    ) -> int:
        if not updates:
            return 0
        updated = 0
        with self._write_lock:
            conn = self._get_connection()
            for track_id, sources in updates.items():
                conn.execute(
                    "UPDATE playlist_tracks SET available_sources = ? "
                    "WHERE id = ? AND playlist_id = ?",
                    (json.dumps(sources), track_id, playlist_id),
                )
                updated += 1
            if updated:
                now = datetime.now(timezone.utc).isoformat()
                conn.execute(
                    "UPDATE playlists SET updated_at = ? WHERE id = ?",
                    (now, playlist_id),
                )
                conn.commit()
        return updated

    def update_track_source(
        self,
        playlist_id: str,
        track_id: str,
        source_type: Optional[str] = None,
        available_sources: Optional[list[str]] = None,
        track_source_id: Optional[str] = None,
        plex_rating_key: Optional[str] = _UNSET,
    ) -> Optional[PlaylistTrackRecord]:
        with self._write_lock:
            conn = self._get_connection()
            row = conn.execute(
                "SELECT * FROM playlist_tracks WHERE id = ? AND playlist_id = ?",
                (track_id, playlist_id),
            ).fetchone()
            if not row:
                return None

            new_source_type = source_type if source_type is not None else row["source_type"]
            new_available = (
                json.dumps(available_sources)
                if available_sources is not None
                else row["available_sources"]
            )
            new_track_source_id = track_source_id if track_source_id is not None else row["track_source_id"]
            new_plex_rating_key = (
                plex_rating_key
                if plex_rating_key is not _UNSET
                else (row["plex_rating_key"] if "plex_rating_key" in row.keys() else None)
            )

            conn.execute(
                "UPDATE playlist_tracks SET source_type = ?, available_sources = ?, "
                "track_source_id = ?, plex_rating_key = ? "
                "WHERE id = ? AND playlist_id = ?",
                (new_source_type, new_available, new_track_source_id, new_plex_rating_key,
                 track_id, playlist_id),
            )
            now = datetime.now(timezone.utc).isoformat()
            conn.execute(
                "UPDATE playlists SET updated_at = ? WHERE id = ?",
                (now, playlist_id),
            )
            conn.commit()

        avail_parsed = (
            available_sources
            if available_sources is not None
            else self._parse_available_sources(row["available_sources"])
        )
        return PlaylistTrackRecord(
            id=row["id"], playlist_id=row["playlist_id"], position=row["position"],
            track_name=row["track_name"], artist_name=row["artist_name"],
            album_name=row["album_name"], album_id=row["album_id"],
            artist_id=row["artist_id"], track_source_id=new_track_source_id,
            cover_url=row["cover_url"], source_type=new_source_type,
            available_sources=avail_parsed, format=row["format"],
            track_number=row["track_number"],
            disc_number=row["disc_number"] if "disc_number" in row.keys() else None,
            duration=row["duration"],
            created_at=row["created_at"],
            plex_rating_key=new_plex_rating_key,
        )

    def get_tracks(self, playlist_id: str) -> list[PlaylistTrackRecord]:
        conn = self._get_connection()
        rows = conn.execute(
            "SELECT * FROM playlist_tracks WHERE playlist_id = ? ORDER BY position",
            (playlist_id,),
        ).fetchall()
        return [self._row_to_track(r) for r in rows]

    def get_track(self, playlist_id: str, track_id: str) -> Optional[PlaylistTrackRecord]:
        conn = self._get_connection()
        row = conn.execute(
            "SELECT * FROM playlist_tracks WHERE id = ? AND playlist_id = ?",
            (track_id, playlist_id),
        ).fetchone()
        if not row:
            return None
        return self._row_to_track(row)

    def check_track_membership(
        self, tracks: list[tuple[str, str, str]],
    ) -> dict[str, list[int]]:
        """Check which playlists already contain the given tracks.

        Args:
            tracks: list of (track_name, artist_name, album_name) tuples.

        Returns:
            dict mapping playlist_id to list of input indices that are already present.
        """
        if not tracks:
            return {}

        conn = self._get_connection()
        rows = conn.execute(
            "SELECT playlist_id, LOWER(track_name) AS tn, "
            "LOWER(artist_name) AS an, LOWER(album_name) AS aln "
            "FROM playlist_tracks"
        ).fetchall()

        lookup: dict[str, set[tuple[str, str, str]]] = {}
        for row in rows:
            pid = row["playlist_id"]
            key = (row["tn"], row["an"], row["aln"])
            lookup.setdefault(pid, set()).add(key)

        result: dict[str, list[int]] = {}
        normalised = [
            (t[0].lower(), t[1].lower(), t[2].lower()) for t in tracks
        ]
        for pid, existing in lookup.items():
            matched = [i for i, t in enumerate(normalised) if t in existing]
            if matched:
                result[pid] = matched
        return result


    def close(self) -> None:
        conn = getattr(self._local, "conn", None)
        if conn is not None:
            try:
                conn.close()
            except sqlite3.Error:
                pass
            self._local.conn = None


    @staticmethod
    def _parse_available_sources(raw: Optional[str]) -> Optional[list[str]]:
        if raw is None:
            return None
        try:
            return json.loads(raw)
        except (json.JSONDecodeError, TypeError):
            return None

    @staticmethod
    def _row_to_playlist(row: sqlite3.Row) -> PlaylistRecord:
        return PlaylistRecord(
            id=row["id"],
            name=row["name"],
            cover_image_path=row["cover_image_path"],
            created_at=row["created_at"],
            updated_at=row["updated_at"],
            source_ref=row["source_ref"] if "source_ref" in row.keys() else None,
        )

    @classmethod
    def _row_to_track(cls, row: sqlite3.Row) -> PlaylistTrackRecord:
        return PlaylistTrackRecord(
            id=row["id"],
            playlist_id=row["playlist_id"],
            position=row["position"],
            track_name=row["track_name"],
            artist_name=row["artist_name"],
            album_name=row["album_name"],
            album_id=row["album_id"],
            artist_id=row["artist_id"],
            track_source_id=row["track_source_id"],
            cover_url=row["cover_url"],
            source_type=row["source_type"],
            available_sources=cls._parse_available_sources(row["available_sources"]),
            format=row["format"],
            track_number=row["track_number"],
            disc_number=row["disc_number"] if "disc_number" in row.keys() else None,
            duration=row["duration"],
            created_at=row["created_at"],
            plex_rating_key=row["plex_rating_key"] if "plex_rating_key" in row.keys() else None,
        )
