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

//! # Request handler

use {
    std::{
        borrow::Cow,
        collections::HashSet,
        convert::TryFrom,
        io::{Read, Write},
        sync::mpsc,
    },

    bailamos::{
        db::{
            Id,
            notifications::Notification,
        },
        rpc::{self, Content},
    },
    binn_ir::Value,
    blackhole::Job,
    native_tls::TlsStream,

    crate::{
        task::{
            ResponseSender,
            db_manager::{self, MsgSender},
        },
    },
};

type TaskResult<'a> = (rpc::StatusCode, Cow<'a, str>, Option<Value>);

const OK: Cow<str> = Cow::Borrowed("Ok");

/// # Request handler
pub struct RequestHandler<S> where S: Read + Write {
    stream: Option<TlsStream<S>>,
    db_msg_sender: MsgSender,
    request: Content,
    user_id: Id,
}

impl<S> RequestHandler<S> where S: Read + Write {

    /// # Makes new instance
    pub fn new(stream: TlsStream<S>, db_msg_sender: MsgSender, request: Content, user_id: Id) -> Self {
        Self {
            stream: Some(stream),
            db_msg_sender,
            request,
            user_id,
        }
    }

    /// # Inserts notification
    fn insert_notification(&mut self) -> TaskResult {
        match self.request.remove(&rpc::REQUEST_PARAM_DATA) {
            Some(data) => match Notification::try_from(data) {
                Ok(notification) => {
                    let (sender, receiver) = mpsc::channel();
                    match self.db_msg_sender.send(
                        db_manager::Msg::InsertNotification { notification, user_id: self.user_id, result_sender: sender },
                    ) {
                        Ok(()) => match receiver.recv() {
                            Ok(Ok(())) => (rpc::STATUS_OK, OK, None),
                            Ok(Err(err)) => (rpc::STATUS_INVALID_INPUT, err.to_string().into(), None),
                            Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
                        },
                        Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
                    }
                },
                Err(err) => (rpc::STATUS_INVALID_INPUT, err.to_string().into(), None),
            },
            None => (rpc::STATUS_INVALID_INPUT, "Missing notification data".into(), None),
        }
    }

    /// # Loads unread notifications
    fn load_unread_notifications(&self) -> TaskResult {
        let (sender, receiver) = mpsc::channel();
        match self.db_msg_sender.send(db_manager::Msg::LoadUnreadNotifications { user_id: self.user_id, result_sender: sender }) {
            Ok(()) => match receiver.recv() {
                Ok(Ok(data)) => (rpc::STATUS_OK, OK, Some(data)),
                Ok(Err(err)) => (rpc::STATUS_INVALID_INPUT, err.to_string().into(), None),
                Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
            },
            Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
        }
    }

    /// # Marks notifications as read
    fn mark_notifications_as_read(&mut self) -> TaskResult {
        const INVALID: TaskResult = (rpc::STATUS_INVALID_INPUT, Cow::Borrowed("Invalid notification IDs"), None);

        match self.request.remove(&rpc::REQUEST_PARAM_DATA) {
            Some(Value::List(list)) => {
                let mut notification_ids = HashSet::with_capacity(list.len());
                for item in list {
                    match item {
                        Value::I64(id) => notification_ids.insert(id),
                        _ => return INVALID,
                    };
                }

                let (sender, receiver) = mpsc::channel();
                match self.db_msg_sender.send(db_manager::Msg::MarkNotificationsAsRead {
                    user_id: self.user_id, notification_ids, result_sender: sender,
                }) {
                    Ok(()) => match receiver.recv() {
                        Ok(Ok(data)) => (rpc::STATUS_OK, OK, Some(data)),
                        Ok(Err(err)) => (rpc::STATUS_INVALID_INPUT, err.to_string().into(), None),
                        Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
                    },
                    Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
                }
            },
            _ => INVALID,
        }
    }

    /// # Deletes notifications via range of dates
    fn delete_notifications_via_range_of_dates(&mut self) -> TaskResult {
        const INVALID: TaskResult = (rpc::STATUS_INVALID_INPUT, Cow::Borrowed("Invalid date range"), None);

        match self.request.remove(&rpc::REQUEST_PARAM_DATA) {
            Some(Value::List(mut list)) => match (list.pop(), list.pop(), list.pop()) {
                (Some(Value::U64(start)), Some(Value::U64(end)), None) => {
                    let (sender, receiver) = mpsc::channel();
                    match self.db_msg_sender.send(db_manager::Msg::DeleteNotificationsViaRangeOfDates {
                        user_id: self.user_id, range_of_dates: db_manager::RangeOfDates::new(start, end), result_sender: sender,
                    }) {
                        Ok(()) => match receiver.recv() {
                            Ok(Ok(data)) => (rpc::STATUS_OK, OK, Some(data)),
                            Ok(Err(err)) => (rpc::STATUS_INVALID_INPUT, err.to_string().into(), None),
                            Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
                        },
                        Err(err) => (rpc::STATUS_SERVER_ERROR, err.to_string().into(), None),
                    }
                },
                _ => INVALID,
            },
            _ => INVALID,
        }
    }

}

impl<S> Job for RequestHandler<S> where S: Read + Write + Send + 'static {

    fn run(&mut self) -> Option<Box<dyn Job>> {
        match self.stream.take() {
            Some(stream) => {
                let (status_code, msg, data): (_, Cow<str>, _) = match (
                    self.request.get(&rpc::REQUEST_PARAM_METHOD), self.request.get(&rpc::REQUEST_PARAM_WHAT),
                ) {
                    (Some(Value::U8(rpc::REQUEST_METHOD_POST)), Some(Value::U8(rpc::REQUEST_FOR_INSERTING_NEW_NOTIFICATION)))
                    => self.insert_notification(),
                    (Some(Value::U8(rpc::REQUEST_METHOD_GET)), Some(Value::U8(rpc::REQUEST_FOR_GETTING_UNREAD_NOTIFICATIONS)))
                    => self.load_unread_notifications(),
                    (Some(Value::U8(rpc::REQUEST_METHOD_POST)), Some(Value::U8(rpc::REQUEST_FOR_MARKING_NOTIFICATIONS_AS_READ)))
                    => self.mark_notifications_as_read(),
                    (Some(Value::U8(rpc::REQUEST_METHOD_DELETE)), Some(Value::U8(rpc::REQUEST_FOR_DELETING_NOTIFICATIONS_VIA_RANGE_OF_DATES)))
                    => self.delete_notifications_via_range_of_dates(),
                    _ => (rpc::STATUS_INVALID_INPUT, "What content do you want to request?".into(), None),
                };
                match rpc::make_response(status_code, msg, data) {
                    Ok(response) => return Some(Box::new(ResponseSender::new(stream, response))),
                    Err(err) => __e!("{}", err),
                };
            },
            None => print_internal_error!(),
        };

        None
    }

}
