use std::{collections::HashMap, sync::Arc};

use async_std::sync::RwLock;
use chrono::{DateTime, Duration, TimeDelta, Utc};
use mime::Mime;
use result_extended::{error, fatal, ok, result_as_fatal, return_error, ResultExt};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use typeshare::typeshare;
use uuid::Uuid;

use crate::{
    asset_db::{self, AssetId, Assets},
    database::{CharacterId, Database, GameId, SessionId, UserId},
    types::AccountState,
    types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview},
};

const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
    red: 0xca,
    green: 0xb9,
    blue: 0xbb,
};

#[derive(Clone, Serialize)]
#[typeshare]
pub struct Status {
    pub ok: bool,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "content")]
#[typeshare]
pub enum AuthResponse {
    Success(SessionId),
    PasswordReset(SessionId),
    Locked,
}

#[derive(Debug)]
struct WebsocketClient {
    sender: Option<UnboundedSender<Message>>,
}

pub struct AppState {
    pub asset_store: Box<dyn Assets + Sync + Send + 'static>,
    pub db: Box<dyn Database + Sync + Send + 'static>,
    pub clients: HashMap<String, WebsocketClient>,

    pub tabletop: Tabletop,
}

#[derive(Clone)]
pub struct Core(Arc<RwLock<AppState>>);

impl Core {
    pub fn new<A, DB>(assetdb: A, db: DB) -> Self
    where
        A: Assets + Sync + Send + 'static,
        DB: Database + Sync + Send + 'static,
    {
        Self(Arc::new(RwLock::new(AppState {
            asset_store: Box::new(assetdb),
            db: Box::new(db),
            clients: HashMap::new(),
            tabletop: Tabletop {
                background_color: DEFAULT_BACKGROUND_COLOR,
                background_image: None,
            },
        })))
    }

    pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
        /*
        let state = self.0.write().await;
        let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await {
            Ok(Some(admin_user)) => ok(admin_user),
            Ok(None) => {
                return ok(Status {
                    admin_enabled: false,
                });
            }
            Err(err) => fatal(err),
        });

        ok(Status {
            ok: !admin_user.password.is_empty(),
        })
        */
        ok(Status { ok: true })
    }

    pub async fn register_client(&self) -> String {
        let mut state = self.0.write().await;
        let uuid = Uuid::new_v4().simple().to_string();

        let client = WebsocketClient { sender: None };

        state.clients.insert(uuid.clone(), client);
        uuid
    }

    pub async fn unregister_client(&self, client_id: String) {
        let mut state = self.0.write().await;
        let _ = state.clients.remove(&client_id);
    }

    pub async fn connect_client(&self, client_id: String) -> UnboundedReceiver<Message> {
        let mut state = self.0.write().await;

        match state.clients.get_mut(&client_id) {
            Some(client) => {
                let (tx, rx) = unbounded_channel();
                client.sender = Some(tx);
                rx
            }
            None => {
                unimplemented!();
            }
        }
    }

    pub async fn user_by_username(
        &self,
        username: &str,
    ) -> ResultExt<Option<User>, AppError, FatalError> {
        let state = self.0.read().await;
        match state.db.user_by_username(username).await {
            Ok(Some(user_row)) => ok(Some(User::from(user_row))),
            Ok(None) => ok(None),
            Err(err) => fatal(err),
        }
    }

    pub async fn list_users(&self) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
        let users = self.0.write().await.db.users().await;
        match users {
            Ok(users) => ok(users
                .into_iter()
                .map(|user| UserOverview {
                    id: user.id,
                    name: user.name,
                    state: user.state,
                    is_admin: user.admin,
                })
                .collect()),
            Err(err) => fatal(err),
        }
    }

    pub async fn user(
        &self,
        user_id: UserId,
    ) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
        let users = return_error!(self.list_users().await);
        match users.into_iter().find(|user| user.id == user_id) {
            Some(user) => ok(Some(user)),
            None => return ok(None),
        }
    }

    pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> {
        let state = self.0.read().await;
        match return_error!(self.user_by_username(username).await) {
            Some(_) => error(AppError::UsernameUnavailable),
            None => match state
                .db
                .create_user(username, "", false, AccountState::PasswordReset(Utc::now() + Duration::minutes(60)))
                .await
            {
                Ok(user_id) => ok(user_id),
                Err(err) => fatal(err),
            },
        }
    }

    pub async fn disable_user(&self, _userid: UserId) -> ResultExt<(), AppError, FatalError> {
        unimplemented!();
    }

    pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> {
        let games = self.0.read().await.db.games().await;
        match games {
            // Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
            Ok(games) => ok(games
                .into_iter()
                .map(|game| GameOverview {
                    id: game.id,
                    type_: "".to_owned(),
                    name: game.name,
                    gm: game.gm,
                    players: game.players,
                })
                .collect::<Vec<GameOverview>>()),
            Err(err) => fatal(err),
        }
    }

    pub async fn create_game(
        &self,
        gm: &UserId,
        game_type: &str,
        game_name: &str,
    ) -> ResultExt<GameId, AppError, FatalError> {
        let state = self.0.read().await;
        match state.db.create_game(gm, game_type, game_name).await {
            Ok(game_id) => ok(game_id),
            Err(err) => fatal(err),
        }
    }

    pub async fn tabletop(&self) -> Tabletop {
        self.0.read().await.tabletop.clone()
    }

    pub async fn get_asset(
        &self,
        asset_id: AssetId,
    ) -> ResultExt<(Mime, Vec<u8>), AppError, FatalError> {
        ResultExt::from(
            self.0
                .read()
                .await
                .asset_store
                .get(asset_id.clone())
                .map_err(|err| match err {
                    asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)),
                    asset_db::Error::Inaccessible => {
                        AppError::Inaccessible(format!("{}", asset_id))
                    }
                    asset_db::Error::Unexpected(err) => AppError::Inaccessible(format!("{}", err)),
                }),
        )
    }

    pub async fn available_images(&self) -> Vec<AssetId> {
        self.0
            .read()
            .await
            .asset_store
            .assets()
            .filter_map(
                |(asset_id, value)| match mime_guess::from_path(value).first() {
                    Some(mime) if mime.type_() == mime::IMAGE => Some(asset_id.clone()),
                    _ => None,
                },
            )
            .collect()
    }

    pub async fn set_background_image(
        &self,
        asset: AssetId,
    ) -> ResultExt<(), AppError, FatalError> {
        let tabletop = {
            let mut state = self.0.write().await;
            state.tabletop.background_image = Some(asset.clone());
            state.tabletop.clone()
        };
        self.publish(Message::UpdateTabletop(tabletop)).await;
        ok(())
    }

    pub async fn get_charsheet(
        &self,
        id: CharacterId,
    ) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> {
        let state = self.0.write().await;
        let cr = state.db.character(&id).await;
        match cr {
            Ok(Some(row)) => ok(Some(row.data)),
            Ok(None) => ok(None),
            Err(err) => fatal(err),
        }
    }

    pub async fn publish(&self, message: Message) {
        let state = self.0.read().await;

        state.clients.values().for_each(|client| {
            if let Some(ref sender) = client.sender {
                let _ = sender.send(message.clone());
            }
        });
    }

    pub async fn save_user(
        &self,
        id: UserId,
        name: &str,
        password: &str,
        admin: bool,
        account_state: AccountState,
    ) -> ResultExt<UserId, AppError, FatalError> {
        let state = self.0.read().await;
        match state
            .db
            .save_user(User {
                id,
                name: name.to_owned(),
                password: password.to_owned(),
                admin,
                state: account_state,
            })
            .await
        {
            Ok(uuid) => ok(uuid),
            Err(err) => fatal(err),
        }
    }

    pub async fn set_password(
        &self,
        uuid: UserId,
        password: String,
    ) -> ResultExt<(), AppError, FatalError> {
        let state = self.0.write().await;
        let user = match state.db.user(&uuid).await {
            Ok(Some(row)) => row,
            Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())),
            Err(err) => return fatal(err),
        };
        match state
            .db
            .save_user(User {
                password,
                state: AccountState::Normal,
                ..user
            })
            .await
        {
            Ok(_) => ok(()),
            Err(err) => fatal(err),
        }
    }

    pub async fn auth(
        &self,
        username: &str,
        password: &str,
    ) -> ResultExt<AuthResponse, AppError, FatalError> {
        let now = Utc::now();
        let state = self.0.read().await;
        let user = state.db.user_by_username(username).await.unwrap().unwrap();
        let user_info = return_error!(match state.db.user_by_username(username).await {
            Ok(Some(row)) if row.password == password => ok(row),
            Ok(_) => error(AppError::AuthFailed),
            Err(err) => fatal(err),
        });

        match user_info.state {
            AccountState::Normal => result_as_fatal(state.db.create_session(&user_info.id).await)
                .map(|session_id| AuthResponse::Success(session_id)),

            AccountState::PasswordReset(exp) => {
                if exp < now {
                    error(AppError::AuthFailed)
                } else {
                    result_as_fatal(state.db.create_session(&user_info.id).await)
                        .map(|session_id| AuthResponse::PasswordReset(session_id))
                }
            }

            AccountState::Locked => ok(AuthResponse::Locked),
        }
    }

    pub async fn session(
        &self,
        session_id: &SessionId,
    ) -> ResultExt<Option<User>, AppError, FatalError> {
        let state = self.0.read().await;
        match state.db.session(session_id).await {
            Ok(Some(user_row)) => ok(Some(User::from(user_row))),
            Ok(None) => ok(None),
            Err(fatal_error) => fatal(fatal_error),
        }
    }

    pub async fn delete_session(&self, session_id: &SessionId) -> ResultExt<(), AppError, FatalError> {
        let state = self.0.read().await;
        match state.db.delete_session(session_id).await {
            Ok(_) => ok(()),
            Err(err) => fatal(err),
        }
    }
}

fn create_expiration_date() -> DateTime<Utc> {
    Utc::now() + TimeDelta::days(365)
}

#[cfg(test)]
mod test {
    use std::path::PathBuf;

    use super::*;

    use cool_asserts::assert_matches;

    use crate::{asset_db::mocks::MemoryAssets, database::DbConn};

    async fn test_core() -> Core {
        let assets = MemoryAssets::new(vec![
            (
                AssetId::from("asset_1"),
                "asset_1.png".to_owned(),
                String::from("abcdefg").into_bytes(),
            ),
            (
                AssetId::from("asset_2"),
                "asset_2.jpg".to_owned(),
                String::from("abcdefg").into_bytes(),
            ),
            (
                AssetId::from("asset_3"),
                "asset_3".to_owned(),
                String::from("abcdefg").into_bytes(),
            ),
            (
                AssetId::from("asset_4"),
                "asset_4".to_owned(),
                String::from("abcdefg").into_bytes(),
            ),
            (
                AssetId::from("asset_5"),
                "asset_5".to_owned(),
                String::from("abcdefg").into_bytes(),
            ),
        ]);
        let memory_db: Option<PathBuf> = None;
        let conn = DbConn::new(memory_db);
        conn.create_user("admin", "aoeu", true, AccountState::Normal)
            .await
            .unwrap();
        conn.create_user(
            "gm_1",
            "aoeu",
            false,
            AccountState::PasswordReset(Utc::now()),
        )
        .await
        .unwrap();
        Core::new(assets, conn)
    }

    #[tokio::test]
    async fn it_lists_available_images() {
        let core = test_core().await;
        let image_paths = core.available_images().await;
        assert_eq!(image_paths.len(), 2);
    }

    #[tokio::test]
    async fn it_retrieves_an_asset() {
        let core = test_core().await;
        assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => {
            assert_eq!(mime.type_(), mime::IMAGE);
            assert_eq!(data, "abcdefg".as_bytes());
        });
    }

    #[tokio::test]
    async fn it_can_retrieve_the_default_tabletop() {
        let core = test_core().await;
        assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
            assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
            assert_eq!(background_image, None);
        });
    }

    #[tokio::test]
    async fn it_can_change_the_tabletop_background() {
        let core = test_core().await;
        assert_matches!(
            core.set_background_image(AssetId::from("asset_1")).await,
            ResultExt::Ok(())
        );
        assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
            assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
            assert_eq!(background_image, Some(AssetId::from("asset_1")));
        });
    }

    #[tokio::test]
    async fn it_sends_notices_to_clients_on_tabletop_change() {
        let core = test_core().await;
        let client_id = core.register_client().await;
        let mut receiver = core.connect_client(client_id).await;

        assert_matches!(
            core.set_background_image(AssetId::from("asset_1")).await,
            ResultExt::Ok(())
        );
        match receiver.recv().await {
            Some(Message::UpdateTabletop(Tabletop {
                background_color,
                background_image,
            })) => {
                assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
                assert_eq!(background_image, Some(AssetId::from("asset_1")));
            }
            None => panic!("receiver did not get a message"),
        }
    }

    #[tokio::test]
    async fn it_creates_a_sessionid_on_successful_auth() {
        let core = test_core().await;
        match core.auth("admin", "aoeu").await {
            ResultExt::Ok(AuthResponse::Success(session_id)) => {
                let st = core.0.read().await;
                match st.db.session(&session_id).await {
                    Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
                    Ok(None) => panic!("no matching user row for the session id"),
                    Err(err) => panic!("{}", err),
                }
            }
            ResultExt::Ok(AuthResponse::PasswordReset(_)) => panic!("user is in password reset state"),
            ResultExt::Ok(AuthResponse::Locked) => panic!("user has been locked"),
            ResultExt::Err(err) => panic!("{}", err),
            ResultExt::Fatal(err) => panic!("{}", err),
        }
    }
}