use std::io::ErrorKind;
use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
use std::thread;
use std::time::{Duration, Instant};

use anyhow::{Context, Result, bail};

use crate::net::protocol::{
    ClientPacket, DEFAULT_SERVER, MAX_PACKET_SIZE, PROTOCOL_VERSION, PlayerSnapshot, ServerPacket,
    decode, encode,
};

pub struct ClientNet {
    socket: UdpSocket,
    server_addr: SocketAddr,
    pub player_id: u8,
    pub map_text: String,
    pub initial: PlayerSnapshot,
    seq: u32,
    ping_nonce: u32,
    ping_sent: Vec<(u32, Instant)>,
}

impl ClientNet {
    pub fn connect(server: &str, name: &str) -> Result<Self> {
        let server_addr = resolve_server(server)?;
        let bind_addr = if server_addr.is_ipv4() {
            "0.0.0.0:0"
        } else {
            "[::]:0"
        };
        let socket = UdpSocket::bind(bind_addr).context("failed to bind local UDP socket")?;
        socket
            .set_nonblocking(true)
            .context("failed to set UDP socket nonblocking")?;

        let hello = ClientPacket::ClientHello {
            protocol_version: PROTOCOL_VERSION,
            name: name.to_owned(),
        };
        let hello_bytes = encode(&hello)?;
        let deadline = Instant::now() + Duration::from_secs(5);
        let mut next_send = Instant::now();
        let mut buf = [0_u8; MAX_PACKET_SIZE];

        loop {
            let now = Instant::now();
            if now >= deadline {
                bail!("timed out waiting for server welcome from {server_addr}");
            }
            if now >= next_send {
                socket.send_to(&hello_bytes, server_addr)?;
                next_send = now + Duration::from_millis(250);
            }

            match socket.recv_from(&mut buf) {
                Ok((len, from)) if from == server_addr => {
                    let packet: ServerPacket = decode(&buf[..len])?;
                    if let ServerPacket::ServerWelcome {
                        accepted,
                        player_id,
                        map_text,
                        initial,
                        message,
                        ..
                    } = packet
                    {
                        if !accepted {
                            bail!("server rejected join: {message}");
                        }
                        let Some(initial) = initial else {
                            bail!("server welcome did not include initial player state");
                        };
                        return Ok(Self {
                            socket,
                            server_addr,
                            player_id,
                            map_text,
                            initial,
                            seq: 0,
                            ping_nonce: 1,
                            ping_sent: Vec::new(),
                        });
                    }
                }
                Ok(_) => {}
                Err(err) if err.kind() == ErrorKind::ConnectionReset => {
                    thread::sleep(Duration::from_millis(5));
                }
                Err(err) if err.kind() == ErrorKind::WouldBlock => {
                    thread::sleep(Duration::from_millis(5));
                }
                Err(err) => return Err(err).context("failed receiving welcome"),
            }
        }
    }

    pub fn server_addr(&self) -> SocketAddr {
        self.server_addr
    }

    pub fn send_input(
        &mut self,
        client_tick: u64,
        buttons: u16,
        turn: i8,
        fire_seq: u32,
    ) -> Result<()> {
        self.seq = self.seq.wrapping_add(1);
        let packet = ClientPacket::ClientInput {
            protocol_version: PROTOCOL_VERSION,
            seq: self.seq,
            client_tick,
            buttons,
            turn,
            fire_seq,
        };
        self.send(&packet)
    }

    pub fn send_ping(&mut self, client_time_ms: u64, now: Instant) -> Result<()> {
        let nonce = self.ping_nonce;
        self.ping_nonce = self.ping_nonce.wrapping_add(1).max(1);
        self.ping_sent.push((nonce, now));
        self.ping_sent
            .retain(|(_, sent)| now.duration_since(*sent) < Duration::from_secs(5));
        self.send(&ClientPacket::Ping {
            protocol_version: PROTOCOL_VERSION,
            nonce,
            client_time_ms,
        })
    }

    pub fn disconnect(&self) {
        if let Ok(bytes) = encode(&ClientPacket::Disconnect {
            protocol_version: PROTOCOL_VERSION,
        }) {
            let _ = self.socket.send_to(&bytes, self.server_addr);
        }
    }

    pub fn recv_packets(&mut self, now: Instant) -> Result<Vec<ClientEvent>> {
        let mut events = Vec::new();
        let mut buf = [0_u8; MAX_PACKET_SIZE];
        loop {
            match self.socket.recv_from(&mut buf) {
                Ok((len, from)) if from == self.server_addr => {
                    let packet: ServerPacket = decode(&buf[..len])?;
                    match packet {
                        ServerPacket::ServerSnapshot { players, .. } => {
                            events.push(ClientEvent::Snapshot { players })
                        }
                        ServerPacket::Pong { nonce, .. } => {
                            if let Some((_, sent)) = self
                                .ping_sent
                                .iter()
                                .find(|(sent_nonce, _)| *sent_nonce == nonce)
                            {
                                events.push(ClientEvent::Ping(now.duration_since(*sent)));
                            }
                            self.ping_sent
                                .retain(|(sent_nonce, _)| *sent_nonce != nonce);
                        }
                        ServerPacket::ServerWelcome { .. } => {}
                    }
                }
                Ok(_) => {}
                Err(err) if err.kind() == ErrorKind::ConnectionReset => break,
                Err(err) if err.kind() == ErrorKind::WouldBlock => break,
                Err(err) => return Err(err).context("failed receiving server packet"),
            }
        }
        Ok(events)
    }

    fn send(&self, packet: &ClientPacket) -> Result<()> {
        let bytes = encode(packet)?;
        self.socket
            .send_to(&bytes, self.server_addr)
            .context("failed sending UDP packet")?;
        Ok(())
    }
}

pub enum ClientEvent {
    Snapshot { players: Vec<PlayerSnapshot> },
    Ping(Duration),
}

fn resolve_server(server: &str) -> Result<SocketAddr> {
    let normalized = if server == "local" {
        DEFAULT_SERVER.to_owned()
    } else if server.contains(':') {
        server.to_owned()
    } else {
        format!("{server}:4000")
    };
    normalized
        .to_socket_addrs()
        .with_context(|| format!("failed to resolve server address {normalized}"))?
        .next()
        .with_context(|| format!("no address found for {normalized}"))
}
