use crate::{
    auth::{AuthInfo, AuthInfoData, GetAuthInfo, Username},
    common::Storage,
    db_storage::models::{
        DieselAuthInfo, DieselAuthInfoEmail, DieselAuthInfoGithub, DieselGame, DieselGamePlayer,
        DieselGameScore, DieselPlayer,
    },
    env::{ENV_PG_URL, ENV_REDIS_URL},
};
use async_trait::async_trait;
use diesel::pg::PgConnection;
use diesel::prelude::*;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use mahjong_core::{Game, GameId, PlayerId, Players};
use redis::Commands;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use service_contracts::{ServiceGame, ServicePlayer, ServicePlayerGame};
use tracing::debug;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
use self::{
    models::{
        DieselAuthInfoAnonymous, DieselGameBoard, DieselGameDrawWall, DieselGameHand,
        DieselGameSettings,
    },
    models_translation::DieselGameExtra,
};
mod models;
mod models_translation;
mod schema;
pub struct DBStorage {
    db_path: String,
    redis_path: String,
}
#[derive(Serialize, Deserialize)]
struct FileContent {
    auth: Option<FxHashMap<Username, AuthInfo>>,
    games: Option<FxHashMap<GameId, Game>>,
    players: Option<FxHashMap<PlayerId, ServicePlayer>>,
}
#[async_trait]
impl Storage for DBStorage {
    async fn get_auth_info(&self, get_auth_info: GetAuthInfo) -> Result<Option<AuthInfo>, String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        match get_auth_info {
            GetAuthInfo::EmailUsername(username) => {
                DieselAuthInfoEmail::get_info_by_username(&mut connection, &username)
            }
            GetAuthInfo::GithubUsername(username) => {
                DieselAuthInfoGithub::get_info_by_username(&mut connection, &username)
            }
            GetAuthInfo::PlayerId(player_id) => {
                DieselAuthInfo::get_info_by_id(&mut connection, &player_id)
            }
            GetAuthInfo::AnonymousToken(id_token) => {
                DieselAuthInfoAnonymous::get_info_by_hashed_token(&mut connection, &id_token)
            }
        }
    }
    async fn get_player_total_score(&self, player_id: &PlayerId) -> Result<i32, String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        let total_score = DieselGameScore::read_total_from_player(&mut connection, player_id);
        Ok(total_score)
    }
    async fn save_auth_info(&self, auth_info: &AuthInfo) -> Result<(), String> {
        use schema::auth_info::table;
        use schema::auth_info_anonymous::table as anonymous_table;
        use schema::auth_info_email::table as email_table;
        use schema::auth_info_github::table as github_table;
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        let diesel_auth_info = DieselAuthInfo::from_raw(auth_info);
        diesel::insert_into(table)
            .values(&diesel_auth_info)
            .execute(&mut connection)
            .unwrap();
        match auth_info.data {
            AuthInfoData::Email(ref email) => {
                let diesel_auth_info_email = DieselAuthInfoEmail::from_raw(email);
                diesel::insert_into(email_table)
                    .values(&diesel_auth_info_email)
                    .execute(&mut connection)
                    .unwrap();
            }
            AuthInfoData::Github(ref github) => {
                let diesel_auth_info_github = DieselAuthInfoGithub::from_raw(github);
                diesel::insert_into(github_table)
                    .values(&diesel_auth_info_github)
                    .execute(&mut connection)
                    .unwrap();
            }
            AuthInfoData::Anonymous(ref anonymous) => {
                let diesel_auth_info_anonymous = DieselAuthInfoAnonymous::from_raw(anonymous);
                diesel::insert_into(anonymous_table)
                    .values(&diesel_auth_info_anonymous)
                    .execute(&mut connection)
                    .unwrap();
            }
        }
        Ok(())
    }
    async fn save_game(&self, service_game: &ServiceGame) -> Result<(), String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        let redis_client = redis::Client::open(self.redis_path.clone()).unwrap();
        let mut redis_connection = redis_client.get_connection().unwrap();
        let game_str = serde_json::to_string(&service_game).unwrap();
        let redis_key = format!("game:{}", service_game.game.id);
        let _: () = redis_connection.set(redis_key.clone(), game_str).unwrap();
        let _: () = redis_connection.expire(redis_key, 60 * 60).unwrap();
        DieselPlayer::update_from_game(&mut connection, service_game);
        let diesel_game_extra = DieselGameExtra {
            created_at: chrono::DateTime::from_timestamp_millis(service_game.created_at)
                .unwrap()
                .naive_utc(),
            game: service_game.game.clone(),
            updated_at: chrono::DateTime::from_timestamp_millis(service_game.updated_at)
                .unwrap()
                .naive_utc(),
        };
        let diesel_game = DieselGame::from_raw(&diesel_game_extra);
        diesel_game.update(&mut connection);
        let diesel_game_players = DieselGamePlayer::from_game(&service_game.game);
        DieselGamePlayer::update(&mut connection, &diesel_game_players, &service_game.game);
        DieselGameScore::update_from_game(&mut connection, service_game);
        DieselGameBoard::update_from_game(&mut connection, service_game);
        DieselGameDrawWall::update_from_game(&mut connection, service_game);
        DieselGameHand::update_from_game(&mut connection, service_game);
        DieselGameSettings::update_from_game(&mut connection, service_game);
        Ok(())
    }
    async fn get_game(&self, id: &GameId, use_cache: bool) -> Result<Option<ServiceGame>, String> {
        let redis_client = redis::Client::open(self.redis_path.clone()).unwrap();
        let mut redis_connection = redis_client.get_connection().unwrap();
        let redis_key = format!("game:{}", id);
        if use_cache {
            let game_str: Option<String> = redis_connection.get(redis_key).unwrap();
            if game_str.is_some() {
                let game: ServiceGame = serde_json::from_str(&game_str.unwrap()).unwrap();
                return Ok(Some(game));
            }
        } else {
            redis_connection.del::<String, bool>(redis_key).unwrap();
        }
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        let result = DieselGame::read_from_id(&mut connection, id);
        if result.is_none() {
            return Ok(None);
        }
        let game_players = DieselGamePlayer::read_from_game(&mut connection, id);
        let players = DieselPlayer::read_from_ids(&mut connection, &game_players);
        let score = DieselGameScore::read_from_game(&mut connection, id);
        let board = DieselGameBoard::read_from_game(&mut connection, id);
        let draw_wall = DieselGameDrawWall::read_from_game(&mut connection, id);
        let (hands, bonus_tiles) = DieselGameHand::read_from_game(&mut connection, id);
        let settings = DieselGameSettings::read_from_game(&mut connection, id);
        if settings.is_none() {
            return Ok(None);
        }
        let game_extra = result.unwrap();
        let mut game = game_extra.game;
        game.players = Players(game_players);
        game.score = score;
        game.table.hands = hands;
        game.table.bonus_tiles = bonus_tiles;
        game.table.board = board;
        game.table.draw_wall = draw_wall;
        let service_game = ServiceGame {
            created_at: game_extra.created_at.and_utc().timestamp_millis(),
            game,
            players,
            settings: settings.unwrap(),
            updated_at: game_extra.updated_at.and_utc().timestamp_millis(),
        };
        Ok(Some(service_game))
    }
    async fn get_player_games(
        &self,
        player_id: &Option<PlayerId>,
    ) -> Result<Vec<ServicePlayerGame>, String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        if player_id.is_some() {
            let result =
                DieselGamePlayer::read_from_player(&mut connection, &player_id.clone().unwrap());
            return Ok(result);
        }
        let all = DieselGame::read_player_games(&mut connection);
        Ok(all)
    }
    async fn get_player(&self, player_id: &PlayerId) -> Result<Option<ServicePlayer>, String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        let player = DieselPlayer::read_from_id(&mut connection, player_id);
        Ok(player)
    }
    async fn save_player(&self, player: &ServicePlayer) -> Result<(), String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        DieselPlayer::save(&mut connection, player);
        Ok(())
    }
    async fn delete_games(&self, ids: &[GameId]) -> Result<(), String> {
        let mut connection = PgConnection::establish(&self.db_path).unwrap();
        DieselGamePlayer::delete_games(&mut connection, ids);
        DieselGameScore::delete_games(&mut connection, ids);
        DieselGameBoard::delete_games(&mut connection, ids);
        DieselGameDrawWall::delete_games(&mut connection, ids);
        DieselGameHand::delete_games(&mut connection, ids);
        DieselGameSettings::delete_games(&mut connection, ids);
        DieselGame::delete_games(&mut connection, ids);
        Ok(())
    }
}
impl DBStorage {
    #[allow(dead_code)]
    pub fn new_dyn() -> Box<dyn Storage> {
        let db_path = std::env::var(ENV_PG_URL)
            .unwrap_or("postgres://postgres:postgres@localhost/mahjong".to_string());
        let redis_path = std::env::var(ENV_REDIS_URL).unwrap_or("redis://localhost".to_string());
        debug!("DBStorage: {} {}", db_path, redis_path);
        let file_storage = Self {
            db_path: db_path.clone(),
            redis_path,
        };
        let mut connection = PgConnection::establish(&db_path).unwrap();
        connection.run_pending_migrations(MIGRATIONS).unwrap();
        Box::new(file_storage)
    }
}