#[macro_use]
mod macros;
pub mod entities;
mod episodes;
mod error;
mod host;
mod libraries;
mod media_meta;
mod merge;
mod migration;
mod movies;
pub mod queries;
mod seasons;
mod shows;
mod tables;

pub use error::DatabaseError;
use queries::{Command, Query};
use rusqlite::{Connection, Params, Row, fallible_iterator::FallibleIterator};

#[cfg(debug_assertions)]
use rusqlite::trace::{TraceEvent, TraceEventCodes};

pub trait FromRow: Sized {
    fn from_row(row: &Row) -> rusqlite::Result<Self>;
}

pub struct Database {
    conn: Connection,
    db_path: String,
}

impl Database {
    const TABLES: &[tables::Table] = &[
        tables::HOST,
        tables::LIBRARIES,
        tables::MOVIES,
        tables::IMAGES,
        tables::MOVIE_IMAGES,
        tables::SHOWS,
        tables::SEASONS,
        tables::EPISODES,
        tables::SHOW_IMAGES,
        tables::SEASON_IMAGES,
        tables::MEDIA_META,
    ];

    pub fn open(db: &str) -> Result<Self, DatabaseError> {
        let mut conn = if db == ":memory:" {
            Connection::open(db)?
        } else {
            migration::migrate_if_needed(db, Self::TABLES)?
        };
        log::info!("database opened: {db}");

        conn.execute("PRAGMA foreign_keys = ON", [])?;
        conn.pragma_update(None, "journal_mode", "WAL")?;

        #[cfg(debug_assertions)]
        conn.trace_v2(
            TraceEventCodes::SQLITE_TRACE_STMT,
            Some(|event| {
                if let TraceEvent::Stmt(_, stmt) = event {
                    log::debug!("{}", stmt);
                }
            }),
        );

        tables::init(&mut conn, Self::TABLES)?;

        Ok(Self {
            conn,
            db_path: db.to_string(),
        })
    }

    pub fn get_db_path(&self) -> String {
        self.db_path.clone()
    }

    pub fn exec_cmd<P>(&mut self, cmd: &Command, params: P) -> Result<(usize, i64), DatabaseError>
    where
        P: Params,
    {
        let tx = self.conn.transaction()?;
        let updated = tx.execute(cmd.sql, params)?;
        tx.commit()?;
        let id = self.conn.last_insert_rowid();
        Ok((updated, id))
    }

    pub fn transaction<F, T>(&mut self, f: F) -> Result<T, DatabaseError>
    where
        F: FnOnce(&rusqlite::Transaction) -> Result<T, DatabaseError>,
    {
        let tx = self.conn.transaction()?;
        let result = f(&tx)?;
        tx.commit()?;
        Ok(result)
    }

    pub fn query_all<T, P>(&mut self, query: &Query<T>, params: P) -> Result<Vec<T>, DatabaseError>
    where
        T: FromRow,
        P: Params,
    {
        let mut stmt = self.conn.prepare(query.sql)?;
        let rows = stmt.query(params)?;
        let res = rows.map(|r| T::from_row(r)).collect()?;
        Ok(res)
    }

    pub fn query_opt<T, P>(
        &mut self,
        query: &Query<T>,
        params: P,
    ) -> Result<Option<T>, DatabaseError>
    where
        T: FromRow,
        P: Params,
    {
        let mut stmt = self.conn.prepare(query.sql)?;
        match stmt.query_one(params, |r| T::from_row(r)) {
            Ok(row) => Ok(Some(row)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e.into()),
        }
    }
}
