use std::{error::Error, net::Ipv4Addr, sync::Arc};

use common::{
    base::NetService,
    net::{tools::Packet, ServiceType},
    tools::time_utils,
    ServiceQualifier,
};
use proto::{
    PacketHead, PlayerLoginReq, PlayerLoginRsp, PlayerLogoutReason, PlayerLogoutReq,
    PlayerStatusType, Retcode,
};

use crate::{gameserver_service, network::PlayerSessionProxy, player::Player};

pub async fn on_player_login_req(packet: Packet) -> Result<(), Box<dyn Error>> {
    let Ok(req) = packet.get_proto::<PlayerLoginReq>() else {
        tracing::error!(
            "get_proto PlayerLoginReq failed, uid: {}",
            packet.head.user_id
        );
        return Ok(());
    };

    if let Err(ret) = player_login(packet, req).await {
        tracing::error!("LoginHandler::player_login failed, retcode: {ret:?}",);
    }

    Ok(())
}

async fn player_login(packet: Packet, req: PlayerLoginReq) -> Result<(), Retcode> {
    let client_ip = Ipv4Addr::from(packet.head.user_ip).to_string();
    tracing::info!("[TEST_IP] playerLogin, ip:{client_ip}");

    let uid = packet.head.user_id;

    let session = if let Some(session) = gameserver_service().find_player_session(uid) {
        if is_from_same_client_session(&session, &packet) {
            tracing::warn!("RET_REPEAT_LOGIN uid: {uid}");
            return Err(Retcode::RetRepeatLogin);
        }

        // tracing::error!("GameserverService::bind_player_session: not implemented.");
        // stop previous session

        gameserver_service().remove_player_session(uid);
        gameserver_service().player_mgr.remove_from_online_map(uid);

        if let Some(coro) = session.exec_coroutine.read().as_ref() {
            coro.push_packet(Packet::new(
                PlayerLogoutReq {
                    reason: PlayerLogoutReason::ClientReq.into(),
                },
                PacketHead {
                    user_id: session.uid,
                    user_session_id: session.get_gateserver_session_id(),
                    ..Default::default()
                },
            ));
        }

        let Some(session) = gameserver_service().create_player_session(&packet, req.clone()) else {
            tracing::error!("create_player_session failed");
            return Err(Retcode::RetFail);
        };

        session
    } else {
        let Some(session) = gameserver_service().create_player_session(&packet, req.clone()) else {
            tracing::error!("create_player_session failed");
            return Err(Retcode::RetFail);
        };

        session
    };

    session.gen_new_login_rand();
    async_modify_player_status(
        &packet,
        PlayerStatusType::PlayerStatusOnline,
        ModifyPlayerStatusContext {
            last_login_rand: req.login_rand,
            cur_login_rand: session.get_cur_login_rand(),
        },
    )
    .await;

    let service = gameserver_service();
    let _ = service
        .service_box
        .push_packet_to_service(
            ServiceQualifier::Type(ServiceType::Dbgate),
            Packet::new(
                PlayerLoginReq {
                    login_rand: session.get_cur_login_rand(),
                    ..req
                },
                PacketHead {
                    sender_app_id: service.network_mgr.app_id(),
                    ..packet.head
                },
            ),
        )
        .await;

    Ok(())
}

async fn async_modify_player_status(
    packet: &Packet,
    status: PlayerStatusType,
    context: ModifyPlayerStatusContext,
) {
    let app_id = gameserver_service().network_mgr.app_id();
    gameserver_service()
        .redis_mgr
        .compare_and_modify_player_status(
            packet.head.user_id,
            context.last_login_rand,
            proto::PlayerStatusRedisData {
                status: status.into(),
                gameserver_id: app_id,
                login_rand: context.cur_login_rand,
                login_time: time_utils::get_now() as u32,
            },
        )
        .await;
}

pub async fn on_player_login_rsp(packet: Packet) {
    let Some(session) = gameserver_service().find_player_session(packet.head.user_id) else {
        tracing::warn!(
            "find_player_session uid: {} failed,or session is not connected",
            packet.head.user_id
        );
        return;
    };

    player_login_result(session, packet);
}

fn player_login_result(session: Arc<PlayerSessionProxy>, packet: Packet) {
    let service = gameserver_service();

    let Ok(rsp) = packet.get_proto::<PlayerLoginRsp>() else {
        tracing::error!("get_proto failed");
        return;
    };

    if service.player_mgr.find_online_player(session.uid).is_some() {
        tracing::error!("uid: {} player is still exist", session.uid);
        return;
    }

    let cur_login_rand = session.get_cur_login_rand();
    if cur_login_rand == rsp.login_rand {
        // asyncPlayerLoginInitCallback
        add_player_to_online_map(session.clone(), rsp);
    }
}

fn add_player_to_online_map(session: Arc<PlayerSessionProxy>, rsp: PlayerLoginRsp) {
    *session.exec_coroutine.write() = Some(Player::start(session.clone(), session.uid));

    gameserver_service()
        .player_mgr
        .add_to_online_map(session.uid, session.get_gateserver_session_id());

    session
        .exec_coroutine
        .read()
        .as_ref()
        .unwrap()
        .push_packet(Packet::new(rsp, PacketHead::default()));
}

fn is_from_same_client_session(session: &PlayerSessionProxy, packet: &Packet) -> bool {
    packet.head.sender_app_id == session.get_gateserver_id()
        && packet.head.user_session_id == session.get_gateserver_session_id()
}

struct ModifyPlayerStatusContext {
    pub last_login_rand: u64,
    pub cur_login_rand: u64,
}

pub async fn on_player_force_exit_req(packet: Packet) -> Result<(), Box<dyn Error>> {
    let Some(session) = gameserver_service().find_player_session(packet.head.user_id) else {
        tracing::warn!(
            "on_player_force_exit: player session not found, session id: {}, uid: {}",
            packet.head.user_session_id,
            packet.head.user_id
        );
        return Ok(());
    };

    let uid = packet.head.user_id;

    if let Some(coroutine) = session.exec_coroutine.read().as_ref() {
        coroutine.push_packet(Packet::new(
            PlayerLogoutReq {
                reason: PlayerLogoutReason::ClientReq.into(),
            },
            packet.head.clone(),
        ));
    } else {
        tracing::warn!("on_player_force_exit: coroutine not running, uid: {uid}");
    }

    gameserver_service().player_mgr.remove_from_online_map(uid);
    async_modify_player_status(
        &packet,
        PlayerStatusType::PlayerStatusOffline,
        ModifyPlayerStatusContext {
            cur_login_rand: 0,
            last_login_rand: session.get_cur_login_rand(),
        },
    )
    .await;

    gameserver_service().remove_player_session(uid);

    Ok(())
}
