use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};

use lanspread_db::db::GameFileDescription;
use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;

use super::{
    planning::{DownloadChunk, build_peer_plans, extract_version_descriptor, prepare_game_storage},
    retry::retry_failed_chunks,
    transport::download_from_peer,
    version_ini::{
        VersionIniBuffer,
        begin_version_ini_transaction,
        commit_version_ini_buffer,
        rollback_version_ini_transaction,
    },
};
use crate::{PeerEvent, config::MAX_RETRY_COUNT};

/// Downloads all game files from available peers.
#[allow(clippy::too_many_lines)]
pub async fn download_game_files(
    game_id: &str,
    game_file_descs: Vec<GameFileDescription>,
    games_folder: PathBuf,
    peers: Vec<SocketAddr>,
    file_peer_map: HashMap<String, Vec<SocketAddr>>,
    tx_notify_ui: UnboundedSender<PeerEvent>,
    cancel_token: CancellationToken,
) -> eyre::Result<()> {
    if peers.is_empty() {
        eyre::bail!("no peers available for game {game_id}");
    }

    if cancel_token.is_cancelled() {
        eyre::bail!("download cancelled for game {game_id}");
    }

    let (version_desc, transfer_descs) =
        extract_version_descriptor(game_id, game_file_descs, &tx_notify_ui)?;
    let version_buffer = match VersionIniBuffer::new(&version_desc) {
        Ok(buffer) => Arc::new(buffer),
        Err(err) => {
            tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
                id: game_id.to_string(),
            })?;
            return Err(err);
        }
    };
    let game_root = games_folder.join(game_id);

    if let Err(err) = begin_version_ini_transaction(&game_root).await {
        tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
            id: game_id.to_string(),
        })?;
        return Err(err);
    }
    if let Err(err) = prepare_game_storage(&games_folder, &transfer_descs).await {
        rollback_version_ini_transaction(&game_root).await;
        tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
            id: game_id.to_string(),
        })?;
        return Err(err);
    }
    if cancel_token.is_cancelled() {
        rollback_version_ini_transaction(&game_root).await;
        eyre::bail!("download cancelled for game {game_id}");
    }

    tx_notify_ui.send(PeerEvent::DownloadGameFilesBegin {
        id: game_id.to_string(),
    })?;

    let plans = build_peer_plans(&peers, &transfer_descs, &file_peer_map);

    let mut tasks = Vec::new();
    for (peer_addr, plan) in plans {
        let base_dir = games_folder.clone();
        let game_id = game_id.to_string();
        let cancel_token = cancel_token.clone();
        let version_buffer = version_buffer.clone();
        tasks.push(tokio::spawn(async move {
            download_from_peer(
                peer_addr,
                &game_id,
                plan,
                base_dir,
                &cancel_token,
                Some(version_buffer),
            )
            .await
        }));
    }

    let mut failed_chunks: Vec<DownloadChunk> = Vec::new();
    let mut last_err: Option<eyre::Report> = None;

    for handle in tasks {
        if cancel_token.is_cancelled() {
            rollback_version_ini_transaction(&game_root).await;
            eyre::bail!("download cancelled for game {game_id}");
        }

        match handle.await {
            Ok(Ok(results)) => {
                if cancel_token.is_cancelled() {
                    rollback_version_ini_transaction(&game_root).await;
                    eyre::bail!("download cancelled for game {game_id}");
                }

                for chunk_result in results {
                    if let Err(e) = chunk_result.result {
                        log::warn!(
                            "Failed to download chunk from {}: {e}",
                            chunk_result.peer_addr
                        );
                        if chunk_result.chunk.retry_count < MAX_RETRY_COUNT {
                            let mut retry_chunk = chunk_result.chunk;
                            retry_chunk.retry_count += 1;
                            retry_chunk.last_peer = Some(chunk_result.peer_addr);
                            failed_chunks.push(retry_chunk);
                        } else {
                            last_err = Some(eyre::eyre!(
                                "Max retries exceeded for chunk: {}",
                                chunk_result.chunk.relative_path
                            ));
                        }
                    }
                }
            }
            Ok(Err(_)) | Err(_) if cancel_token.is_cancelled() => {
                rollback_version_ini_transaction(&game_root).await;
                eyre::bail!("download cancelled for game {game_id}");
            }
            Ok(Err(e)) => last_err = Some(e),
            Err(e) => last_err = Some(eyre::eyre!("task join error: {e}")),
        }
    }

    // Retry failed chunks if any
    if !failed_chunks.is_empty() && !peers.is_empty() {
        if cancel_token.is_cancelled() {
            rollback_version_ini_transaction(&game_root).await;
            eyre::bail!("download cancelled for game {game_id}");
        }

        log::info!("Retrying {} failed chunks", failed_chunks.len());

        let retry_results = match retry_failed_chunks(
            failed_chunks,
            &peers,
            &games_folder,
            game_id,
            &file_peer_map,
            &cancel_token,
            Some(version_buffer.clone()),
        )
        .await
        {
            Ok(results) => results,
            Err(_) if cancel_token.is_cancelled() => {
                rollback_version_ini_transaction(&game_root).await;
                eyre::bail!("download cancelled for game {game_id}");
            }
            Err(err) => {
                last_err = Some(err);
                Vec::new()
            }
        };

        for chunk_result in retry_results {
            if cancel_token.is_cancelled() {
                rollback_version_ini_transaction(&game_root).await;
                eyre::bail!("download cancelled for game {game_id}");
            }

            if let Err(e) = chunk_result.result {
                log::error!("Retry failed for chunk: {e}");
                last_err = Some(e);
            }
        }
    }

    if cancel_token.is_cancelled() {
        rollback_version_ini_transaction(&game_root).await;
        eyre::bail!("download cancelled for game {game_id}");
    }

    if let Some(err) = last_err {
        rollback_version_ini_transaction(&game_root).await;
        tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
            id: game_id.to_string(),
        })?;
        return Err(err);
    }

    if let Err(err) = commit_version_ini_buffer(&game_root, &version_buffer).await {
        rollback_version_ini_transaction(&game_root).await;
        tx_notify_ui.send(PeerEvent::DownloadGameFilesFailed {
            id: game_id.to_string(),
        })?;
        return Err(err);
    }
    log::info!("all files downloaded for game: {game_id}");
    tx_notify_ui.send(PeerEvent::DownloadGameFilesFinished {
        id: game_id.to_string(),
    })?;
    Ok(())
}
