use crate::commands::ExecutionResult;
use crate::config::*;
use crate::db::sqlite::Database;
use crate::db::DbState;
use crate::error::BotError;
use crate::state::DiceBotState;
use log::info;
use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, SyncSettings};
use std::clone::Clone;
use std::sync::{Arc, RwLock};

mod command_execution;
pub mod event_handlers;

/// How many commands can be in one message. If the amount is higher
/// than this, we reject execution.
const MAX_COMMANDS_PER_MESSAGE: usize = 50;

/// The DiceBot struct represents an active dice bot. The bot is not
/// connected to Matrix until its run() function is called.
pub struct DiceBot {
    /// A reference to the configuration read in on application start.
    config: Arc<Config>,

    /// The matrix client.
    client: Client,

    /// State of the dicebot
    state: Arc<RwLock<DiceBotState>>,

    /// Active database layer
    db: Database,
}

impl DiceBot {
    /// Create a new dicebot with the given configuration and state
    /// actor. This function returns a Result because it is possible
    /// for client creation to fail for some reason (e.g. invalid
    /// homeserver URL).
    pub fn new(
        config: &Arc<Config>,
        state: &Arc<RwLock<DiceBotState>>,
        db: &Database,
        client: &Client,
    ) -> Result<Self, BotError> {
        Ok(DiceBot {
            client: client.clone(),
            config: config.clone(),
            state: state.clone(),
            db: db.clone(),
        })
    }

    /// Logs in to matrix and potentially records a new device ID. If
    /// no device ID is found in the database, a new one will be
    /// generated by the matrix SDK, and we will store it.
    async fn login(&self, client: &Client) -> Result<(), BotError> {
        let username = self.config.matrix_username();
        let password = self.config.matrix_password();

        // Pull device ID from database, if it exists. Then write it
        // to DB if the library generated one for us.
        let device_id: Option<String> = self.db.get_device_id().await?;
        let device_id: Option<&str> = device_id.as_deref();

        client
            .login(username, password, device_id, Some("matrix dice bot"))
            .await?;

        if device_id.is_none() {
            let device_id = client.device_id().await.ok_or(BotError::NoDeviceIdFound)?;
            self.db.set_device_id(device_id.as_str()).await?;
            info!("Recorded new device ID: {}", device_id.as_str());
        } else {
            info!("Using existing device ID: {}", device_id.unwrap());
        }

        info!("Logged in as {}", username);
        Ok(())
    }

    /// Logs the bot in to Matrix and listens for events until program
    /// terminated, or a panic occurs. Originally adapted from the
    /// matrix-rust-sdk command bot example.
    pub async fn run(self) -> Result<(), BotError> {
        let client = self.client.clone();
        self.login(&client).await?;

        client.set_event_handler(Box::new(self)).await;
        info!("Listening for commands");

        // TODO replace with sync_with_callback for cleaner shutdown
        // process.
        client.sync(SyncSettings::default()).await;
        Ok(())
    }

    async fn execute_commands(
        &self,
        room: &Joined,
        sender: &str,
        msg_body: &str,
    ) -> Vec<(String, ExecutionResult)> {
        let commands: Vec<&str> = msg_body
            .lines()
            .filter(|line| line.starts_with("!"))
            .take(MAX_COMMANDS_PER_MESSAGE + 1)
            .collect();

        //Up to 50 commands allowed, otherwise we send back an error.
        let results: Vec<(String, ExecutionResult)> = if commands.len() < MAX_COMMANDS_PER_MESSAGE {
            command_execution::execute(commands, &self.db, &self.client, room, sender).await
        } else {
            vec![("".to_owned(), Err(BotError::MessageTooLarge))]
        };

        results
    }

    pub async fn handle_results(
        &self,
        room: &Joined,
        sender_username: &str,
        event_id: EventId,
        results: Vec<(String, ExecutionResult)>,
    ) {
        if results.len() >= 1 {
            if results.len() == 1 {
                command_execution::handle_single_result(
                    &self.client,
                    &results[0].1,
                    sender_username,
                    &room,
                    event_id,
                )
                .await;
            } else if results.len() > 1 {
                command_execution::handle_multiple_results(
                    &self.client,
                    &results,
                    sender_username,
                    &room,
                )
                .await;
            }
        }
    }
}
