// License: see LICENSE file at root directory of `master` branch

//! # Database manager

use {
    std::{
        collections::{HashMap, HashSet},
        convert::TryFrom,
        io::{self, Error, ErrorKind},
        sync::mpsc::{self, Receiver, Sender},
        time::{UNIX_EPOCH, Duration, SystemTime},
    },

    bailamos::{
        db::{
            Id,
            notifications::Notification,
        },
    },
    binn_ir::Value,
    dia_range::Range,
    rusqlite::Connection,
    tiny_keccak::Keccak,
    zeros::{ReadOnlyZeros, Zeros},

    crate::db::{
        ID,
        notifications,
        users::{Passphrase, User},
    },
};

/// # Range of dates
pub type RangeOfDates = Range<u64, u64>;

type Credentials = HashMap<ReadOnlyZeros, Id>;

const CREDENTIALS_SYNC: Duration = Duration::from_secs(60);

/// # Message sender
pub type MsgSender = Sender<Msg>;

/// # Message
#[derive(Debug)]
pub enum Msg {

    /// # Verifies user
    VerifyUser {

        /// # User
        user: User,

        /// # Result sender
        result_sender: Sender<Option<Id>>,

    },

    /// # Inserts notification
    InsertNotification {

        /// # Notification
        ///
        /// Date created and user ID will be ignored.
        notification: Notification,

        /// # User ID
        ///
        /// This is used for [`notification`][::notification].
        ///
        /// [::notification]: #variant.InsertNotification.field.notification
        user_id: Id,

        /// # Result sender
        result_sender: Sender<io::Result<()>>,

    },

    /// # Load unread notifications
    LoadUnreadNotifications {

        /// # User ID
        user_id: Id,

        /// # Result sender
        result_sender: Sender<io::Result<Value>>,

    },

    /// # Mark notifications as read
    MarkNotificationsAsRead {

        /// # User ID
        user_id: Id,

        /// # Notification IDs
        notification_ids: HashSet<Id>,

        /// # Result sender
        result_sender: Sender<io::Result<Value>>,

    },

    /// # Delete notifications via range of dates
    DeleteNotificationsViaRangeOfDates {

        /// # User ID
        user_id: Id,

        /// # Range of dates
        range_of_dates: RangeOfDates,

        /// # Result sender
        result_sender: Sender<io::Result<Value>>,

    },

}

/// # Database manager
#[derive(Debug)]
pub struct DbManager {
    conn: Connection,
    receiver: Receiver<Msg>,

    credentials: Credentials,
    credentials_last_synced: Option<SystemTime>,
}

impl DbManager {

    /// # Makes new instance
    pub fn new(conn: Connection) -> (Self, MsgSender) {
        let (sender, receiver) = mpsc::channel();
        (Self {
            conn,
            receiver,

            credentials: Credentials::new(),
            credentials_last_synced: None,
        }, sender)
    }

    /// # Runs the manager
    pub fn run(&mut self) -> io::Result<()> {
        loop {
            match self.receiver.recv() {
                Ok(Msg::VerifyUser { user, result_sender }) => if let Err(err) = result_sender.send(self.verify_user(user)) {
                    __e!("Failed sending user verification result: {:?}", err);
                },
                Ok(Msg::InsertNotification { notification, user_id, result_sender }) => if let Err(err) = result_sender.send(
                    self.insert_notification(notification, user_id),
                ) {
                    __e!("Failed sending result of inserting new notification: {:?}", err);
                },
                Ok(Msg::LoadUnreadNotifications { user_id, result_sender }) => if let Err(err) = result_sender.send(
                    self.load_unread_notifications(user_id),
                ) {
                    __e!("Failed sending result of loading unread notifications: {:?}", err);
                },
                Ok(Msg::MarkNotificationsAsRead { user_id, notification_ids, result_sender }) => if let Err(err) = result_sender.send(
                    self.mark_notifications_as_read(user_id, notification_ids),
                ) {
                    __e!("Failed sending result of marking notifications as reads: {:?}", err);
                },
                Ok(Msg::DeleteNotificationsViaRangeOfDates { user_id, range_of_dates, result_sender }) => if let Err(err) = result_sender.send(
                    self.delete_notifications_via_range_of_dates(user_id, range_of_dates),
                ) {
                    __e!("Failed sending result of deleting notifications via range of dates: {:?}", err);
                },
                Err(err) => return Err(Error::new(ErrorKind::Other, format!("Database manager stopped: {:?}", err))),
            };
        }
    }

    /// # Verifies user
    fn verify_user(&mut self, user: User) -> Option<Id> {
        let credentials = encode_credentials(user.name(), user.passphrase());

        if let Some(id) = self.credentials.get(&credentials) {
            return Some(*id);
        }

        if let Some(credentials_last_synced) = self.credentials_last_synced {
            if CREDENTIALS_SYNC < match credentials_last_synced.duration_since(SystemTime::now()) {
                Ok(d) => d,
                Err(err) => err.duration(),
            } {
                return None;
            }
        }

        match load_credentials(&self.conn) {
            Ok(new_credentials) => {
                self.credentials = new_credentials;
                self.credentials_last_synced = Some(SystemTime::now());
                self.credentials.get(&credentials).map(|id| *id)
            },
            Err(err) => {
                __e!("Failed loading credentials: {:?}", err);
                None
            },
        }
    }

    /// # Inserts notification
    fn insert_notification(&self, notification: Notification, user_id: Id) -> io::Result<()> {
        let date_created = SystemTime::now().duration_since(UNIX_EPOCH)
            .map_err(|e| Error::new(ErrorKind::Other, format!("Failed getting system time: {:?}", e)))?
            .as_secs() as i64;
        let result = move || -> rusqlite::Result<_> {
            match self.conn.execute(
                &format!(
                    "insert into {} ({},{},{},{},{},{}) values (?,?,?,?,?,?);",
                    notifications::TABLE, notifications::USER_ID, notifications::DATE_CREATED, notifications::TITLE, notifications::CONTENT,
                    notifications::DATA, notifications::READ,
                ),
                rusqlite::params![
                    user_id, date_created, notification.title(), notification.content(), notification.data(), notification.is_read(),
                ],
            )? {
                1 => Ok(()),
                other => Err(rusqlite::Error::StatementChangedRows(other)),
            }
        };
        result().map_err(|e| Error::new(ErrorKind::Other, e))
    }

    /// # Loads unread notifications
    fn load_unread_notifications(&self, user_id: Id) -> io::Result<Value> {
        let result = move || -> rusqlite::Result<_> {
            // TODO: add flag for clients to know about limits
            let mut statement = self.conn.prepare(&format!(
                "select {},{},{},{},{} from {} where {}=? and {}=? order by {} desc limit 11;",
                ID, notifications::DATE_CREATED, notifications::TITLE, notifications::CONTENT, notifications::DATA,
                notifications::TABLE, notifications::USER_ID, notifications::READ, notifications::DATE_CREATED,
            ))?;
            let mut result = vec![];
            for r in statement.query_map(
                rusqlite::params![user_id, false], |r| Ok((r.get(0)?, r.get::<_, i64>(1)?, r.get(2)?, r.get(3)?, r.get(4)?)),
            )? {
                let (id, date_created, title, content, data) = r?;
                result.push(Value::from(Notification::new(Some(id), Some(date_created as u64), Some(user_id), title, content, data, true)));
            }
            Ok(Value::from(result))
        };
        result().map_err(|e| Error::new(ErrorKind::Other, e))
    }

    /// # Marks notifications as read
    fn mark_notifications_as_read(&mut self, user_id: Id, notification_ids: HashSet<Id>) -> io::Result<Value> {
        if notification_ids.is_empty() {
            return Ok(Value::U64(0));
        }

        let result = move || -> rusqlite::Result<_> {
            let transaction = self.conn.transaction()?;
            let mut result: u64 = 0;
            {
                let mut statement = transaction.prepare_cached(&format!(
                    "update {} set {}=? where {}=? and {}=? and {}=?;",
                    notifications::TABLE, notifications::READ, ID, notifications::USER_ID, notifications::READ,
                ))?;
                for id in notification_ids {
                    result = result.saturating_add(statement.execute(rusqlite::params![true, id, user_id, false])? as u64);
                }
            }
            transaction.commit()?;
            Ok(Value::U64(result))
        };
        result().map_err(|e| Error::new(ErrorKind::Other, e))
    }

    /// # Deletes notifications via range of dates
    fn delete_notifications_via_range_of_dates(&self, user_id: Id, range_of_dates: RangeOfDates) -> io::Result<Value> {
        let result = move || -> rusqlite::Result<_> {
            let count = self.conn.execute(
                &format!(
                    "delete from {} where {}=? and {} >= ? and {} <= ?;",
                    notifications::TABLE, notifications::USER_ID, notifications::DATE_CREATED, notifications::DATE_CREATED,
                ),
                rusqlite::params![user_id, range_of_dates.start() as i64, range_of_dates.end() as i64],
            )?;
            Ok(Value::from(count as u64))
        };
        result().map_err(|e| Error::new(ErrorKind::Other, e))
    }

}

/// # Loads credentials
fn load_credentials(conn: &Connection) -> io::Result<Credentials> {
    use crate::db::users;

    let result = move || -> rusqlite::Result<_> {
        let mut statement = conn.prepare(&format!("select {},{},{} from {};", ID, users::NAME, users::PASSPHRASE, users::TABLE))?;
        let mut result = Credentials::with_capacity(50);
        for r in statement.query_map(rusqlite::NO_PARAMS, |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)))? {
            let (id, name, passphrase): (_, String, Vec<u8>) = r?;
            let passphrase = Passphrase::try_from(passphrase).map_err(|e| rusqlite::Error::UserFunctionError(Box::new(Error::from(e))))?;
            result.insert(encode_credentials(name, &passphrase), id);
        }
        Ok(result)
    };
    result().map_err(|e| Error::new(ErrorKind::Other, e))
}

/// # Encodes credentials
fn encode_credentials<S>(name: S, passphrase: &Passphrase) -> ReadOnlyZeros where S: AsRef<str> {
    let mut k = Keccak::new_sha3_512();
    k.update(name.as_ref().as_bytes());
    k.update(&tiny_keccak::sha3_512(passphrase.as_ref()));

    let mut z = Zeros::new_512();
    k.finalize(z.as_mut());

    ReadOnlyZeros::new(z)
}
