// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{net::{UdpSocket, SocketAddr, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}, time::Instant, io::{Read, ErrorKind}};

use sysinfo::Networks;

use super::super::{ServerId, PROTOCOL_VERSION, DISCOVER_MAGIC_NUMBER, RESPONSE_MAGIC_NUMBER, SERVER_PORT, CLIENT_PORT};

use crate::net::{serp::vu30::{ReadVu30, ExtendVu30}, utils::{Ipv4Network, udp}};

pub struct Socket {
	inner: UdpSocket,
	start: Instant,
	next_time: f32,
	socket_addrs: Vec<SocketAddr>,
	recv_buf: Box<[u8]>,
}

const WAIT_TIME: f32 = 5.0;
const SEND_INTERVAL: f32 = 0.5;

impl Socket {
	pub fn build() -> Result<Socket, String> {
		const MAX_LEN: usize = RESPONSE_MAGIC_NUMBER.len() + 4 /* Largest protocol version */ + 8 + 2;

		let inner = udp::new(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), CLIENT_PORT)).or_else(|err| {
			// More helpful warning in the most likely reason why this failed
			let reason = if err.kind() == ErrorKind::AddrInUse {
				String::from(" because that port is already used (you might be scanning the LAN on multiple games at the same time)")
			} else {
				format!(": {err}")
			};

			log::warn!("failed binding LAN discovery to port {CLIENT_PORT}{reason}");
			log::warn!("this could mean you will get no results if a firewall is blocking incoming traffic but allowing UDP port {CLIENT_PORT}");
			udp::new(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0))
		}).map_err(|err| format!("cannot bind socket: {err}"))?;

		inner.set_broadcast(true).map_err(|err| format!("set_broadcast failed: {err}"))?;
		inner.set_nonblocking(true).map_err(|err| format!("set_nonblocking failed: {err}"))?;

		let mut socket = Socket { inner, start: Instant::now(), next_time: SEND_INTERVAL, socket_addrs: Vec::new(), recv_buf: vec![0; MAX_LEN + 1].into_boxed_slice() };
		socket.set_addrs();
		socket.send();
		Ok(socket)
	}

	pub fn reset(&mut self) {
		self.start = Instant::now();
		self.next_time = SEND_INTERVAL;
		self.set_addrs();
		self.send();
	}

	fn set_addrs(&mut self) {
		self.socket_addrs.clear();

		/*
		 * Adds the IPv4 limited broadcast address in case a router is configured
		 * to drop directed broadcasts, and the two nodes are on the same L2
		 * segment.
		 */
		self.socket_addrs.push(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::BROADCAST, SERVER_PORT)));

		let networks = Networks::new_with_refreshed_list();

		// IPv4 directed broadcast addresses of each IPv4 network
		for ip_net in Ipv4Network::networks(&networks) {
			self.socket_addrs.push(SocketAddr::V4(SocketAddrV4::new(ip_net.directed_broadcast(), SERVER_PORT)));
		}

		/*
		 * Many IPv6 link-local multicast addresses to all hosts.
		 *
		 * Unlike IPv4, IPv6 socket addresses also need flow information and a
		 * scope id. The flow information isn't too important and can be set to
		 * zero, but the scope id identifies the interface that should be used.
		 *
		 * Annoyingly, the `sysinfo` crate doesn't provide a way to get this scope
		 * id of an interface, but on Linux I worked out that there is one for
		 * each interface and the first id is 1.
		 */
		for scope_id in 0..=networks.len() as u32 {
			self.socket_addrs.push(SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1), SERVER_PORT, 0, scope_id)));
		}
	}

	pub fn update(&mut self) -> bool {
		let time = self.start.elapsed().as_secs_f32();

		if time >= self.next_time {
			self.send();
			self.next_time += SEND_INTERVAL;
			if self.next_time >= WAIT_TIME - 1.0 { self.next_time = f32::INFINITY; } // Unlikely to receive any responses
		}

		time < WAIT_TIME
	}

	fn send(&self) {
		log::debug!("Sending LAN discovery message");
		let mut msg = Vec::with_capacity(DISCOVER_MAGIC_NUMBER.len() + PROTOCOL_VERSION.len());
		msg.extend_from_slice(DISCOVER_MAGIC_NUMBER);
		msg.extend_vu30(PROTOCOL_VERSION);
		for socket_addr in &self.socket_addrs {
			/*
			 * These errors are normal as you can get them from attempting to send
			 * a message to ff02::1 on an invalid scope id. I don't have any
			 * problem with these as there isn't any way to derive the scope id
			 * from the interface name.
			 */
			if let Err(err) = self.inner.send_to(&msg, socket_addr) {
				log::debug!("cannot send to {socket_addr}: {err}");
			}
		}
	}

	pub fn receive(&mut self) -> Result<Option<(ServerId, SocketAddr)>, String> {
		match self.inner.recv_from(&mut self.recv_buf) {
			Ok((len, src_addr)) => match Self::decode_info(&self.recv_buf[..len]) {
				Ok((id, port)) => {
					let socket_addr = match src_addr {
						SocketAddr::V4(addr) => SocketAddr::V4(SocketAddrV4::new(*addr.ip(), port)),
						SocketAddr::V6(addr) => SocketAddr::V6(SocketAddrV6::new(*addr.ip(), port, 0, addr.scope_id())),
					};

					Ok(Some((id, socket_addr)))
				},
				Err(err) => {
					log::debug!("bad LAN discovery response from {src_addr}: {err}");
					Ok(None)
				},
			},
			Err(err) if err.kind() == ErrorKind::WouldBlock => Ok(None),
			Err(err) => Err(err.to_string()),
		}
	}

	fn decode_info(mut buf: &[u8]) -> Result<(ServerId, u16), String> {
		let mut magic = [0; RESPONSE_MAGIC_NUMBER.len()];
		buf.read_exact(&mut magic).map_err(|_| Self::too_few_bytes())?;
		if magic != RESPONSE_MAGIC_NUMBER {
			return Err(format!("magic number doesn't match, got {magic:?}"));
		}

		let version = buf.read_vu30().map_err(|()| Self::too_few_bytes())?;
		if version != PROTOCOL_VERSION {
			return Err(format!("protocol version doesn't match, expected {PROTOCOL_VERSION}, got {version}"));
		}

		let mut data = [0; 8 + 2];
		buf.read_exact(&mut data).map_err(|_| Self::too_few_bytes())?;
		if !buf.is_empty() {
			return Err(String::from("trailing data found in datagram"));
		}

		#[allow(clippy::host_endian_bytes)] // Only interested in equality between ids, not their value
		let id = ServerId(u64::from_ne_bytes(data[..8].try_into().unwrap()));
		let port = u16::from_le_bytes(data[8..].try_into().unwrap());
		Ok((id, port))
	}

	fn too_few_bytes() -> String { String::from("too few bytes") }
}
