use rayon::prelude::IntoParallelIterator;
use rayon::prelude::ParallelIterator;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;

use walkdir::WalkDir;

use func::is_video_file;
pub use video::Video;
mod func;
mod video;

#[macro_export]
macro_rules! query_row_map {
    ($db:ident, $query:expr, $param:expr, $map_fn:expr) => {{
        let mut state = $db.prepare($query).unwrap();

        let r: Vec<_> = state
            .query_map($param, $map_fn)
            .unwrap()
            .flatten()
            .collect();
        r
    }};
}

#[derive(Debug, Clone)]
pub struct Library {
    conn: Arc<Mutex<rusqlite::Connection>>,
}

impl Library {
    pub fn new() -> Self {
        log::info!("Creating database connection");
        let conn = Arc::new(Mutex::new(rusqlite::Connection::open("videos.db").unwrap()));

        let s = Self { conn };

        s.init_schema();

        s
    }
}

// DB
impl Library {
    pub fn init_schema(&self) {
        let mut con = self.conn.lock().unwrap();
        let tx = con.transaction().unwrap();
        tx.execute_batch(include_str!("../schema.sql")).unwrap();
        tx.commit().unwrap();
    }
}

// Functions
impl Library {
    // directories

    pub fn get_directories(&self) -> Vec<String> {
        let db = self.conn.lock().unwrap();
        query_row_map!(db, "SELECT DISTINCT directory FROM videos;", [], |x| {
            x.get::<usize, String>(0)
        })
    }

    pub fn get_directory_videos(&self, dir: &str) -> Vec<Video> {
        let db = self.conn.lock().unwrap();

        let videos: Vec<_> = query_row_map!(
            db,
            "SELECT sha256 FROM videos INNER JOIN youtube_meta ON youtube_meta.id = videos.youtube_id WHERE directory = ?1 ORDER BY youtube_meta.upload_date DESC;",
            &[dir],
            |x| {
                Ok(Video::from_hash(
                    &x.get::<usize, String>(0)?,
                    self.conn.clone(),
                ))
            }
        );

        videos
    }

    // YT

    pub fn get_channel_name_yt(&self, id: &str) -> String {
        let db = self.conn.lock().unwrap();

        let res: Vec<String> = query_row_map!(
            db,
            "SELECT uploader_name FROM youtube_meta WHERE uploader_id = ?1",
            &[id],
            |x| { x.get(0) }
        );

        res.first().unwrap().to_owned()
    }

    pub fn get_tags_yt(&self) -> Vec<String> {
        let db = self.conn.lock().unwrap();

        let tags: Vec<_> =
            query_row_map!(db, "SELECT DISTINCT tag FROM youtube_meta_tags", [], |x| {
                x.get(0)
            });

        tags
    }

    pub fn get_videos_by_tag_yt(&self, tag: &str) -> Vec<Video> {
        let db = self.conn.lock().unwrap();

        let videos: Vec<_> = query_row_map!(
            db,
            "SELECT sha256 FROM youtube_meta_tags INNER JOIN youtube_meta ON youtube_meta_tags.youtube_id = youtube_meta.id  INNER JOIN videos ON videos.youtube_id = youtube_meta.id WHERE tag = ?1;",
            &[tag],
            |x| {
                Ok(Video::from_hash(
                    &x.get::<usize, String>(0)?,
                    self.conn.clone(),
                ))
            }
        );

        videos
    }

    pub fn get_channel_videos_yt(&self, id: &str) -> Vec<Video> {
        let db = self.conn.lock().unwrap();

        let videos: Vec<_> = query_row_map!(
            db,
            "SELECT sha256 FROM youtube_meta INNER JOIN videos ON youtube_meta.id = videos.youtube_id WHERE uploader_id = ?1 ORDER BY youtube_meta.upload_date DESC;",
            &[id],
            |x| {
                Ok(Video::from_hash(
                    &x.get::<usize, String>(0)?,
                    self.conn.clone(),
                ))
            }
        );

        videos
    }

    // videos

    pub fn get_random_videos(&self, n: usize) -> Vec<Video> {
        let db = self.conn.lock().unwrap();

        query_row_map!(
            db,
            "SELECT sha256 FROM videos ORDER BY RANDOM() LIMIT ?1;",
            [n],
            |x| {
                Ok(Video::from_hash(
                    &x.get::<usize, String>(0)?,
                    self.conn.clone(),
                ))
            }
        )
    }

    pub fn get_video_by_hash(&self, hash: &str) -> Option<(String, Video)> {
        let db = self.conn.lock().unwrap();

        let res: Vec<(String, Video)> = query_row_map!(
            db,
            "SELECT sha256, directory FROM videos WHERE sha256 = ?1;",
            &[hash],
            |x| {
                Ok((
                    x.get(1)?,
                    Video::from_hash(&x.get::<usize, String>(0)?, self.conn.clone()),
                ))
            }
        );
        if !res.is_empty() {
            return res.first().map(std::borrow::ToOwned::to_owned);
        }
        None
    }

    pub fn get_video_by_youtube_id(&self, id: &str) -> Option<(String, Video)> {
        let db = self.conn.lock().unwrap();

        let res = query_row_map!(
            db,
            "SELECT sha256, directory FROM videos WHERE youtube_id = ?1",
            &[id],
            |x| {
                Ok((
                    x.get(1)?,
                    Video::from_hash(&x.get::<usize, String>(0)?, self.conn.clone()),
                ))
            }
        );

        if !res.is_empty() {
            return res.first().map(std::borrow::ToOwned::to_owned);
        }
        None
    }

    pub fn has_path(&self, path: &Path) -> bool {
        let db = self.conn.lock().unwrap();
        let mut state = db
            .prepare("SELECT path FROM videos WHERE path = ?1;")
            .unwrap();

        let x = state
            .query_map([path.to_str().unwrap()], |x| {
                let r: String = x.get(0)?;
                Ok(r)
            })
            .unwrap()
            .flatten()
            .next()
            .is_some();
        x
    }

    // search

    pub fn search_video(&self, query: &str, start: usize, n: usize) -> Vec<Video> {
        let db = self.conn.lock().unwrap();
        let query = format!("%{query}%");

        query_row_map!(
            db,
            &format!(
                r#"SELECT DISTINCT
            vm.sha256,
            ( -- Calculate a score for the video based on matches
                (ym.title LIKE ?1) +
                (ym.description LIKE ?1) +
                (ym.uploader_name LIKE ?1) +
                (vm.directory LIKE ?1)
            ) AS score
        FROM
            youtube_meta AS ym
        LEFT JOIN
            videos AS vm ON ym.id = vm.youtube_id
        LEFT JOIN
            youtube_meta_tags AS ymt ON ym.id = ymt.youtube_id
        WHERE
            (ym.title LIKE ?1) OR
            (ym.description LIKE ?1) OR
            (ym.uploader_name LIKE ?1) OR
            (vm.directory LIKE ?1) OR
            (ymt.tag LIKE ?1)
        ORDER BY
            score DESC,
            ym.upload_date DESC LIMIT {n} OFFSET {start};"#
            ),
            &[&query],
            |x| {
                Ok(Video::from_hash(
                    &x.get::<usize, String>(0)?,
                    self.conn.clone(),
                ))
            }
        )
    }
}

// video library scan
impl Library {
    pub fn scan_dir(&self, dir: &PathBuf) {
        log::info!("Scanning {dir:?}");
        let lib = self.get_video_paths(dir);
        let _: Vec<Video> = lib
            .into_par_iter()
            .map(|x| Video::insert_path_to_db(&self.conn.clone(), &x))
            .collect();
        let db = self.conn.lock().unwrap();
        db.flush_prepared_statement_cache();
        log::info!("Finished scanning {dir:?}");
    }

    fn get_video_paths(&self, dir: &PathBuf) -> Vec<PathBuf> {
        let mut videos: Vec<PathBuf> = vec![];

        for entry in WalkDir::new(dir).follow_links(true) {
            match entry {
                Ok(entry) => {
                    if entry.file_type().is_file() {
                        log::info!("Scanning {entry:?}");
                        let file_name = entry.file_name().to_string_lossy();
                        if is_video_file(&file_name) {
                            let video_path = entry.path().to_path_buf();

                            if self.has_path(&video_path) {
                                continue;
                            }

                            videos.push(video_path);
                        }
                    }
                }
                Err(err) => eprintln!("Error reading entry: {err}"),
            }
        }
        videos
    }
}
