#[cfg(target_os = "windows")]
use std::fs::File;
use std::{
    collections::{HashMap, HashSet},
    net::SocketAddr,
    path::{Path, PathBuf},
    sync::Arc,
};

use eyre::bail;
use lanspread_compat::eti::get_games;
use lanspread_db::db::{Game, GameDB, GameFileDescription};
use lanspread_peer::{
    ActiveOperation,
    ActiveOperationKind,
    InstallOperation,
    PeerCommand,
    PeerEvent,
    PeerGameDB,
    PeerRuntimeHandle,
    UnpackFuture,
    Unpacker,
    start_peer,
};
use tauri::{AppHandle, Emitter as _, Manager};
use tauri_plugin_shell::{ShellExt, process::Command};
use tokio::sync::{
    RwLock,
    mpsc::{UnboundedReceiver, UnboundedSender},
};

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/

/// Tauri-managed runtime state shared by commands and setup tasks.
#[derive(Default)]
struct LanSpreadState {
    peer_ctrl: Arc<RwLock<Option<UnboundedSender<PeerCommand>>>>,
    peer_runtime: Arc<RwLock<Option<PeerRuntimeHandle>>>,
    games: Arc<RwLock<GameDB>>,
    active_operations: Arc<RwLock<HashMap<String, UiOperationKind>>>,
    games_folder: Arc<RwLock<String>>,
    peer_game_db: Arc<RwLock<PeerGameDB>>,
    catalog: Arc<RwLock<HashSet<String>>>,
}

struct PeerEventTx(UnboundedSender<PeerEvent>);

#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
enum UiOperationKind {
    Downloading,
    Installing,
    Updating,
    Uninstalling,
}

#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
struct UiActiveOperation {
    id: String,
    operation: UiOperationKind,
}

#[derive(Clone, Debug, serde::Serialize)]
struct GamesListPayload {
    games: Vec<Game>,
    active_operations: Vec<UiActiveOperation>,
}

struct SidecarUnpacker {
    app_handle: AppHandle,
}

impl Unpacker for SidecarUnpacker {
    fn unpack<'a>(&'a self, archive: &'a Path, dest: &'a Path) -> UnpackFuture<'a> {
        Box::pin(async move {
            let sidecar = self.app_handle.shell().sidecar("unrar")?;
            do_unrar(sidecar, archive, dest).await
        })
    }
}

#[cfg(target_os = "windows")]
const FIRST_START_DONE_FILE: &str = ".softlan_first_start_done";

#[tauri::command]
async fn request_games(state: tauri::State<'_, LanSpreadState>) -> tauri::Result<()> {
    log::debug!("request_games");

    let peer_ctrl_arc = state.inner().peer_ctrl.clone();
    let peer_ctrl = peer_ctrl_arc.read().await.clone();

    if let Some(peer_ctrl) = peer_ctrl {
        if let Err(e) = peer_ctrl.send(PeerCommand::ListGames) {
            log::error!("Failed to send message to peer: {e:?}");
        }
    } else {
        log::warn!("Peer system not initialized yet");
    }

    Ok(())
}

#[tauri::command]
async fn install_game(id: String, state: tauri::State<'_, LanSpreadState>) -> tauri::Result<bool> {
    if state
        .inner()
        .active_operations
        .read()
        .await
        .contains_key(&id)
    {
        log::warn!("Game already has an active operation: {id}");
        return Ok(false);
    }

    let peer_ctrl_arc = state.inner().peer_ctrl.clone();
    let peer_ctrl = peer_ctrl_arc.read().await.clone();
    let games_folder = state.inner().games_folder.read().await.clone();
    let game_path = PathBuf::from(games_folder).join(&id);
    let downloaded = game_path.join("version.ini").is_file();
    let installed = local_install_is_present(&game_path);

    let handled = if let Some(peer_ctrl) = peer_ctrl {
        let command = if !downloaded {
            PeerCommand::GetGame(id)
        } else if !installed {
            PeerCommand::InstallGame { id }
        } else {
            log::info!("Game is already installed: {id}");
            return Ok(false);
        };

        if let Err(e) = peer_ctrl.send(command) {
            log::error!("Failed to send message to peer: {e:?}");
        }
        true
    } else {
        log::warn!("Peer system not initialized yet");
        false
    };

    Ok(handled)
}

#[tauri::command]
async fn update_game(id: String, state: tauri::State<'_, LanSpreadState>) -> tauri::Result<bool> {
    if state
        .inner()
        .active_operations
        .read()
        .await
        .contains_key(&id)
    {
        log::warn!("Game already has an active operation: {id}");
        return Ok(false);
    }

    log::info!("Starting update for game: {id}");
    let peer_ctrl_arc = state.inner().peer_ctrl.clone();
    let peer_ctrl = peer_ctrl_arc.read().await.clone();

    if let Some(peer_ctrl) = peer_ctrl {
        if let Err(e) = peer_ctrl.send(PeerCommand::GetGame(id)) {
            log::error!("Failed to send message to peer: {e:?}");
            return Ok(false);
        }
        Ok(true)
    } else {
        log::warn!("Peer system not initialized yet");
        Ok(false)
    }
}

#[tauri::command]
async fn uninstall_game(
    id: String,
    state: tauri::State<'_, LanSpreadState>,
) -> tauri::Result<bool> {
    if state
        .inner()
        .active_operations
        .read()
        .await
        .contains_key(&id)
    {
        log::warn!("Game already has an active operation: {id}");
        return Ok(false);
    }

    let peer_ctrl_arc = state.inner().peer_ctrl.clone();
    let peer_ctrl = peer_ctrl_arc.read().await.clone();
    if let Some(peer_ctrl) = peer_ctrl {
        if let Err(e) = peer_ctrl.send(PeerCommand::UninstallGame { id }) {
            log::error!("Failed to send message to peer: {e:?}");
            return Ok(false);
        }
        Ok(true)
    } else {
        log::warn!("Peer system not initialized yet");
        Ok(false)
    }
}

#[tauri::command]
async fn get_peer_count(state: tauri::State<'_, LanSpreadState>) -> tauri::Result<usize> {
    let peer_ctrl_arc = state.inner().peer_ctrl.clone();
    let peer_ctrl = peer_ctrl_arc.read().await.clone();

    if let Some(peer_ctrl) = peer_ctrl {
        // Send a request to get peer count
        if let Err(e) = peer_ctrl.send(PeerCommand::GetPeerCount) {
            log::error!("Failed to send GetPeerCount message to peer: {e:?}");
        }
    }

    // For now, we'll return 0 and rely on the event-based updates
    // The UI will get the actual count through peer-count-updated events
    Ok(0)
}

#[tauri::command]
async fn get_game_thumbnail(
    game_id: String,
    app_handle: tauri::AppHandle,
) -> tauri::Result<String> {
    use base64::Engine;

    let resource_path = app_handle.path().resolve(
        format!("assets/{game_id}.jpg"),
        tauri::path::BaseDirectory::Resource,
    )?;

    dbg!(&resource_path);

    let image_data = std::fs::read(&resource_path)?;
    let base64_data = base64::engine::general_purpose::STANDARD.encode(&image_data);
    Ok(format!("data:image/jpeg;base64,{base64_data}"))
}

#[cfg(target_os = "windows")]
fn run_as_admin(file: &str, params: &str, dir: &str) -> bool {
    use std::{ffi::OsStr, os::windows::ffi::OsStrExt};

    use windows::{Win32::UI::Shell::ShellExecuteW, core::PCWSTR};

    let file_wide: Vec<u16> = OsStr::new(file).encode_wide().chain(Some(0)).collect();
    let params_wide: Vec<u16> = OsStr::new(params).encode_wide().chain(Some(0)).collect();
    let dir_wide: Vec<u16> = OsStr::new(dir).encode_wide().chain(Some(0)).collect();
    let runas_wide: Vec<u16> = OsStr::new("runas").encode_wide().chain(Some(0)).collect();

    let result = unsafe {
        ShellExecuteW(
            None,
            PCWSTR::from_raw(runas_wide.as_ptr()),
            PCWSTR::from_raw(file_wide.as_ptr()),
            PCWSTR::from_raw(params_wide.as_ptr()),
            PCWSTR::from_raw(dir_wide.as_ptr()),
            windows::Win32::UI::WindowsAndMessaging::SW_HIDE,
        )
    };

    (result.0 as usize) > 32 // Success if greater than 32
}

#[cfg(target_os = "windows")]
async fn run_game_windows(
    id: String,
    state: tauri::State<'_, LanSpreadState>,
) -> tauri::Result<()> {
    let games_folder_lock = state.inner().games_folder.clone();
    let games_folder = {
        let guard = games_folder_lock.read().await;
        guard.clone()
    };

    let games_folder = PathBuf::from(games_folder);
    if !games_folder.exists() {
        log::error!("games_folder {} does not exist", games_folder.display());
        return Ok(());
    }

    let game_path = games_folder.join(id.clone());

    let game_setup_bin = game_path.join("game_setup.cmd");
    let game_start_bin = game_path.join("game_start.cmd");

    let first_start_done_file = game_path.join("local").join(FIRST_START_DONE_FILE);
    if !first_start_done_file.exists() && game_setup_bin.exists() {
        if !local_install_is_present(&game_path) {
            log::warn!(
                "local install is missing for {}; skipping game_setup",
                game_path.display()
            );
            return Ok(());
        }

        let result = run_as_admin(
            "cmd.exe",
            &format!(
                r#"/c "{} local {} de playername""#,
                game_setup_bin.display(),
                &id
            ),
            &game_path.display().to_string(),
        );

        if !result {
            log::error!("failed to run game_setup.cmd");
            return Ok(());
        }

        if let Err(e) = File::create(&first_start_done_file) {
            log::error!(
                "failed to create first-start marker {}: {e}",
                first_start_done_file.display()
            );
        }
    }

    if game_start_bin.exists() {
        let result = run_as_admin(
            "cmd.exe",
            &format!(
                r#"/c "{} local {} de playername""#,
                game_start_bin.display(),
                &id
            ),
            &game_path.display().to_string(),
        );

        if !result {
            log::error!("failed to run game_start.cmd");
        }
    }

    Ok(())
}

#[tauri::command]
async fn run_game(id: String, state: tauri::State<'_, LanSpreadState>) -> tauri::Result<()> {
    #[cfg(target_os = "windows")]
    {
        run_game_windows(id, state).await?;
    }

    #[cfg(not(target_os = "windows"))]
    {
        let _ = state;
        log::error!("run_game not implemented for this platform: id={id}");
    }

    Ok(())
}

fn eti_package_exists(game_path: &Path, game_id: &str) -> bool {
    game_path.is_dir() && game_path.join(format!("{game_id}.eti")).is_file()
}

fn local_install_is_present(game_path: &Path) -> bool {
    game_path.join("local").is_dir()
}

fn update_game_installation_state(game: &mut Game, games_root: &Path) {
    let game_path = games_root.join(&game.id);
    if !game_path.is_dir() {
        return;
    }

    let downloaded = game_path.join("version.ini").is_file();
    game.set_downloaded(downloaded);

    let installed = local_install_is_present(&game_path);
    game.installed = installed;

    // Size stays anchored to bundled game.db; skip expensive recalculation.

    if downloaded {
        match lanspread_db::db::read_version_from_ini(&game_path) {
            Ok(version) => {
                game.local_version = version;
                if let Some(ref version) = game.local_version {
                    log::debug!("Read local version for game {}: {}", game.id, version);
                }
            }
            Err(e) => {
                log::warn!("Failed to read local version.ini for game {}: {e}", game.id);
                game.local_version = None;
            }
        }
    } else {
        game.local_version = None;
    }

    if installed {
        log::debug!("Set {game} to installed");
    }

    if eti_package_exists(&game_path, &game.id) && !downloaded {
        log::debug!(
            "Game {} has archives but no version.ini sentinel; treating as not downloaded",
            game.id
        );
    }
}

/// Left in place for potential re-enablement. Currently not invoked to avoid expensive IO.
#[allow(dead_code)]
fn calculate_directory_size_sync(dir: &Path) -> eyre::Result<u64> {
    let mut total_size = 0u64;

    for entry in walkdir::WalkDir::new(dir) {
        let entry = entry?;
        let path = entry.path();

        if path.is_file() {
            let metadata = std::fs::metadata(path)?;
            total_size += metadata.len();
        }
    }

    Ok(total_size)
}

/// Used for peer-majority calculations but currently disabled.
#[allow(dead_code)]
fn calculate_size_from_file_descriptions(
    file_descriptions: &[lanspread_db::db::GameFileDescription],
) -> u64 {
    file_descriptions
        .iter()
        .filter(|desc| !desc.is_dir)
        .map(|desc| desc.size)
        .sum()
}

/// Future hook for reintroducing peer-driven size updates.
#[allow(dead_code)]
async fn update_game_sizes_from_peers(
    games: &mut std::collections::HashMap<String, Game>,
    peer_game_db: &Arc<RwLock<PeerGameDB>>,
) {
    log::debug!("Updating game sizes from peer data where local files are not available");

    let peer_db = peer_game_db.read().await;

    for game in games.values_mut() {
        if !game.downloaded && !game.installed {
            let peer_files_for_game = peer_db.aggregated_game_files(&game.id);

            if peer_files_for_game.is_empty() {
                if let Some(peer_size) = peer_db.majority_game_size(&game.id) {
                    if peer_size > 0 {
                        game.size = peer_size;
                        log::debug!(
                            "Updated size for game {} from peer totals: {} bytes",
                            game.id,
                            peer_size
                        );
                    } else {
                        log::debug!(
                            "Peer-reported size for game {} is 0; keeping previous value",
                            game.id
                        );
                    }
                } else {
                    log::debug!("No peer size data available for game {}", game.id);
                }
            } else {
                let peer_size = calculate_size_from_file_descriptions(&peer_files_for_game);

                if peer_size > 0 {
                    game.size = peer_size;
                    log::debug!(
                        "Updated size for game {} from peer files: {} bytes ({} files)",
                        game.id,
                        peer_size,
                        peer_files_for_game.len()
                    );
                } else {
                    log::debug!(
                        "Peer files for game {} exist but calculated size is 0",
                        game.id
                    );
                }
            }
        }
    }
}

async fn refresh_games_list(app_handle: &AppHandle) {
    let state = app_handle.state::<LanSpreadState>();

    let games_folder_lock = state.games_folder.clone();
    let games_db_lock = state.games.clone();

    let games_folder = games_folder_lock.read().await.clone();

    let path = if games_folder.is_empty() {
        log::debug!("Games folder not set; emitting current game list without rescan");
        None
    } else {
        Some(PathBuf::from(&games_folder))
    };

    let mut game_db = games_db_lock.write().await;

    if game_db.games.is_empty() {
        log::debug!("Game database empty during refresh; skipping emit");
        return;
    }

    if let Some(ref path) = path {
        if path.exists() {
            game_db.set_all_uninstalled();
            for game in game_db.games.values_mut() {
                update_game_installation_state(game, path);
            }
        } else {
            log::error!(
                "game dir {} does not exist; keeping last known installation state",
                path.display()
            );
        }
    }

    let games_to_emit = game_db
        .all_games()
        .into_iter()
        .cloned()
        .collect::<Vec<Game>>();

    drop(game_db);

    let active_operations = {
        let active_operations = state.active_operations.read().await;
        ui_active_operations_from_map(&active_operations)
    };

    let payload = GamesListPayload {
        games: games_to_emit,
        active_operations,
    };

    if let Err(e) = app_handle.emit("games-list-updated", Some(payload)) {
        log::error!("Failed to emit games-list-updated event: {e}");
    } else {
        log::info!("Emitted games-list-updated event");
    }
}

fn ui_active_operations_from_map(
    active_operations: &HashMap<String, UiOperationKind>,
) -> Vec<UiActiveOperation> {
    let mut snapshot = active_operations
        .iter()
        .map(|(id, operation)| UiActiveOperation {
            id: id.clone(),
            operation: *operation,
        })
        .collect::<Vec<_>>();
    snapshot.sort_by(|left, right| left.id.cmp(&right.id));
    snapshot
}

fn reconcile_active_operations(
    active_operations: &mut HashMap<String, UiOperationKind>,
    snapshot: &[ActiveOperation],
) {
    active_operations.clear();
    active_operations.extend(snapshot.iter().map(|operation| {
        (
            operation.id.clone(),
            ui_operation_from_peer(operation.operation),
        )
    }));
}

fn ui_operation_from_peer(operation: ActiveOperationKind) -> UiOperationKind {
    match operation {
        ActiveOperationKind::Downloading => UiOperationKind::Downloading,
        ActiveOperationKind::Installing => UiOperationKind::Installing,
        ActiveOperationKind::Updating => UiOperationKind::Updating,
        ActiveOperationKind::Uninstalling => UiOperationKind::Uninstalling,
    }
}

#[tauri::command]
async fn update_game_directory(app_handle: tauri::AppHandle, path: String) -> tauri::Result<()> {
    log::info!("update_game_directory: {path}");

    let games_folder = PathBuf::from(&path);
    if !games_folder.is_dir() {
        log::error!("game dir {} does not exist", games_folder.display());
        return Ok(());
    }

    let state = app_handle.state::<LanSpreadState>();
    let current_path = state.games_folder.read().await.clone();
    let active_ids = state
        .active_operations
        .read()
        .await
        .keys()
        .cloned()
        .collect::<Vec<_>>();
    if current_path != path && !active_ids.is_empty() {
        log::warn!(
            "Rejecting game directory change to {} while UI operations are active for: {}",
            games_folder.display(),
            active_ids.join(", ")
        );
        return Ok(());
    }

    *state.games_folder.write().await = path;

    ensure_bundled_game_db_loaded(&app_handle).await;
    refresh_games_list(&app_handle).await;
    ensure_peer_started(&app_handle, &games_folder).await;

    Ok(())
}

async fn update_game_db(games: Vec<Game>, app: AppHandle) {
    for game in &games {
        log::trace!("peer event ListGames iter: {game:?}");
    }

    let state = app.state::<LanSpreadState>();

    {
        let mut game_db = state.games.write().await;

        // Reset peer counts up front. Presence/metadata stay anchored to the baked game.db.
        for game in game_db.games.values_mut() {
            game.peer_count = 0;
        }

        for peer_game in games {
            if let Some(existing) = game_db.get_mut_game_by_id(&peer_game.id) {
                existing.peer_count = peer_game.peer_count;

                if let Some(peer_version) = &peer_game.eti_game_version {
                    match &existing.eti_game_version {
                        Some(current_version) if current_version >= peer_version => {}
                        _ => {
                            existing.eti_game_version = Some(peer_version.clone());
                            log::debug!(
                                "Updated eti_game_version for {} to {} based on peer data",
                                peer_game.id,
                                peer_version
                            );
                        }
                    }
                }
            } else {
                log::debug!(
                    "Peer advertised unknown game {id}; ignoring because game.db is ground truth",
                    id = peer_game.id
                );
            }
        }
    }

    refresh_games_list(&app).await;
}

async fn update_local_games_in_db(local_games: Vec<Game>, app: AppHandle) {
    let state = app.state::<LanSpreadState>();

    // Collect local game IDs first to avoid move issues
    let local_game_ids: HashSet<String> = local_games.iter().map(|g| g.id.clone()).collect();

    {
        let mut game_db = state.games.write().await;

        // Update installation status for games that exist locally
        for local_game in &local_games {
            if let Some(existing_game) = game_db.get_mut_game_by_id(&local_game.id) {
                existing_game.set_downloaded(local_game.downloaded);
                existing_game.installed = local_game.installed;
                existing_game
                    .local_version
                    .clone_from(&local_game.local_version);
                log::debug!("Updated local game status for: {}", local_game.id);
            }
        }

        // For games in the main DB that are not in the local list,
        // mark them as not downloaded/installed (they were deleted)
        for game in game_db.games.values_mut() {
            if !local_game_ids.contains(&game.id) && (game.downloaded || game.installed) {
                log::info!(
                    "Game {} no longer exists locally, marking as uninstalled",
                    game.id
                );
                game.set_downloaded(false);
                game.installed = false;
                game.local_version = None;
            }
        }
    }

    refresh_games_list(&app).await;
}

fn add_final_slash(path: &str) -> String {
    #[cfg(target_os = "windows")]
    const SLASH_CHAR: char = '\\';

    #[cfg(not(target_os = "windows"))]
    const SLASH_CHAR: char = '/';

    if path.ends_with(SLASH_CHAR) {
        path.to_string()
    } else {
        format!("{path}{SLASH_CHAR}")
    }
}

async fn do_unrar(sidecar: Command, rar_file: &Path, dest_dir: &Path) -> eyre::Result<()> {
    if let Ok(()) = std::fs::create_dir_all(dest_dir) {
        if let Ok(rar_file) = rar_file.canonicalize() {
            if let Ok(dest_dir) = dest_dir.canonicalize() {
                let dest_dir = dest_dir
                    .to_str()
                    .ok_or_else(|| eyre::eyre!("failed to get str of dest_dir"))?;

                log::info!(
                    "unrar game: {} to {}",
                    rar_file.canonicalize()?.display(),
                    dest_dir
                );

                let out = sidecar
                    .arg("x") // extract files
                    .arg(rar_file.canonicalize()?)
                    .arg("-y") // Assume Yes on all queries
                    .arg("-o") // Set overwrite mode
                    .arg(add_final_slash(dest_dir))
                    .output()
                    .await?;

                if !out.status.success() {
                    log::error!("unrar stderr: {}", String::from_utf8_lossy(&out.stderr));
                    bail!(
                        "unrar failed with status {:?}: {}",
                        out.status.code(),
                        String::from_utf8_lossy(&out.stderr)
                    );
                }

                return Ok(());
            }
            log::error!("dest_dir canonicalize failed: {}", dest_dir.display());
        } else {
            log::error!("rar_file canonicalize failed: {}", rar_file.display());
        }
    }

    bail!("failed to create directory: {dest_dir:?}");
}

/// Resolve the bundled catalog database packaged with the Tauri application.
fn resolve_bundled_game_db_path(app_handle: &AppHandle) -> PathBuf {
    app_handle
        .path()
        .resolve("game.db", tauri::path::BaseDirectory::Resource)
        .unwrap_or_else(|e| {
            log::error!("Failed to resolve game.db resource: {e}");
            panic!("game.db resource is required - cannot continue");
        })
}

/// Load the bundled catalog into the in-memory game database used by the UI.
async fn load_bundled_game_db(app_handle: &AppHandle) -> GameDB {
    let game_db_path = resolve_bundled_game_db_path(app_handle);
    let eti_games = get_games(&game_db_path).await.unwrap_or_else(|e| {
        log::error!("Failed to load ETI games: {e}");
        panic!("game.db resource is required - cannot continue");
    });

    log::info!("Loaded {} ETI games from game.db", eti_games.len());

    let games: Vec<Game> = eti_games.into_iter().map(Into::into).collect();
    GameDB::from(games)
}

async fn ensure_bundled_game_db_loaded(app_handle: &AppHandle) {
    let state = app_handle.state::<LanSpreadState>();
    let needs_load = { state.games.read().await.games.is_empty() };

    if needs_load {
        let game_db = load_bundled_game_db(app_handle).await;
        let catalog = game_db.games.keys().cloned().collect::<HashSet<_>>();
        *state.games.write().await = game_db;
        *state.catalog.write().await = catalog;
    }
}

async fn ensure_peer_started(app_handle: &AppHandle, games_folder: &Path) {
    let state = app_handle.state::<LanSpreadState>();
    let mut peer_ctrl = state.peer_ctrl.write().await;

    if let Some(peer_ctrl) = peer_ctrl.as_ref() {
        if let Err(e) = peer_ctrl.send(PeerCommand::SetGameDir(games_folder.to_path_buf())) {
            log::error!("Failed to send PeerCommand::SetGameDir: {e}");
        }
        return;
    }

    let tx_peer_event = app_handle.state::<PeerEventTx>().inner().0.clone();
    let unpacker = Arc::new(SidecarUnpacker {
        app_handle: app_handle.clone(),
    });
    match start_peer(
        games_folder.to_path_buf(),
        tx_peer_event,
        state.peer_game_db.clone(),
        unpacker,
        state.catalog.clone(),
    ) {
        Ok(handle) => {
            let sender = handle.sender();
            *peer_ctrl = Some(sender.clone());
            *state.peer_runtime.write().await = Some(handle);
            if let Err(e) = sender.send(PeerCommand::ListGames) {
                log::error!("Failed to send initial PeerCommand::ListGames: {e}");
            }
            log::info!("Peer system initialized successfully with games directory");
        }
        Err(e) => {
            log::error!("Failed to initialize peer system: {e}");
        }
    }
}

fn emit_game_id_event(app_handle: &AppHandle, event: &str, id: &str, label: &str) {
    if let Err(e) = app_handle.emit(event, Some(id.to_owned())) {
        log::error!("{label}: Failed to emit {event} event: {e}");
    }
}

fn emit_peer_addr_event(app_handle: &AppHandle, event: &str, addr: SocketAddr) {
    if let Err(e) = app_handle.emit(event, Some(addr.to_string())) {
        log::error!("Failed to emit {event} event: {e}");
    }
}

fn spawn_peer_event_loop(app_handle: AppHandle, mut rx_peer_event: UnboundedReceiver<PeerEvent>) {
    tauri::async_runtime::spawn(async move {
        while let Some(event) = rx_peer_event.recv().await {
            handle_peer_event(&app_handle, event).await;
        }
    });
}

#[allow(clippy::too_many_lines)]
async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) {
    match event {
        PeerEvent::ListGames(games) => {
            log::info!("PeerEvent::ListGames received");
            update_game_db(games, app_handle.clone()).await;
        }
        PeerEvent::LocalGamesUpdated {
            games: local_games,
            active_operations,
        } => {
            log::info!("PeerEvent::LocalGamesUpdated received");
            {
                let state = app_handle.state::<LanSpreadState>();
                let mut ui_active_operations = state.active_operations.write().await;
                reconcile_active_operations(&mut ui_active_operations, &active_operations);
            }
            update_local_games_in_db(local_games, app_handle.clone()).await;
        }
        PeerEvent::GotGameFiles {
            id,
            file_descriptions,
        } => {
            handle_got_game_files(app_handle, id, file_descriptions).await;
        }
        PeerEvent::NoPeersHaveGame { id } => {
            log::warn!("PeerEvent::NoPeersHaveGame received for {id}");
            emit_game_id_event(
                app_handle,
                "game-no-peers",
                &id,
                "PeerEvent::NoPeersHaveGame",
            );
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
        }
        PeerEvent::DownloadGameFilesBegin { id } => {
            log::info!("PeerEvent::DownloadGameFilesBegin received");
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .insert(id.clone(), UiOperationKind::Downloading);
            emit_game_id_event(
                app_handle,
                "game-download-begin",
                &id,
                "PeerEvent::DownloadGameFilesBegin",
            );
        }
        PeerEvent::DownloadGameFilesFinished { id } => {
            handle_download_finished(app_handle, id).await;
        }
        PeerEvent::DownloadGameFilesFailed { id } => {
            log::warn!("PeerEvent::DownloadGameFilesFailed received");
            emit_game_id_event(
                app_handle,
                "game-download-failed",
                &id,
                "PeerEvent::DownloadGameFilesFailed",
            );
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
        }
        PeerEvent::DownloadGameFilesAllPeersGone { id } => {
            log::warn!("PeerEvent::DownloadGameFilesAllPeersGone received for {id}");
            emit_game_id_event(
                app_handle,
                "game-download-peers-gone",
                &id,
                "PeerEvent::DownloadGameFilesAllPeersGone",
            );
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
        }
        PeerEvent::InstallGameBegin { id, operation } => {
            let operation_name: &'static str = (&operation).into();
            log::info!("PeerEvent::InstallGameBegin received for {id}: {operation_name}");
            let ui_operation = match operation {
                InstallOperation::Installing => UiOperationKind::Installing,
                InstallOperation::Updating => UiOperationKind::Updating,
            };
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .insert(id.clone(), ui_operation);
            emit_game_id_event(
                app_handle,
                "game-install-begin",
                &id,
                "PeerEvent::InstallGameBegin",
            );
        }
        PeerEvent::InstallGameFinished { id } => {
            log::info!("PeerEvent::InstallGameFinished received for {id}");
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
            emit_game_id_event(
                app_handle,
                "game-install-finished",
                &id,
                "PeerEvent::InstallGameFinished",
            );
        }
        PeerEvent::InstallGameFailed { id } => {
            log::warn!("PeerEvent::InstallGameFailed received for {id}");
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
            emit_game_id_event(
                app_handle,
                "game-install-failed",
                &id,
                "PeerEvent::InstallGameFailed",
            );
        }
        PeerEvent::UninstallGameBegin { id } => {
            log::info!("PeerEvent::UninstallGameBegin received for {id}");
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .insert(id.clone(), UiOperationKind::Uninstalling);
            emit_game_id_event(
                app_handle,
                "game-uninstall-begin",
                &id,
                "PeerEvent::UninstallGameBegin",
            );
        }
        PeerEvent::UninstallGameFinished { id } => {
            log::info!("PeerEvent::UninstallGameFinished received for {id}");
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
            emit_game_id_event(
                app_handle,
                "game-uninstall-finished",
                &id,
                "PeerEvent::UninstallGameFinished",
            );
        }
        PeerEvent::UninstallGameFailed { id } => {
            log::warn!("PeerEvent::UninstallGameFailed received for {id}");
            app_handle
                .state::<LanSpreadState>()
                .active_operations
                .write()
                .await
                .remove(&id);
            emit_game_id_event(
                app_handle,
                "game-uninstall-failed",
                &id,
                "PeerEvent::UninstallGameFailed",
            );
        }
        PeerEvent::PeerConnected(addr) => {
            log::info!("Peer connected: {addr}");
            emit_peer_addr_event(app_handle, "peer-connected", addr);
        }
        PeerEvent::PeerDisconnected(addr) => {
            log::info!("Peer disconnected: {addr}");
            emit_peer_addr_event(app_handle, "peer-disconnected", addr);
        }
        PeerEvent::PeerDiscovered(addr) => {
            log::info!("Peer discovered: {addr}");
            emit_peer_addr_event(app_handle, "peer-discovered", addr);
        }
        PeerEvent::PeerLost(addr) => {
            log::info!("Peer lost: {addr}");
            emit_peer_addr_event(app_handle, "peer-lost", addr);
        }
        PeerEvent::PeerCountUpdated(count) => {
            log::info!("Peer count updated: {count}");
            if let Err(e) = app_handle.emit("peer-count-updated", Some(count)) {
                log::error!("Failed to emit peer-count-updated event: {e}");
            }
        }
        PeerEvent::RuntimeFailed { component, error } => {
            let component_name: &'static str = (&component).into();
            log::error!("Peer runtime component {component_name} failed: {error}");
            if let Err(e) = app_handle.emit(
                "peer-runtime-failed",
                Some((component_name.to_string(), error)),
            ) {
                log::error!("Failed to emit peer-runtime-failed event: {e}");
            }
        }
    }
}

async fn handle_got_game_files(
    app_handle: &AppHandle,
    id: String,
    file_descriptions: Vec<GameFileDescription>,
) {
    log::info!("PeerEvent::GotGameFiles received");
    emit_game_id_event(
        app_handle,
        "game-download-pre",
        &id,
        "PeerEvent::GotGameFiles",
    );

    let state = app_handle.state::<LanSpreadState>();
    let peer_ctrl = state.peer_ctrl.read().await.clone();
    if let Some(peer_ctrl) = peer_ctrl
        && let Err(e) = peer_ctrl.send(PeerCommand::DownloadGameFiles {
            id,
            file_descriptions,
        })
    {
        log::error!("Failed to send PeerCommand::DownloadGameFiles: {e}");
    }
}

async fn handle_download_finished(app_handle: &AppHandle, id: String) {
    log::info!("PeerEvent::DownloadGameFilesFinished received");
    emit_game_id_event(
        app_handle,
        "game-download-finished",
        &id,
        "PeerEvent::DownloadGameFilesFinished",
    );

    app_handle
        .state::<LanSpreadState>()
        .active_operations
        .write()
        .await
        .remove(&id);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn active_operation_reconciliation_replaces_stale_ui_history() {
        let mut active_operations = HashMap::from([
            ("stale".to_string(), UiOperationKind::Installing),
            ("keep".to_string(), UiOperationKind::Downloading),
        ]);
        let snapshot = vec![
            ActiveOperation {
                id: "keep".to_string(),
                operation: ActiveOperationKind::Updating,
            },
            ActiveOperation {
                id: "new".to_string(),
                operation: ActiveOperationKind::Uninstalling,
            },
        ];

        reconcile_active_operations(&mut active_operations, &snapshot);

        assert_eq!(active_operations.len(), 2);
        assert_eq!(
            active_operations.get("keep"),
            Some(&UiOperationKind::Updating)
        );
        assert_eq!(
            active_operations.get("new"),
            Some(&UiOperationKind::Uninstalling)
        );
        assert!(
            !active_operations.contains_key("stale"),
            "snapshot reconciliation should drop stale UI operations"
        );
    }

    #[test]
    fn active_operation_payload_is_sorted_for_stable_ui_updates() {
        let active_operations = HashMap::from([
            ("zeta".to_string(), UiOperationKind::Downloading),
            ("alpha".to_string(), UiOperationKind::Installing),
        ]);

        let snapshot = ui_active_operations_from_map(&active_operations);

        assert_eq!(
            snapshot,
            vec![
                UiActiveOperation {
                    id: "alpha".to_string(),
                    operation: UiOperationKind::Installing,
                },
                UiActiveOperation {
                    id: "zeta".to_string(),
                    operation: UiOperationKind::Downloading,
                },
            ]
        );
    }
}

#[allow(clippy::missing_panics_doc)]
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let tauri_logger_builder = tauri_plugin_log::Builder::new()
        .clear_targets()
        .target(tauri_plugin_log::Target::new(
            tauri_plugin_log::TargetKind::Stdout,
        ))
        .level(log::LevelFilter::Info)
        .level_for("mdns_sd::service_daemon", log::LevelFilter::Off);

    // channel to receive events from the peer
    let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();

    tauri::Builder::default()
        .plugin(tauri_plugin_store::Builder::new().build())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_logger_builder.build())
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![
            request_games,
            install_game,
            run_game,
            update_game_directory,
            update_game,
            uninstall_game,
            get_peer_count,
            get_game_thumbnail
        ])
        .manage(LanSpreadState::default())
        .manage(PeerEventTx(tx_peer_event))
        .setup(move |app| {
            spawn_peer_event_loop(app.handle().clone(), rx_peer_event);
            Ok(())
        })
        .build(tauri::generate_context!())
        .expect("error while building tauri application")
        .run(|app_handle, event| {
            if matches!(event, tauri::RunEvent::Exit) {
                shutdown_peer_runtime(app_handle);
            }
        });
}

fn shutdown_peer_runtime(app_handle: &AppHandle) {
    let state = app_handle.state::<LanSpreadState>();
    let peer_runtime = state.peer_runtime.clone();

    tauri::async_runtime::block_on(async move {
        let Some(mut handle) = peer_runtime.write().await.take() else {
            return;
        };
        handle.shutdown();
        if tokio::time::timeout(std::time::Duration::from_secs(2), handle.wait_stopped())
            .await
            .is_err()
        {
            log::warn!("Peer runtime did not stop within 2s of shutdown request");
        }
    });
}
