use avian3d::prelude::{
    Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderMassProperties,
    CollisionEventsEnabled, CollisionLayers, Sensor,
};
use bevy::{
    ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance, utils::synccell::SyncCell,
};
use bevy_trenchbroom::geometry::Brushes;
use lightyear::{
    link::{LinkConditioner, prelude::*},
    netcode::Key,
    prelude::{
        client::{Input, InputDelayConfig, NetcodeConfig},
        input::native::InputMarker,
        *,
    },
};
use nil::prelude::Mutex;
use shared::{
    GameState,
    control::ControlState,
    global_observer,
    player::Player,
    protocol::{
        ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping,
        channels::UnorderedReliableChannel, messages::DespawnTbMapEntity,
    },
    tb_entities::{Platform, PlatformTarget},
};
use std::{
    env::current_exe,
    fs::File,
    io::{BufRead, BufReader},
    net::{IpAddr, Ipv4Addr, SocketAddr},
    process::Stdio,
    sync::{LazyLock, mpsc},
    time::Duration,
};

/// Cache of server processes to be cleared at process exit
static SERVER_PROCESSES: LazyLock<Mutex<Vec<std::process::Child>>> = LazyLock::new(Mutex::default);

pub fn plugin(app: &mut App) {
    app.add_systems(OnEnter(GameState::Connecting), attempt_connection);
    app.add_systems(
        Update,
        parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
    );
    app.add_systems(Last, close_server_processes);
    app.add_systems(Update, despawn_absent_map_entities);

    global_observer!(app, on_connecting);
    global_observer!(app, on_connection_failed);
    global_observer!(app, on_connection_succeeded);
    global_observer!(app, temp_give_player_marker);
    global_observer!(app, connect_on_local_server_started);
    global_observer!(app, received_remote_map_entity);
}

fn close_server_processes(mut app_exit: EventReader<AppExit>) {
    if app_exit.read().next().is_some() {
        let mut lock = SERVER_PROCESSES.lock();
        for mut process in lock.drain(..) {
            if let Err(err) = process.wait() {
                error!("{err}");
            }
        }
    }
}

fn attempt_connection(mut commands: Commands) -> Result {
    let mut args = std::env::args();
    let client_port = loop {
        match args.next().as_deref() {
            Some("--port") => {
                break args.next().unwrap().parse::<u16>().unwrap();
            }
            Some(_) => (),
            None => break 25564,
        }
    };

    let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), client_port);
    let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);
    let auth = Authentication::Manual {
        server_addr,
        client_id: client_port as u64,
        private_key: Key::default(),
        protocol_id: 0,
    };
    let sync_config = SyncConfig {
        jitter_multiple: 5,
        jitter_margin: Duration::from_millis(15),
        ..default()
    };
    let conditioner = LinkConditioner::new(LinkConditionerConfig {
        incoming_latency: Duration::from_millis(10),
        incoming_jitter: Duration::from_millis(0),
        incoming_loss: 0.0,
    });
    commands
        .spawn((
            Name::from("Client"),
            Client::default(),
            Link::new(Some(conditioner)),
            LocalAddr(client_addr),
            PeerAddr(server_addr),
            ReplicationReceiver::default(),
            client::NetcodeClient::new(
                auth,
                NetcodeConfig {
                    client_timeout_secs: 1,
                    ..default()
                },
            )?,
            UdpIo::default(),
            InputTimeline(Timeline::from(Input::new(
                sync_config,
                InputDelayConfig::balanced(),
            ))),
        ))
        .trigger(Connect);

    Ok(())
}

fn on_connection_succeeded(
    _trigger: Trigger<OnAdd, Connected>,
    state: Res<State<GameState>>,
    mut change_state: ResMut<NextState<GameState>>,
    mut sender: Single<&mut TriggerSender<ClientEnteredPlaying>>,
) {
    if *state == GameState::Connecting {
        change_state.set(GameState::Playing);
        sender.trigger::<UnorderedReliableChannel>(ClientEnteredPlaying);
    }
}

/// A client starts `Disconnected`, so in order to tell if it *actually* failed to connect/disconnected
/// vs. simply having been created, we need some extra state.
#[derive(Component)]
struct ClientActive;

fn on_connecting(trigger: Trigger<OnAdd, Connecting>, mut commands: Commands) {
    commands.entity(trigger.target()).insert(ClientActive);
}

#[derive(Resource)]
struct LocalServerStdout(SyncCell<mpsc::Receiver<String>>);

fn on_connection_failed(
    trigger: Trigger<OnAdd, Disconnected>,
    disconnected: Query<&Disconnected>,
    mut commands: Commands,
    client_active: Query<&ClientActive>,
    mut opened_server: Local<bool>,
) -> Result {
    let disconnected = disconnected.get(trigger.target()).unwrap();
    if *opened_server {
        panic!(
            "failed to connect to local server: {:?}",
            disconnected.reason
        );
    }

    let client = trigger.target();
    if client_active.contains(client) {
        commands.entity(client).remove::<ClientActive>();

        // the server executable is assumed to be adjacent to the client executable
        let mut exe_path = current_exe().expect("failed to get path of client executable");
        exe_path.set_file_name("server");
        let server_log_file = File::create("server.log")?;
        let mut server_process = std::process::Command::new(exe_path)
            .args(["--timeout", "60", "--close-on-client-disconnect"])
            .env("NO_COLOR", "1")
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(server_log_file)
            .spawn()
            .expect("failed to start server");
        let server_stdout = server_process.stdout.take().unwrap();
        SERVER_PROCESSES.lock().push(server_process);

        let (tx, rx) = std::sync::mpsc::channel();

        let stdout = BufReader::new(server_stdout).lines();

        std::thread::spawn(move || {
            for line in stdout {
                match line {
                    Ok(line) => {
                        tx.send(line).unwrap();
                    }
                    Err(error) => {
                        error!("error reading local server stdout: `{error}`");
                    }
                }
            }
        });

        commands.insert_resource(LocalServerStdout(SyncCell::new(rx)));

        *opened_server = true;
    }

    Ok(())
}

#[derive(Event)]
struct LocalServerStarted;

fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalServerStdout>) {
    let stdout: &mut LocalServerStdout = &mut stdout;

    while let Ok(line) = stdout.0.get().try_recv() {
        if let "hedz.server_started" = &line[..] {
            commands.trigger(LocalServerStarted);
        } else {
            info!("SERVER: {line}");
        }
    }
}

fn connect_on_local_server_started(
    _trigger: Trigger<LocalServerStarted>,
    state: Res<State<GameState>>,
    mut commands: Commands,
    client: Single<Entity, With<Client>>,
) {
    if *state == GameState::Connecting {
        commands.entity(*client).trigger(Connect);
    }
}

fn temp_give_player_marker(trigger: Trigger<OnAdd, Player>, mut commands: Commands) {
    commands
        .entity(trigger.target())
        .insert(InputMarker::<ControlState>::default());
}

fn received_remote_map_entity(
    trigger: Trigger<OnAdd, TbMapEntityId>,
    world: &mut World,
    mut child_buffer: Local<Vec<Entity>>,
) {
    let serverside = trigger.target();

    if world.get::<Replicated>(serverside).is_none() {
        return;
    }

    let id = *world.get::<TbMapEntityId>(serverside).unwrap();

    let Some(clientside) = world.resource_mut::<TbMapEntityMapping>().0.remove(&id.id) else {
        warn!("received unknown MapEntity ID `{id:?}`");
        return;
    };

    // cannot just use `take` directly with a bundle because then any missing component would cause
    // the entire bundle to fail
    move_component::<Brushes>(world, clientside, serverside);
    move_component::<(
        Collider,
        ColliderAabb,
        ColliderDensity,
        ColliderMarker,
        ColliderMassProperties,
        CollisionLayers,
    )>(world, clientside, serverside);
    move_component::<CollisionEventsEnabled>(world, clientside, serverside);
    move_component::<Platform>(world, clientside, serverside);
    move_component::<PlatformTarget>(world, clientside, serverside);
    move_component::<SceneInstance>(world, clientside, serverside);
    move_component::<SceneRoot>(world, clientside, serverside);
    move_component::<Sensor>(world, clientside, serverside);

    if let Some(children) = world.get::<Children>(clientside) {
        child_buffer.extend(children.iter());
        for child in child_buffer.drain(..) {
            world.entity_mut(child).insert(ChildOf(serverside));
        }
    }

    world.entity_mut(clientside).despawn();
}

fn move_component<B: Bundle + BundleFromComponents>(world: &mut World, from: Entity, to: Entity) {
    let comp = world.entity_mut(from).take::<B>();
    if let Some(comp) = comp {
        world.entity_mut(to).insert(comp);
    }
}

fn despawn_absent_map_entities(
    mut commands: Commands,
    mut messages: Query<&mut MessageReceiver<DespawnTbMapEntity>>,
    mut map: ResMut<TbMapEntityMapping>,
) {
    for mut recv in messages.iter_mut() {
        for msg in recv.receive() {
            // the server may double-send DespawnTbMapEntity for a given ID, so ignore it if the entity
            // was already despawned.
            let Some(entity) = map.0.remove(&msg.0) else {
                continue;
            };

            commands.entity(entity).despawn();
        }
    }
}
