#![allow(non_upper_case_globals)]

use crate::database::entities::{
    ContinueWatchingEpisode, ContinueWatchingMovie, Episode, EpisodeWithLibrary, FileRow,
    HostEntry, IdRow, ImageRow, Library, Movie, MovieWithCover, RecentSeasonWithCover, Season,
    SeasonWithCover, Show, ShowWithCover, UnmatchedSeason,
};
use std::{marker::PhantomData, ops::Deref};

pub struct Query<T> {
    pub(crate) sql: &'static str,
    _marker: PhantomData<T>,
}

impl<T> Query<T> {
    pub const fn new(sql: &'static str) -> Self {
        Self {
            sql,
            _marker: PhantomData,
        }
    }
}

pub struct Command {
    pub(crate) sql: &'static str,
}

impl Command {
    pub fn exec<C, P>(&self, conn: &C, params: P) -> Result<(usize, i64), rusqlite::Error>
    where
        C: Deref<Target = rusqlite::Connection>,
        P: rusqlite::Params,
    {
        let updated = conn.execute(self.sql, params)?;
        let id = conn.last_insert_rowid();
        Ok((updated, id))
    }
}

pub const create_movie: Command = Command {
    sql: r"
    INSERT INTO movies (library_id, tmdb_id, title, original_title, release_year, file)
    VALUES (?1, ?2, ?3, ?4, ?5, ?6);
    ",
};

pub const update_movie_tmdb: Command = Command {
    sql: r"
    UPDATE movies SET tmdb_id=?1, original_title=?2, description=?3, rating=?4, updated_at=CURRENT_TIMESTAMP WHERE id=?5;
    ",
};

pub const set_movie_skip_auto_match: Command = Command {
    sql: r"UPDATE movies SET skip_auto_match=1, updated_at=CURRENT_TIMESTAMP WHERE id=?1;",
};

pub const set_show_skip_auto_match: Command = Command {
    sql: r"UPDATE shows SET skip_auto_match=1, updated_at=CURRENT_TIMESTAMP WHERE id=?1;",
};

pub const set_season_skip_auto_match: Command = Command {
    sql: r"UPDATE seasons SET skip_auto_match=1, updated_at=CURRENT_TIMESTAMP WHERE id=?1;",
};

pub const reset_movie_auto_match: Command = Command {
    sql: r"UPDATE movies SET skip_auto_match=0, updated_at=CURRENT_TIMESTAMP WHERE tmdb_id IS NULL AND skip_auto_match=1;",
};

pub const reset_show_auto_match: Command = Command {
    sql: r"UPDATE shows SET skip_auto_match=0, updated_at=CURRENT_TIMESTAMP WHERE tmdb_id IS NULL AND skip_auto_match=1;",
};

pub const reset_season_auto_match: Command = Command {
    sql: r"UPDATE seasons SET skip_auto_match=0, updated_at=CURRENT_TIMESTAMP WHERE tmdb_id IS NULL AND skip_auto_match=1;",
};

pub const set_movie_watched: Command = Command {
    sql: r"UPDATE movies SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE id=?2;",
};

pub const set_show_watched: Command = Command {
    sql: r"UPDATE shows SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE id=?2;",
};

pub const set_season_watched: Command = Command {
    sql: r"UPDATE seasons SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE id=?2;",
};

pub const set_episode_watched: Command = Command {
    sql: r"UPDATE episodes SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE id=?2;",
};

pub const set_seasons_watched_by_show: Command = Command {
    sql: r"UPDATE seasons SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE show_id=?2;",
};

pub const set_episodes_watched_by_season: Command = Command {
    sql: r"UPDATE episodes SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE season_id=?2;",
};

pub const set_episodes_watched_by_show: Command = Command {
    sql: r"UPDATE episodes SET watched=?1, updated_at=CURRENT_TIMESTAMP WHERE season_id IN (SELECT id FROM seasons WHERE show_id=?2);",
};

pub const create_image: Command = Command {
    sql: r"
    INSERT INTO images (image, filename, mime_type, size) VALUES (?1, ?2, ?3, ?4);
    ",
};

pub const link_movie_image: Command = Command {
    sql: r"
    INSERT INTO movie_images (movie_id, image_id, type) VALUES (?1, ?2, ?3);
    ",
};

pub const find_movies_with_covers: Query<MovieWithCover> = Query::new(
    r"
    SELECT m.id, m.tmdb_id, m.title, m.original_title, m.release_year, m.file, l.root_path, i.filename, ib.filename, m.description, m.rating, m.watched, m.playback_position
    FROM movies m
    JOIN libraries l ON m.library_id = l.id
    LEFT JOIN movie_images mi ON m.id = mi.movie_id AND mi.type = 'cover'
    LEFT JOIN images i ON mi.image_id = i.id
    LEFT JOIN movie_images mib ON m.id = mib.movie_id AND mib.type = 'backdrop'
    LEFT JOIN images ib ON mib.image_id = ib.id
    ORDER BY COALESCE(m.original_title, m.title) ASC;
    ",
);

pub const delete_movie_images: Command = Command {
    sql: r"
    DELETE FROM movie_images WHERE movie_id = ?1 AND type = ?2;
    ",
};

pub const delete_orphaned_images: Command = Command {
    sql: r"
    DELETE FROM images WHERE id NOT IN (SELECT image_id FROM movie_images)
    AND id NOT IN (SELECT image_id FROM show_images)
    AND id NOT IN (SELECT image_id FROM season_images);
    ",
};

pub const find_all_images: Query<ImageRow> = Query::new(
    r"
    SELECT id, image, filename, mime_type, size FROM images;
    ",
);

pub const create_show: Command = Command {
    sql: r"
    INSERT INTO shows (tmdb_id, title, original_title, release_year, created_at, updated_at)
    VALUES (?1, ?2, ?3, ?4, ?5, ?6);
    ",
};

pub const find_show_by_name: Query<Show> = Query::new(
    r"SELECT id, tmdb_id, title, original_title, release_year, description, rating, watched, skip_auto_match, created_at, updated_at
    FROM shows WHERE title=?1",
);

pub const create_season: Command = Command {
    sql: r"INSERT INTO seasons (show_id, tmdb_id, season_number, title, release_year, created_at, updated_at)
    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);
    ",
};

pub const find_season_by_number: Query<Season> = Query::new(
    r"SELECT id, show_id, tmdb_id, season_number, title, release_year, description, rating, watched, skip_auto_match, created_at, updated_at
    FROM seasons WHERE show_id=?1 AND season_number=?2",
);

pub const create_episode: Command = Command {
    sql: r"INSERT INTO episodes (season_id, library_id, tmdb_id, episode_number, title, file, created_at, updated_at)
    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);
    ",
};

pub const find_unmatched_movies: Query<Movie> = Query::new(
    r"SELECT id, library_id, tmdb_id, title, original_title, release_year, file, description, rating, watched, skip_auto_match, playback_position, last_playback, created_at, updated_at
    FROM movies WHERE tmdb_id IS NULL AND skip_auto_match = 0",
);

pub const find_unmatched_shows: Query<Show> = Query::new(
    r"SELECT id, tmdb_id, title, original_title, release_year, description, rating, watched, skip_auto_match, created_at, updated_at
    FROM shows WHERE tmdb_id IS NULL AND skip_auto_match = 0",
);

pub const find_matched_movies: Query<Movie> = Query::new(
    r"SELECT id, library_id, tmdb_id, title, original_title, release_year, file, description, rating, watched, skip_auto_match, playback_position, last_playback, created_at, updated_at
    FROM movies WHERE tmdb_id IS NOT NULL",
);

pub const find_matched_shows: Query<Show> = Query::new(
    r"SELECT id, tmdb_id, title, original_title, release_year, description, rating, watched, skip_auto_match, created_at, updated_at
    FROM shows WHERE tmdb_id IS NOT NULL",
);

pub const find_seasons_by_show: Query<Season> = Query::new(
    r"SELECT id, show_id, tmdb_id, season_number, title, release_year, description, rating, watched, skip_auto_match, created_at, updated_at
    FROM seasons WHERE show_id=?1",
);

pub const find_episodes_by_season: Query<Episode> = Query::new(
    r"SELECT id, season_id, library_id, tmdb_id, episode_number, title, file, watched, playback_position, last_playback, created_at, updated_at
    FROM episodes WHERE season_id=?1 ORDER BY episode_number",
);

pub const find_episodes_with_library_by_season: Query<EpisodeWithLibrary> = Query::new(
    r"SELECT e.id, e.episode_number, e.title, e.file, l.root_path, e.watched, e.playback_position
    FROM episodes e
    JOIN libraries l ON e.library_id = l.id
    WHERE e.season_id=?1 ORDER BY e.episode_number",
);

pub const update_show_tmdb: Command = Command {
    sql: r"UPDATE shows SET tmdb_id=?1, original_title=?2, release_year=?3, description=?4, rating=?5, updated_at=CURRENT_TIMESTAMP WHERE id=?6;",
};

pub const update_season_tmdb: Command = Command {
    sql: r"UPDATE seasons SET tmdb_id=?1, title=?2, release_year=?3, description=?4, rating=?5, updated_at=CURRENT_TIMESTAMP WHERE id=?6;",
};

pub const update_episode_tmdb: Command = Command {
    sql: r"UPDATE episodes SET tmdb_id=?1, title=?2, updated_at=CURRENT_TIMESTAMP WHERE id=?3;",
};

pub const link_show_image: Command = Command {
    sql: r"INSERT INTO show_images (show_id, image_id, type) VALUES (?1, ?2, ?3);",
};

pub const link_season_image: Command = Command {
    sql: r"INSERT INTO season_images (season_id, image_id, type) VALUES (?1, ?2, ?3);",
};

pub const find_shows_with_covers: Query<ShowWithCover> = Query::new(
    r"
    SELECT s.id, s.tmdb_id, s.title, s.original_title, s.release_year, i.filename, ib.filename, s.description, s.rating, s.watched
    FROM shows s
    LEFT JOIN show_images si ON s.id = si.show_id AND si.type = 'cover'
    LEFT JOIN images i ON si.image_id = i.id
    LEFT JOIN show_images sib ON s.id = sib.show_id AND sib.type = 'backdrop'
    LEFT JOIN images ib ON sib.image_id = ib.id
    ORDER BY COALESCE(s.original_title, s.title) ASC;
    ",
);

pub const find_seasons_with_covers: Query<SeasonWithCover> = Query::new(
    r"
    SELECT s.id, s.show_id, s.tmdb_id, s.season_number, s.title, s.release_year, i.filename, s.description, s.rating, s.watched
    FROM seasons s
    LEFT JOIN season_images si ON s.id = si.season_id AND si.type = 'cover'
    LEFT JOIN images i ON si.image_id = i.id
    WHERE s.show_id = ?1
    ORDER BY s.season_number;
    ",
);

pub const delete_show_images: Command = Command {
    sql: r"
    DELETE FROM show_images WHERE show_id = ?1 AND type = ?2;
    ",
};

pub const delete_season_images: Command = Command {
    sql: r"
    DELETE FROM season_images WHERE season_id = ?1 AND type = ?2;
    ",
};

pub const upsert_host: Command = Command {
    sql: r"
    INSERT OR REPLACE INTO host (key, value) VALUES (?1, ?2);
    ",
};

pub const find_host: Query<HostEntry> = Query::new(
    r"
    SELECT key, CAST(value AS TEXT) AS value FROM host WHERE key = ?1;
    ",
);

pub const create_library: Command = Command {
    sql: r"
    INSERT INTO libraries (name, root_path, media_type) VALUES (?1, ?2, ?3);
    ",
};

pub const find_library_by_root_path: Query<Library> = Query::new(
    r"
    SELECT id, name, root_path, media_type, created_at FROM libraries WHERE root_path = ?1;
    ",
);

pub const find_libraries_by_media_type: Query<Library> = Query::new(
    r"
    SELECT id, name, root_path, media_type, created_at FROM libraries WHERE media_type = ?1;
    ",
);

pub const find_movie_files_by_library: Query<FileRow> =
    Query::new(r"SELECT file FROM movies WHERE library_id = ?1;");

pub const find_episode_files_by_library: Query<FileRow> =
    Query::new(r"SELECT file FROM episodes WHERE library_id = ?1;");

pub const delete_movie_by_file: Command = Command {
    sql: r"
    DELETE FROM movies WHERE library_id = ?1 AND file = ?2;
    ",
};

pub const delete_episode_by_file: Command = Command {
    sql: r"
    DELETE FROM episodes WHERE library_id = ?1 AND file = ?2;
    ",
};

pub const delete_empty_seasons: Command = Command {
    sql: r"
    DELETE FROM seasons WHERE id NOT IN (SELECT season_id FROM episodes);
    ",
};

pub const delete_empty_shows: Command = Command {
    sql: r"
    DELETE FROM shows WHERE id NOT IN (SELECT show_id FROM seasons);
    ",
};

pub const find_unmatched_seasons_with_matched_shows: Query<UnmatchedSeason> = Query::new(
    r"
    SELECT s.id, s.show_id, s.season_number, sh.tmdb_id, COALESCE(sh.original_title, sh.title)
    FROM seasons s
    JOIN shows sh ON s.show_id = sh.id
    WHERE s.tmdb_id IS NULL AND sh.tmdb_id IS NOT NULL AND s.skip_auto_match = 0;
    ",
);

pub const find_recent_movies_with_covers: Query<MovieWithCover> = Query::new(
    r"
    SELECT m.id, m.tmdb_id, m.title, m.original_title, m.release_year, m.file, l.root_path, i.filename, ib.filename, m.description, m.rating, m.watched, m.playback_position
    FROM movies m
    JOIN libraries l ON m.library_id = l.id
    LEFT JOIN movie_images mi ON m.id = mi.movie_id AND mi.type = 'cover'
    LEFT JOIN images i ON mi.image_id = i.id
    LEFT JOIN movie_images mib ON m.id = mib.movie_id AND mib.type = 'backdrop'
    LEFT JOIN images ib ON mib.image_id = ib.id
    ORDER BY m.created_at DESC
    LIMIT 20;
    ",
);

pub const find_recent_seasons_with_covers: Query<RecentSeasonWithCover> = Query::new(
    r"
    SELECT s.id, s.show_id, s.season_number, s.title, COALESCE(sh.original_title, sh.title), i.filename, s.watched
    FROM seasons s
    JOIN shows sh ON s.show_id = sh.id
    LEFT JOIN season_images si ON s.id = si.season_id AND si.type = 'cover'
    LEFT JOIN images i ON si.image_id = i.id
    ORDER BY s.created_at DESC
    LIMIT 20;
    ",
);

pub const upsert_media_meta: Command = Command {
    sql: r"
    INSERT INTO media_meta (file, duration) VALUES (?1, ?2)
    ON CONFLICT(file) DO UPDATE SET duration=excluded.duration;
    ",
};

pub const update_movie_playback: Command = Command {
    sql: r"UPDATE movies SET playback_position=?1, last_playback=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?2;",
};

pub const update_episode_playback: Command = Command {
    sql: r"UPDATE episodes SET playback_position=?1, last_playback=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?2;",
};

pub const clear_movie_playback: Command = Command {
    sql: r"UPDATE movies SET playback_position=NULL, last_playback=NULL, updated_at=CURRENT_TIMESTAMP WHERE id=?1;",
};

pub const clear_episode_playback: Command = Command {
    sql: r"UPDATE episodes SET playback_position=NULL, last_playback=NULL, updated_at=CURRENT_TIMESTAMP WHERE id=?1;",
};

pub const clear_episodes_playback_by_season: Command = Command {
    sql: r"UPDATE episodes SET playback_position=NULL, last_playback=NULL, updated_at=CURRENT_TIMESTAMP WHERE season_id=?1;",
};

pub const clear_episodes_playback_by_show: Command = Command {
    sql: r"UPDATE episodes SET playback_position=NULL, last_playback=NULL, updated_at=CURRENT_TIMESTAMP WHERE season_id IN (SELECT id FROM seasons WHERE show_id=?1);",
};

pub const find_next_queueable_episode: Query<IdRow> = Query::new(
    r"
    SELECT e.id FROM episodes e
    JOIN episodes cur ON cur.id = ?1
    JOIN seasons se ON e.season_id = se.id
    JOIN seasons cur_se ON cur.season_id = cur_se.id
    WHERE se.show_id = cur_se.show_id
      AND (
        (e.season_id = cur.season_id AND e.episode_number > cur.episode_number)
        OR se.season_number > cur_se.season_number
      )
      AND e.watched = 0
      AND e.playback_position IS NULL
    ORDER BY se.season_number ASC, e.episode_number ASC
    LIMIT 1;
    ",
);

pub const find_continue_watching_movies: Query<ContinueWatchingMovie> = Query::new(
    r"
    SELECT m.id, m.title, m.original_title, m.file, l.root_path, ib.filename,
           m.playback_position, mm.duration, m.last_playback
    FROM movies m
    JOIN libraries l ON m.library_id = l.id
    LEFT JOIN media_meta mm ON mm.file = l.root_path || '/' || m.file
    LEFT JOIN movie_images mib ON m.id = mib.movie_id AND mib.type = 'backdrop'
    LEFT JOIN images ib ON mib.image_id = ib.id
    WHERE m.playback_position IS NOT NULL
    ORDER BY m.last_playback DESC
    LIMIT 20;
    ",
);

pub const find_continue_watching_episodes: Query<ContinueWatchingEpisode> = Query::new(
    r"
    SELECT e.id, e.episode_number, e.title, e.file, l.root_path,
           sh.id, COALESCE(sh.original_title, sh.title), se.id, se.season_number, ib.filename,
           e.playback_position, mm.duration, e.last_playback
    FROM episodes e
    JOIN libraries l ON e.library_id = l.id
    JOIN seasons se ON e.season_id = se.id
    JOIN shows sh ON se.show_id = sh.id
    LEFT JOIN media_meta mm ON mm.file = l.root_path || '/' || e.file
    LEFT JOIN show_images sib ON sh.id = sib.show_id AND sib.type = 'backdrop'
    LEFT JOIN images ib ON sib.image_id = ib.id
    WHERE e.playback_position IS NOT NULL
      AND e.id = (
          SELECT e2.id FROM episodes e2
          JOIN seasons se2 ON e2.season_id = se2.id
          WHERE se2.show_id = sh.id AND e2.playback_position IS NOT NULL
          ORDER BY e2.last_playback DESC
          LIMIT 1
      )
    ORDER BY e.last_playback DESC
    LIMIT 20;
    ",
);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::database::Database;
    use rusqlite::params;

    pub(crate) fn seed(db: &mut Database) {
        db.exec_cmd(
            &create_library,
            params!["Test Movies", "/test/movies", "movie"],
        )
        .unwrap();
        db.exec_cmd(
            &create_movie,
            params![
                1i64,
                Option::<i64>::None,
                "Event Horizon",
                "Event Horizon",
                1991,
                "testfile.mkv",
            ],
        )
        .unwrap();
    }

    #[test]
    fn test_find_movie() {
        let mut db = Database::open(":memory:").unwrap();
        seed(&mut db);

        let movies = db.query_all(&find_unmatched_movies, []).unwrap();
        assert_eq!(movies.len(), 1);
    }

    #[test]
    fn test_delete_movies_by_files() {
        use crate::database::entities::NewMovie;

        let mut db = Database::open(":memory:").unwrap();
        let lib = db.find_or_create_library("/test/movies", "movie").unwrap();
        let m1 = db
            .create_movie(NewMovie {
                library_id: lib,
                title: "A".into(),
                release_year: None,
                file: "A/a.mkv".into(),
            })
            .unwrap();
        db.create_movie(NewMovie {
            library_id: lib,
            title: "B".into(),
            release_year: None,
            file: "B/b.mkv".into(),
        })
        .unwrap();
        let img = db
            .create_image(&[1, 2, 3], "poster.jpg", "image/jpeg")
            .unwrap();
        db.link_movie_image(m1, img, "cover").unwrap();

        assert_eq!(db.delete_movies_by_files(lib, &[]).unwrap(), 0);

        // deleting the movie must cascade its movie_images link
        assert_eq!(
            db.delete_movies_by_files(lib, &["A/a.mkv".to_string()])
                .unwrap(),
            1
        );
        assert_eq!(db.find_unmatched_movies().unwrap().len(), 1);

        db.delete_orphaned_images().unwrap();
        assert!(db.find_all_images().unwrap().is_empty());
    }

    #[test]
    fn test_delete_episodes_cleans_up_empty_seasons_and_shows() {
        use crate::database::entities::{NewEpisode, NewSeason, NewShow};

        let mut db = Database::open(":memory:").unwrap();
        let lib = db.find_or_create_library("/test/shows", "show").unwrap();
        let show = db
            .create_show(NewShow {
                title: "Show".into(),
            })
            .unwrap();
        let season = db
            .create_season(NewSeason {
                show_id: show,
                season_number: 1,
            })
            .unwrap();
        for (number, file) in [(1, "Show/s01e01.mkv"), (2, "Show/s01e02.mkv")] {
            db.create_episode(NewEpisode {
                season_id: season,
                library_id: lib,
                episode_number: number,
                file: file.into(),
            })
            .unwrap();
        }

        assert_eq!(
            db.delete_episodes_by_files(lib, &["Show/s01e01.mkv".to_string()])
                .unwrap(),
            1
        );
        assert_eq!(db.delete_empty_seasons().unwrap(), 0);
        assert_eq!(db.delete_empty_shows().unwrap(), 0);
        assert!(db.find_show_by_name("Show").unwrap().is_some());

        assert_eq!(
            db.delete_episodes_by_files(lib, &["Show/s01e02.mkv".to_string()])
                .unwrap(),
            1
        );
        assert_eq!(db.delete_empty_seasons().unwrap(), 1);
        assert_eq!(db.delete_empty_shows().unwrap(), 1);
        assert!(db.find_show_by_name("Show").unwrap().is_none());
    }

    #[test]
    fn test_show_spanning_libraries_survives_partial_delete() {
        use crate::database::entities::{NewEpisode, NewSeason, NewShow};

        let mut db = Database::open(":memory:").unwrap();
        let lib_a = db.find_or_create_library("/test/shows-a", "show").unwrap();
        let lib_b = db.find_or_create_library("/test/shows-b", "show").unwrap();
        let show = db
            .create_show(NewShow {
                title: "Show".into(),
            })
            .unwrap();
        let season = db
            .create_season(NewSeason {
                show_id: show,
                season_number: 1,
            })
            .unwrap();
        for (lib, number, file) in [(lib_a, 1, "Show/s01e01.mkv"), (lib_b, 2, "Show/s01e02.mkv")] {
            db.create_episode(NewEpisode {
                season_id: season,
                library_id: lib,
                episode_number: number,
                file: file.into(),
            })
            .unwrap();
        }

        assert_eq!(
            db.delete_episodes_by_files(lib_a, &["Show/s01e01.mkv".to_string()])
                .unwrap(),
            1
        );
        assert_eq!(db.delete_empty_seasons().unwrap(), 0);
        assert_eq!(db.delete_empty_shows().unwrap(), 0);
        assert!(db.find_show_by_name("Show").unwrap().is_some());
    }
}
