use std::sync::Arc;

use dashmap::DashMap;
use prost::Message;
use proto::{PlayerDataBin, Retcode};
use sqlx::PgPool;
use tokio::sync::Mutex;

use crate::database::{self, DbError};

use super::Player;

pub type SharedPlayerLock = Arc<Mutex<Player>>;

pub struct PlayerManager {
    players: DashMap<u32, SharedPlayerLock>,
    pg_pool: PgPool,
}

impl PlayerManager {
    pub fn new(pg_pool: PgPool) -> Self {
        Self {
            players: DashMap::new(),
            pg_pool,
        }
    }

    pub async fn authorize(
        &self,
        account_uid: &str,
        token: &str,
        platform_type: u32,
    ) -> Result<u32, Retcode> {
        let combo_token =
            match database::select_combo_token_by_account(&self.pg_pool, account_uid).await {
                Ok(row) => row.token,
                Err(_) => return Err(Retcode::RetFail),
            };

        (combo_token == token)
            .then_some(())
            .ok_or(Retcode::RetFail)?;

        let player_uid = match database::select_player_uid_by_account(&self.pg_pool, account_uid)
            .await
        {
            Ok(row) => row.uid,
            Err(DbError::NotFound) => {
                database::insert_player_uid_by_account(&self.pg_pool, account_uid, platform_type)
                    .await
                    .map_err(|err| {
                        tracing::error!("failed to insert player_uid: {err}");
                        Retcode::RetFail
                    })?
                    .uid
            }
            Err(_) => return Err(Retcode::RetFail),
        } as u32;

        Ok(player_uid)
    }

    pub async fn get_player(&self, uid: u32) -> Option<SharedPlayerLock> {
        if let Some(player) = self.players.get(&uid).map(|a| (*a).clone()) {
            return Some(player);
        }

        let player_bin_data =
            match database::select_player_data_by_uid(&self.pg_pool, uid as i32).await {
                Ok(row) => Some(row.bin_data),
                Err(DbError::NotFound) => None,
                Err(DbError::SqlxError(err)) => {
                    tracing::error!("SQL error: {err}");
                    return None;
                }
            };

        let player = match player_bin_data {
            Some(bin_data) => match PlayerDataBin::decode(&*bin_data) {
                Ok(bin) => Player::load_from_bin(bin),
                Err(err) => {
                    tracing::error!("failed to decode PlayerDataBin for uid {uid}, error: {err}");
                    return None;
                }
            },
            None => {
                let mut player = Player::default();
                player.on_first_login();

                match database::insert_player_data_by_uid(
                    &self.pg_pool,
                    uid as i32,
                    player.save_to_bin().encode_to_vec(),
                )
                .await
                {
                    Ok(()) => player,
                    Err(err) => {
                        tracing::error!("failed to insert player data, uid: {uid}, error: {err}");
                        return None;
                    }
                }
            }
        };

        self.players.insert(uid, Arc::new(Mutex::new(player)));
        self.players.get(&uid).map(|a| (*a).clone())
    }

    pub async fn save(&self, player_uid: u32) {
        let Some(player_lock) = self.players.get(&player_uid) else {
            tracing::warn!("save: player with uid {player_uid} not found");
            return;
        };

        let player = player_lock.lock().await;
        self.do_save(player_uid, &*player).await;
    }

    pub async fn save_and_remove(&self, player_uid: u32) {
        let Some((_, player_lock)) = self.players.remove(&player_uid) else {
            tracing::warn!("save_and_remove: player with uid {player_uid} not found");
            return;
        };

        let player = player_lock.lock().await;
        self.do_save(player_uid, &*player).await;
    }

    async fn do_save(&self, uid: u32, player: &Player) {
        let bin = player.save_to_bin();

        if let Err(err) =
            database::update_player_data_by_uid(&self.pg_pool, uid as i32, bin.encode_to_vec())
                .await
        {
            tracing::error!("failed to update player data: {err}");
        }

        tracing::info!("player data flushed to database, uid: {uid}");
    }
}
