//! Network utilities for QUIC connections and peer communication.

use std::{
    net::{IpAddr, SocketAddr},
    time::Duration,
};

use bytes::BytesMut;
use futures::{SinkExt, StreamExt};
use if_addrs::{IfAddr, Interface, get_if_addrs};
use lanspread_db::db::{Game, GameFileDescription};
use lanspread_proto::{Message, Request, Response};
use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits};
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};

use crate::config::CERT_PEM;

/// Establishes a QUIC connection to a peer.
pub async fn connect_to_peer(addr: SocketAddr) -> eyre::Result<Connection> {
    let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;

    let client = QuicClient::builder()
        .with_tls(CERT_PEM)?
        .with_io("0.0.0.0:0")?
        .with_limits(limits)?
        .start()?;

    let conn = Connect::new(addr).with_server_name("localhost");
    let conn = client.connect(conn).await?;
    Ok(conn)
}

/// Performs an initial ping check to verify peer is alive.
pub async fn initial_peer_alive_check(conn: &mut Connection) -> bool {
    let remote_addr = conn.remote_addr().ok();

    let stream = match conn.open_bidirectional_stream().await {
        Ok(stream) => stream,
        Err(e) => {
            log::error!("{remote_addr:?} failed to open stream: {e}");
            return false;
        }
    };

    let (rx, tx) = stream.split();
    let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new());
    let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());

    // send ping
    if let Err(e) = framed_tx.send(Request::Ping.encode()).await {
        log::error!("{remote_addr:?} failed to send ping to peer: {e}");
        return false;
    }
    let _ = framed_tx.close().await;

    // receive pong
    if let Some(Ok(response_bytes)) = framed_rx.next().await {
        let response = Response::decode(response_bytes.freeze());
        match response {
            Response::Pong => {
                log::trace!("{remote_addr:?} peer is alive");
                return true;
            }
            _ => {
                log::error!("{remote_addr:?} peer sent invalid response to ping: {response:?}");
            }
        }
    }

    false
}

/// Pings a peer to check if it's alive.
pub async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result<bool> {
    let mut conn = connect_to_peer(peer_addr).await?;
    let is_alive = initial_peer_alive_check(&mut conn).await;
    Ok(is_alive)
}

/// Fetches the list of games from a peer.
pub async fn fetch_games_from_peer(peer_addr: SocketAddr) -> eyre::Result<Vec<Game>> {
    let mut conn = connect_to_peer(peer_addr).await?;

    let stream = conn.open_bidirectional_stream().await?;
    let (rx, tx) = stream.split();
    let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new());
    let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());

    // Send ListGames request
    framed_tx.send(Request::ListGames.encode()).await?;
    let _ = framed_tx.close().await;

    // Receive response
    let mut data = BytesMut::new();
    while let Some(Ok(bytes)) = framed_rx.next().await {
        data.extend_from_slice(&bytes);
    }

    let response = Response::decode(data.freeze());
    if let Response::ListGames(games) = response {
        Ok(games)
    } else {
        log::warn!("Unexpected response from peer {peer_addr}: {response:?}");
        Ok(Vec::new())
    }
}

/// Announces local games to a peer.
pub async fn announce_games_to_peer(peer_addr: SocketAddr, games: Vec<Game>) -> eyre::Result<()> {
    let mut conn = connect_to_peer(peer_addr).await?;

    let stream = conn.open_bidirectional_stream().await?;
    let (_, tx) = stream.split();
    let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());

    // Send AnnounceGames request
    framed_tx
        .send(Request::AnnounceGames(games).encode())
        .await?;
    let _ = framed_tx.close().await;

    Ok(())
}

/// Requests game file details from a peer.
pub async fn request_game_details_from_peer(
    peer_addr: SocketAddr,
    game_id: &str,
) -> eyre::Result<(Vec<GameFileDescription>, Response)> {
    let mut conn = connect_to_peer(peer_addr).await?;

    let stream = conn.open_bidirectional_stream().await?;
    let (rx, tx) = stream.split();
    let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new());
    let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new());

    framed_tx
        .send(
            Request::GetGame {
                id: game_id.to_string(),
            }
            .encode(),
        )
        .await?;
    framed_tx.close().await?;

    let mut data = BytesMut::new();
    while let Some(Ok(bytes)) = framed_rx.next().await {
        data.extend_from_slice(&bytes);
    }

    let response = Response::decode(data.freeze());
    match &response {
        Response::GetGame {
            id,
            file_descriptions,
        } => {
            if id != game_id {
                eyre::bail!("peer {peer_addr} responded with mismatched game id {id}");
            }
            Ok((file_descriptions.clone(), response))
        }
        Response::GameNotFound(_) => {
            eyre::bail!("peer {peer_addr} does not have game {game_id}")
        }
        Response::InternalPeerError(error_msg) => {
            eyre::bail!("peer {peer_addr} reported internal error: {error_msg}")
        }
        _ => eyre::bail!("unexpected response from {peer_addr}: {response:?}"),
    }
}

// =============================================================================
// IP address selection for mDNS advertisement
// =============================================================================

/// Selects the best IP address to advertise via mDNS.
pub fn select_advertise_ip() -> eyre::Result<IpAddr> {
    let mut best_candidate: Option<(u8, IpAddr)> = None;
    let mut loopback_fallback = None;

    for interface in get_if_addrs()? {
        if interface.is_loopback() {
            loopback_fallback.get_or_insert(interface.ip());
            continue;
        }

        if let Some(candidate) = classify_interface(&interface)
            && best_candidate
                .as_ref()
                .is_none_or(|(rank, _)| candidate.0 < *rank)
        {
            best_candidate = Some(candidate);
        }
    }

    if let Some((_, ip)) = best_candidate {
        return Ok(ip);
    }

    if let Some(ip) = loopback_fallback {
        log::warn!(
            "No non-loopback interface suitable for mDNS advertisement; falling back to {ip}"
        );
        return Ok(ip);
    }

    eyre::bail!("No usable network interface found for mDNS advertisement");
}

/// Classifies a network interface for mDNS advertisement priority.
fn classify_interface(interface: &Interface) -> Option<(u8, IpAddr)> {
    match interface.addr {
        IfAddr::V4(ref v4) => {
            let ip = v4.ip;

            if ip.is_unspecified() || ip.is_link_local() {
                return None;
            }

            let mut rank = if ip.is_private() { 0 } else { 2 };

            if is_virtual_interface(&interface.name) {
                rank += 2;
            }

            Some((rank, IpAddr::V4(ip)))
        }
        IfAddr::V6(_) => None,
    }
}

/// Checks if an interface name suggests it's a virtual interface.
fn is_virtual_interface(name: &str) -> bool {
    const VIRTUAL_HINTS: &[&str] = &[
        "awdl",
        "br-",
        "bridge",
        "docker",
        "ham",
        "llw",
        "tap",
        "tailscale",
        "tun",
        "utun",
        "vbox",
        "veth",
        "virbr",
        "vmnet",
        "wg",
        "zt",
    ];

    let lower = name.to_ascii_lowercase();
    VIRTUAL_HINTS.iter().any(|hint| lower.contains(hint))
}
