use data::tables::{AvatarBaseID, ProcedureConfigID};
use proto::{DisconnectReason, DisconnectScNotify, PlayerSyncScNotify};

use crate::ServerState;

use super::ArgSlice;

pub async fn avatar(
    args: ArgSlice<'_>,
    state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
    const USAGE: &str = "Usage: player avatar [player_uid] [avatar_id]";

    if args.len() != 2 {
        return Ok(USAGE.to_string());
    }

    let uid = args[0].parse::<u32>()?;
    let avatar_id = args[1].parse::<u32>()?;
    let Some(avatar_id) = AvatarBaseID::new(avatar_id) else {
        return Ok(format!("avatar with id {avatar_id} doesn't exist"));
    };

    let Some(player_lock) = state.player_mgr.get_player(uid).await else {
        return Ok(String::from("player not found"));
    };

    let should_save = {
        let mut player = player_lock.lock().await;
        player.basic_data_model.frontend_avatar_id = Some(avatar_id);
        player.current_session_id().is_none()
    };

    if should_save {
        state.player_mgr.save_and_remove(uid).await;
    }

    Ok(format!(
        "changed frontend_avatar_id, you have to re-enter main city now"
    ))
}

pub async fn nickname(
    args: ArgSlice<'_>,
    state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
    const USAGE: &str = "Usage: player nickname [uid] [nickname]";

    if args.len() != 2 {
        return Ok(USAGE.to_string());
    }

    let uid = args[0].parse::<u32>()?;
    let nickname = args[1].trim();

    if !matches!(nickname.len(), (4..=15)) {
        return Ok(String::from(
            "nickname should contain at least 4 and not more than 15 characters",
        ));
    }

    let Some(player_lock) = state.player_mgr.get_player(uid).await else {
        return Ok(String::from("player not found"));
    };

    let (session_id, basic_info) = {
        let mut player = player_lock.lock().await;
        player.basic_data_model.nick_name = Some(nickname.to_string());

        (
            player.current_session_id(),
            player.basic_data_model.player_basic_info(),
        )
    };

    if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
        session
            .notify(PlayerSyncScNotify {
                basic_info: Some(basic_info),
                ..Default::default()
            })
            .await?;
    } else {
        state.player_mgr.save_and_remove(uid).await;
    }

    Ok(format!(
        "successfully changed player {uid} nickname to {nickname}"
    ))
}

pub async fn procedure(
    args: ArgSlice<'_>,
    state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
    const USAGE: &str = "Usage: player procedure [uid] [procedure_id]";

    if args.len() != 2 {
        return Ok(USAGE.to_string());
    }

    let uid = args[0].parse::<u32>()?;
    let procedure_id = args[1].parse::<i32>()?;

    let procedure_id = match procedure_id {
        1.. => ProcedureConfigID::new(procedure_id as u32),
        _ => None,
    };

    let Some(player_lock) = state.player_mgr.get_player(uid).await else {
        return Ok(String::from("player not found"));
    };

    let session_id = {
        let mut player = player_lock.lock().await;
        player.basic_data_model.beginner_procedure_id = procedure_id;

        player.current_session_id()
    };

    if session_id.is_none() {
        state.player_mgr.save_and_remove(uid).await;
    }

    Ok(format!(
        "successfully changed procedure_id to {procedure_id:?}"
    ))
}

pub async fn kick(
    args: ArgSlice<'_>,
    state: &ServerState,
) -> Result<String, Box<dyn std::error::Error>> {
    const USAGE: &str = "Usage: player kick [player_uid]";

    if args.len() > 2 {
        return Ok(USAGE.to_string());
    }

    let uid = args[0].parse::<u32>()?;
    let default_reason = DisconnectReason::ServerKick.into();
    let reason = match args.get(1) {
        Some(arg) => match arg.parse::<i32>() {
            Ok(val) => val,
            Err(_err) => default_reason,
        },
        None => default_reason,
    };
    let reason_str = match DisconnectReason::try_from(reason) {
        Ok(converted_enum) => converted_enum.as_str_name().to_owned(),
        Err(_err) => reason.to_string(),
    };

    let Some(player_lock) = state.player_mgr.get_player(uid).await else {
        return Ok(String::from("player not found"));
    };

    let session_id = player_lock.lock().await.current_session_id();

    if let Some(session) = session_id.map(|id| state.session_mgr.get(id)).flatten() {
        session
            .notify(DisconnectScNotify { reason: reason })
            .await?;
        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
        session.shutdown().await?;
        Ok(format!("kicked player, uid: {uid}, reason: {reason_str}"))
    } else {
        Ok(format!("player uid: {uid} is not online yet."))
    }
}
