use crate::config::ServerConfig;
use bevy::prelude::*;
use lightyear::{
    connection::client::PeerMetadata,
    link::LinkConditioner,
    prelude::{
        server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
        *,
    },
};
use shared::{
    GameState, global_observer,
    heads_database::HeadsDatabase,
    protocol::{
        ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
    },
    tb_entities::SpawnPoint,
};
use std::{
    net::{IpAddr, Ipv4Addr, SocketAddr},
    time::Duration,
};

pub fn plugin(app: &mut App) {
    app.add_systems(
        OnEnter(GameState::Playing),
        (start_server, setup_timeout_timer),
    );
    app.add_systems(
        Update,
        (notify_started, run_timeout).run_if(in_state(GameState::Playing)),
    );

    global_observer!(app, handle_new_client);
    global_observer!(app, on_client_connected);
    global_observer!(app, on_client_playing);
    global_observer!(app, close_on_disconnect);
    global_observer!(app, cancel_timeout);
}

#[derive(Component)]
struct ClientPlayerId(u8);

fn handle_new_client(
    trigger: Trigger<OnAdd, Linked>,
    mut commands: Commands,
    id: Query<&PeerAddr>,
) -> Result {
    let Ok(id) = id.get(trigger.target()) else {
        return Ok(());
    };

    info!("Client connected on IP: {}", id.ip());

    let conditioner = LinkConditioner::new(LinkConditionerConfig {
        incoming_latency: Duration::from_millis(10),
        incoming_jitter: Duration::from_millis(0),
        incoming_loss: 0.0,
    });

    commands.entity(trigger.target()).insert((
        ReplicationSender::default(),
        Link::new(Some(conditioner)),
        ClientPlayerId(0),
    ));

    Ok(())
}

fn on_client_connected(
    trigger: Trigger<OnAdd, ClientOf>,
    mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
) -> Result {
    // `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
    // send the client player id until now.
    let (id, mut sender) = assign_player.get_mut(trigger.target())?;
    sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
    Ok(())
}

fn on_client_playing(
    trigger: Trigger<RemoteTrigger<ClientEnteredPlaying>>,
    commands: Commands,
    query: Query<&Transform, With<SpawnPoint>>,
    heads_db: Res<HeadsDatabase>,
    peers: Res<PeerMetadata>,
) -> Result {
    let Some(&client) = peers.mapping.get(&trigger.from) else {
        return Ok(());
    };

    crate::player::spawn(commands, client, query, heads_db).ok_or("failed to spawn player")?;

    Ok(())
}

fn close_on_disconnect(
    _trigger: Trigger<OnRemove, Connected>,
    config: Res<ServerConfig>,
    mut writer: EventWriter<AppExit>,
) {
    if config.close_on_client_disconnect {
        info!("client disconnected, exiting");
        writer.write(AppExit::Success);
    }
}

fn start_server(mut commands: Commands) -> Result {
    let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);

    let conditioner = LinkConditioner::new(LinkConditionerConfig {
        incoming_latency: Duration::from_millis(10),
        incoming_jitter: Duration::from_millis(0),
        incoming_loss: 0.0,
    });
    let mut commands = commands.spawn((
        Name::from("Server"),
        LocalAddr(server_addr),
        ServerUdpIo::default(),
        NetcodeServer::new(NetcodeConfig::default()),
        Link::new(Some(conditioner)),
    ));
    commands.trigger(server::Start);

    Ok(())
}

fn notify_started(started: Query<&Started>, mut notified: Local<bool>) {
    if !*notified && !started.is_empty() {
        println!("hedz.server_started");
        *notified = true;
    }
}

#[derive(Resource)]
struct TimeoutTimer(f32);

fn setup_timeout_timer(mut commands: Commands, config: Res<ServerConfig>) {
    commands.insert_resource(TimeoutTimer(config.timeout));
}

fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>, time: Res<Time>) {
    timer.0 -= time.delta_secs();

    if timer.0 <= 0.0 {
        info!("client timed out, exiting");
        writer.write(AppExit::Success);
    }
}

fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
    info!("client connected, cancelling timeout");
    timer.0 = f32::INFINITY;
}
