-- ============================================================================
-- SoulSync — VIDEO side schema  (database/video_library.db)
--
-- ISOLATION: this is a SEPARATE SQLite file from the music library. The video
-- code owns it exclusively; music never opens it and it never references music
-- tables. A bug, migration, or reset here cannot touch music data, and the two
-- never contend for the same write lock.
--
-- DESIGN PRINCIPLES (deliberately avoiding the music DB's known pain points):
--   * No polymorphic (entity_type, entity_id) keys. Where a row can belong to a
--     movie OR an episode OR a youtube video, we use separate nullable FKs with
--     a CHECK that exactly one is set — real foreign keys, real cascades.
--   * No "source id" blob / naming spaghetti. External ids are a few explicit,
--     well-named, indexed columns (tmdb_id, tvdb_id, imdb_id, youtube_id).
--   * No metadata dumping-ground column. Structured config that is genuinely a
--     list (quality profile contents) is small, ordered JSON; everything else
--     is a real column.
--   * Watchlist / Wishlist / Calendar are DERIVED VIEWS over monitored + file
--     state, not standalone tables — so they can't drift out of sync with the
--     library the way a duplicated table does. (See note in design summary.)
--
-- Run order matters (FKs reference earlier tables). The init module executes
-- this whole file inside one transaction with foreign_keys ON.
-- ============================================================================

-- ── Meta ────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS meta (
    key   TEXT PRIMARY KEY,
    value TEXT
);

-- ── Configuration ───────────────────────────────────────────────────────────
-- Root folders: where each kind of library content is stored on disk.
CREATE TABLE IF NOT EXISTS root_folders (
    id           INTEGER PRIMARY KEY,
    path         TEXT NOT NULL UNIQUE,
    content_kind TEXT NOT NULL CHECK (content_kind IN ('movie', 'show', 'youtube')),
    created_at   TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Quality profiles: an ordered, named acceptance ladder (Radarr/Sonarr-style).
-- `items` is a small JSON array of allowed quality names, best-first; `cutoff`
-- is the name we stop upgrading at. JSON is appropriate here — it is genuinely
-- an ordered list of config, not a metadata grab-bag.
CREATE TABLE IF NOT EXISTS quality_profiles (
    id         INTEGER PRIMARY KEY,
    name       TEXT NOT NULL UNIQUE,
    cutoff     TEXT,
    items      TEXT NOT NULL DEFAULT '[]',
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Video-side settings. KEY/VALUE for now; at the end-of-branch settings.db
-- consolidation these migrate into the shared config store. Value is JSON.
CREATE TABLE IF NOT EXISTS video_settings (
    key        TEXT PRIMARY KEY,
    value      TEXT,
    updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- ── Content: Movies ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS movies (
    id                   INTEGER PRIMARY KEY,
    server_source        TEXT,            -- 'plex' | 'jellyfin' (NULL = not on a server yet, e.g. wishlist)
    server_id            TEXT,            -- media server native id (Plex ratingKey / Jellyfin Item Id)
    tmdb_id              INTEGER,         -- not unique: same film can sit in >1 library
    imdb_id              TEXT,
    tmdb_match_status    TEXT,            -- enrichment: NULL=pending | matched | not_found | error
    tmdb_last_attempted  TEXT,
    title                TEXT NOT NULL,
    sort_title           TEXT,
    year                 INTEGER,
    overview             TEXT,
    runtime_minutes      INTEGER,
    status               TEXT,            -- announced | in_production | released
    release_date         TEXT,            -- primary/theatrical (ISO date)
    digital_release_date TEXT,
    studio               TEXT,
    content_rating       TEXT,            -- e.g. PG-13
    tagline              TEXT,
    rating               REAL,            -- TMDB audience score (0-10)
    rating_critic        REAL,            -- critic score (0-100) when offered
    imdb_rating          REAL,            -- IMDb (0-10, via OMDb)
    rt_rating            INTEGER,         -- Rotten Tomatoes (0-100)
    metacritic           INTEGER,         -- Metacritic (0-100)
    ratings_synced       INTEGER NOT NULL DEFAULT 0,   -- OMDb ratings fetched?
    poster_url           TEXT,
    backdrop_url         TEXT,
    logo_url             TEXT,            -- transparent title logo (clearlogo)
    monitored            INTEGER NOT NULL DEFAULT 1,   -- tracked for acquisition
    has_file             INTEGER NOT NULL DEFAULT 0,   -- owned? (denormalized)
    quality_profile_id   INTEGER REFERENCES quality_profiles(id) ON DELETE SET NULL,
    root_folder_id       INTEGER REFERENCES root_folders(id)     ON DELETE SET NULL,
    path                 TEXT,            -- folder on disk once owned
    added_at             TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at           TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_movies_tmdb       ON movies(tmdb_id);
CREATE INDEX IF NOT EXISTS idx_movies_monitored  ON movies(monitored, has_file);
CREATE INDEX IF NOT EXISTS idx_movies_release    ON movies(release_date);
-- Upsert/stale-removal key: the server's native id. Multiple NULLs are allowed
-- (wishlist items not yet on a server), so this never blocks non-server rows.
CREATE UNIQUE INDEX IF NOT EXISTS ux_movies_server ON movies(server_source, server_id);

-- ── Content: TV (shows → seasons → episodes) ────────────────────────────────
CREATE TABLE IF NOT EXISTS shows (
    id                 INTEGER PRIMARY KEY,
    server_source      TEXT,             -- 'plex' | 'jellyfin' (NULL = not on a server yet)
    server_id          TEXT,             -- media server native id
    tvdb_id            INTEGER,          -- not unique (same series can sit in >1 library)
    tmdb_id            INTEGER,
    imdb_id            TEXT,
    tmdb_match_status  TEXT,             -- enrichment match state per source
    tmdb_last_attempted TEXT,
    tvdb_match_status  TEXT,
    tvdb_last_attempted TEXT,
    title              TEXT NOT NULL,
    sort_title         TEXT,
    year               INTEGER,
    overview           TEXT,
    status             TEXT,             -- continuing | ended | upcoming
    network            TEXT,
    airs_time          TEXT,             -- TVDB show air time, e.g. "21:00" (network local)
    runtime_minutes    INTEGER,
    content_rating     TEXT,
    tagline            TEXT,
    rating             REAL,             -- TMDB audience score (0-10)
    imdb_rating        REAL,             -- IMDb (0-10, via OMDb)
    rt_rating          INTEGER,          -- Rotten Tomatoes (0-100)
    metacritic         INTEGER,          -- Metacritic (0-100)
    ratings_synced     INTEGER NOT NULL DEFAULT 0,   -- OMDb ratings fetched?
    first_air_date     TEXT,
    last_air_date      TEXT,
    poster_url         TEXT,
    backdrop_url       TEXT,
    logo_url           TEXT,             -- transparent title logo (clearlogo)
    episodes_synced    INTEGER NOT NULL DEFAULT 0,   -- full episode list pulled from metadata?
    monitored          INTEGER NOT NULL DEFAULT 1,   -- "following" (watchlist)
    quality_profile_id INTEGER REFERENCES quality_profiles(id) ON DELETE SET NULL,
    root_folder_id     INTEGER REFERENCES root_folders(id)     ON DELETE SET NULL,
    path               TEXT,
    added_at           TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at         TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_shows_tvdb      ON shows(tvdb_id);
CREATE INDEX IF NOT EXISTS idx_shows_monitored ON shows(monitored);
CREATE UNIQUE INDEX IF NOT EXISTS ux_shows_server ON shows(server_source, server_id);

CREATE TABLE IF NOT EXISTS seasons (
    id            INTEGER PRIMARY KEY,
    show_id       INTEGER NOT NULL REFERENCES shows(id) ON DELETE CASCADE,
    server_id     TEXT,             -- media server native id (for reference/refresh)
    season_number INTEGER NOT NULL,
    title         TEXT,
    overview      TEXT,
    poster_url    TEXT,
    monitored     INTEGER NOT NULL DEFAULT 1,
    UNIQUE (show_id, season_number)
);
CREATE INDEX IF NOT EXISTS idx_seasons_show ON seasons(show_id);

CREATE TABLE IF NOT EXISTS episodes (
    id              INTEGER PRIMARY KEY,
    show_id         INTEGER NOT NULL REFERENCES shows(id)   ON DELETE CASCADE,
    season_id       INTEGER NOT NULL REFERENCES seasons(id) ON DELETE CASCADE,
    server_source   TEXT,             -- 'plex' | 'jellyfin'
    server_id       TEXT,             -- media server native id
    season_number   INTEGER NOT NULL,
    episode_number  INTEGER NOT NULL,
    title           TEXT,
    overview        TEXT,
    air_date        TEXT,             -- ISO date — drives the Calendar
    runtime_minutes INTEGER,
    still_url       TEXT,             -- per-episode thumbnail (server image path)
    rating          REAL,             -- audience score (0-10)
    tvdb_id         INTEGER,
    monitored       INTEGER NOT NULL DEFAULT 1,
    has_file        INTEGER NOT NULL DEFAULT 0,
    UNIQUE (show_id, season_number, episode_number)
);
CREATE INDEX IF NOT EXISTS idx_episodes_show     ON episodes(show_id);
CREATE INDEX IF NOT EXISTS idx_episodes_air      ON episodes(air_date);
CREATE INDEX IF NOT EXISTS idx_episodes_wanted   ON episodes(monitored, has_file);

-- ── Genres (normalised many-to-many; no comma-blob) ─────────────────────────
CREATE TABLE IF NOT EXISTS genres (
    id   INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE COLLATE NOCASE
);
CREATE TABLE IF NOT EXISTS movie_genres (
    movie_id INTEGER NOT NULL REFERENCES movies(id) ON DELETE CASCADE,
    genre_id INTEGER NOT NULL REFERENCES genres(id) ON DELETE CASCADE,
    PRIMARY KEY (movie_id, genre_id)
);
CREATE TABLE IF NOT EXISTS show_genres (
    show_id  INTEGER NOT NULL REFERENCES shows(id)  ON DELETE CASCADE,
    genre_id INTEGER NOT NULL REFERENCES genres(id) ON DELETE CASCADE,
    PRIMARY KEY (show_id, genre_id)
);
CREATE INDEX IF NOT EXISTS idx_movie_genres_genre ON movie_genres(genre_id);
CREATE INDEX IF NOT EXISTS idx_show_genres_genre  ON show_genres(genre_id);

-- ── People + credits (cast & crew; normalised, no blob) ─────────────────────
-- A person appears in many titles; deduped by their provider id. Each credit
-- belongs to exactly one movie OR show (separate nullable FKs + CHECK, no
-- polymorphic id), mirroring the media_files/downloads pattern.
CREATE TABLE IF NOT EXISTS people (
    id        INTEGER PRIMARY KEY,
    name      TEXT NOT NULL,
    tmdb_id   INTEGER UNIQUE,
    photo_url TEXT
);
CREATE INDEX IF NOT EXISTS idx_people_name ON people(name);

CREATE TABLE IF NOT EXISTS credits (
    id         INTEGER PRIMARY KEY,
    person_id  INTEGER NOT NULL REFERENCES people(id) ON DELETE CASCADE,
    movie_id   INTEGER REFERENCES movies(id) ON DELETE CASCADE,
    show_id    INTEGER REFERENCES shows(id)  ON DELETE CASCADE,
    department TEXT NOT NULL,        -- 'cast' | 'crew'
    job        TEXT,                 -- Director | Writer | Creator (crew); 'Actor' (cast)
    character  TEXT,                 -- the role played (cast)
    sort_order INTEGER NOT NULL DEFAULT 0,
    CHECK ((movie_id IS NOT NULL) + (show_id IS NOT NULL) = 1)
);
CREATE INDEX IF NOT EXISTS idx_credits_movie  ON credits(movie_id);
CREATE INDEX IF NOT EXISTS idx_credits_show   ON credits(show_id);
CREATE INDEX IF NOT EXISTS idx_credits_person ON credits(person_id);

-- ── Content: YouTube (channels → videos) ────────────────────────────────────
CREATE TABLE IF NOT EXISTS channels (
    id                 INTEGER PRIMARY KEY,
    youtube_id         TEXT NOT NULL UNIQUE,    -- channel id
    title              TEXT NOT NULL,
    handle             TEXT,                    -- @handle
    description        TEXT,
    avatar_url         TEXT,
    banner_url         TEXT,
    monitored          INTEGER NOT NULL DEFAULT 1,   -- "subscribed" (watchlist)
    quality_profile_id INTEGER REFERENCES quality_profiles(id) ON DELETE SET NULL,
    root_folder_id     INTEGER REFERENCES root_folders(id)     ON DELETE SET NULL,
    path               TEXT,
    added_at           TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at         TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_channels_monitored ON channels(monitored);

CREATE TABLE IF NOT EXISTS channel_videos (
    id               INTEGER PRIMARY KEY,
    channel_id       INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    youtube_id       TEXT NOT NULL UNIQUE,      -- video id
    title            TEXT NOT NULL,
    description      TEXT,
    published_at     TEXT,             -- ISO datetime — drives feed/Calendar
    duration_seconds INTEGER,
    thumbnail_url    TEXT,
    monitored        INTEGER NOT NULL DEFAULT 1,
    has_file         INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_channel_videos_channel   ON channel_videos(channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_videos_published ON channel_videos(published_at);
CREATE INDEX IF NOT EXISTS idx_channel_videos_wanted    ON channel_videos(monitored, has_file);

-- ── Owned media files (the Library = content that has a file) ────────────────
-- Exactly one owner FK is set (no polymorphic id). 1 row per physical file;
-- usually 1:1 with its content, but the table allows history/extras.
CREATE TABLE IF NOT EXISTS media_files (
    id             INTEGER PRIMARY KEY,
    movie_id       INTEGER REFERENCES movies(id)         ON DELETE CASCADE,
    episode_id     INTEGER REFERENCES episodes(id)       ON DELETE CASCADE,
    video_id       INTEGER REFERENCES channel_videos(id) ON DELETE CASCADE,
    relative_path  TEXT NOT NULL,
    size_bytes     INTEGER,
    resolution     TEXT,             -- 480p | 720p | 1080p | 2160p
    video_codec    TEXT,
    audio_codec    TEXT,
    release_source TEXT,             -- bluray | web-dl | webrip | hdtv | youtube
    quality        TEXT,             -- resolved quality name
    runtime_seconds INTEGER,
    added_at       TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CHECK ((movie_id IS NOT NULL) + (episode_id IS NOT NULL) + (video_id IS NOT NULL) = 1)
);
CREATE INDEX IF NOT EXISTS idx_media_files_movie   ON media_files(movie_id);
CREATE INDEX IF NOT EXISTS idx_media_files_episode ON media_files(episode_id);
CREATE INDEX IF NOT EXISTS idx_media_files_video   ON media_files(video_id);

-- ── Downloads (active queue + history) ──────────────────────────────────────
-- One target per row: a movie, a single episode, a whole season (pack), or a
-- youtube video. Exactly one FK set (CHECK), no polymorphic id.
CREATE TABLE IF NOT EXISTS downloads (
    id                 INTEGER PRIMARY KEY,
    movie_id           INTEGER REFERENCES movies(id)         ON DELETE SET NULL,
    episode_id         INTEGER REFERENCES episodes(id)       ON DELETE SET NULL,
    season_id          INTEGER REFERENCES seasons(id)        ON DELETE SET NULL,
    video_id           INTEGER REFERENCES channel_videos(id) ON DELETE SET NULL,
    title              TEXT NOT NULL,        -- display label
    release_title      TEXT,                 -- actual release / nzb / torrent name
    source             TEXT,                 -- torrent | usenet | youtube
    client             TEXT,                 -- qbittorrent | sabnzbd | yt-dlp ...
    client_download_id TEXT,                 -- hash / nzo_id to poll the client
    indexer            TEXT,
    status             TEXT NOT NULL DEFAULT 'queued'
                       CHECK (status IN ('queued','downloading','importing',
                                         'completed','failed','paused')),
    quality            TEXT,
    size_bytes         INTEGER,
    downloaded_bytes   INTEGER NOT NULL DEFAULT 0,
    progress           REAL    NOT NULL DEFAULT 0,   -- 0..100
    download_speed_bps INTEGER NOT NULL DEFAULT 0,
    eta_seconds        INTEGER,
    error_message      TEXT,
    added_at           TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    started_at         TEXT,
    completed_at       TEXT,
    CHECK ((movie_id IS NOT NULL) + (episode_id IS NOT NULL)
         + (season_id IS NOT NULL) + (video_id IS NOT NULL) = 1)
);
CREATE INDEX IF NOT EXISTS idx_downloads_status    ON downloads(status);
CREATE INDEX IF NOT EXISTS idx_downloads_completed ON downloads(completed_at);

-- ── Activity feed (dashboard "Recent Activity") ─────────────────────────────
CREATE TABLE IF NOT EXISTS activity (
    id         INTEGER PRIMARY KEY,
    event_type TEXT NOT NULL,        -- added | grabbed | imported | failed | renamed
    message    TEXT NOT NULL,
    movie_id   INTEGER REFERENCES movies(id)         ON DELETE SET NULL,
    episode_id INTEGER REFERENCES episodes(id)       ON DELETE SET NULL,
    video_id   INTEGER REFERENCES channel_videos(id) ON DELETE SET NULL,
    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_activity_created ON activity(created_at);

-- ── Derived views: Watchlist / Wishlist / Calendar ──────────────────────────
-- WATCHLIST = things you follow for NEW content: monitored shows + channels.
CREATE VIEW IF NOT EXISTS v_watchlist AS
    SELECT 'show'    AS kind, id, title, status, poster_url, monitored
      FROM shows    WHERE monitored = 1
    UNION ALL
    SELECT 'channel' AS kind, id, title, NULL AS status, avatar_url AS poster_url, monitored
      FROM channels WHERE monitored = 1;

-- WISHLIST = wanted-but-missing: monitored movies without a file + monitored
-- episodes that have aired but aren't owned.
CREATE VIEW IF NOT EXISTS v_wishlist AS
    SELECT 'movie'   AS kind, m.id AS ref_id, m.title AS title,
           NULL AS parent_title, m.release_date AS due_date
      FROM movies m
     WHERE m.monitored = 1 AND m.has_file = 0
    UNION ALL
    SELECT 'episode' AS kind, e.id AS ref_id,
           e.title AS title, s.title AS parent_title, e.air_date AS due_date
      FROM episodes e
      JOIN shows s ON s.id = e.show_id
     WHERE e.monitored = 1 AND e.has_file = 0
       AND e.air_date IS NOT NULL AND e.air_date <= date('now');

-- CALENDAR = dated items (episode air dates, movie releases, channel uploads).
CREATE VIEW IF NOT EXISTS v_calendar AS
    SELECT 'episode' AS kind, e.id AS ref_id, e.air_date AS date,
           e.title AS title, s.title AS parent_title
      FROM episodes e JOIN shows s ON s.id = e.show_id
     WHERE e.air_date IS NOT NULL
    UNION ALL
    SELECT 'movie' AS kind, m.id AS ref_id, m.release_date AS date,
           m.title AS title, NULL AS parent_title
      FROM movies m WHERE m.release_date IS NOT NULL
    UNION ALL
    SELECT 'video' AS kind, v.id AS ref_id, v.published_at AS date,
           v.title AS title, c.title AS parent_title
      FROM channel_videos v JOIN channels c ON c.id = v.channel_id
     WHERE v.published_at IS NOT NULL;
