// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{num::NonZeroUsize, fmt::Write, net::IpAddr, collections::BTreeMap};

use serde::{Serialize, Deserialize};

use crate::net::{serp::vu30::Vu30, utils::hostname::Hostname};

use crate::utils::max_len::MaxLenArcStr;
#[cfg(feature = "client")] use crate::utils::max_len::MaxLenString;

#[derive(Serialize, Deserialize)]
pub struct DiscoveryMessage(BTreeMap<Source, Vec<DiscoveryServer>>);

impl DiscoveryMessage {
	/**
	 * Just like all other requests, the client must have a limit to how much
	 * data they can accept to avoid memory exhaustion.
	 *
	 * I also need to make sure that the server doesn't accidentally produce
	 * discovery messages that serialise to be greater than this limit, which I
	 * am doing by limiting the number of game servers that are included.
	 *
	 * To avoid exceeding the limit, I make an educated guess for an upper bound
	 * of the number of bytes an individual game server can take up, and multiply
	 * this by the number of game servers I want to support.
	 */
	#[cfg(feature = "client")]
	pub const SIZE_LIMIT: usize = Self::GAME_SERVER_SIZE_UPPER_BOUND * Self::MAX_GAME_SERVERS;

	/**
	 * An upper bound to how large a game server can be when serialised.
	 *
	 * I'm not concerned about the exact limit because I don't need to, and
	 * calculating the exact limit by figuring out how `bincode` works is error
	 * prone and too much work.
	 *
	 * I'm much more worried about underestimating than overestimating, so this
	 * is a pretty conservative upper bound.
	 *
	 * The upper bound is calculated as:
	 * 	4 + 4 (request type and version) + 2 (port) +
	 * 	8 + 8 + 8 (player/spectator/max count) +
	 * 	64 + 64 + 512 (game id, name, description) +
	 * 	253 (hostname)
	 *
	 * This gets 927 which I'm rounding up to 1,000 which would include string
	 * lengths and the worst-case scenario for variable-length integers.
	 */
	#[cfg(feature = "client")]
	const GAME_SERVER_SIZE_UPPER_BOUND: usize = 1000;

	/**
	 * I don't see this game ever having more than 1,000 active servers.
	 */
	const MAX_GAME_SERVERS: usize = 1000;

	pub fn new(official: Vec<DiscoveryServer>, publications: BTreeMap<UnofficialSource, Vec<DiscoveryServer>>) -> DiscoveryMessage {
		let mut all_servers = BTreeMap::new();
		if Self::add_sources(&mut all_servers, official, publications).is_err() {
			log::warn!("discovery message truncated to {} game servers", Self::MAX_GAME_SERVERS);
		}
		DiscoveryMessage(all_servers)
	}

	fn add_sources(all_servers: &mut BTreeMap<Source, Vec<DiscoveryServer>>, official: Vec<DiscoveryServer>, publications: BTreeMap<UnofficialSource, Vec<DiscoveryServer>>) -> Result<(), ()> {
		let mut total_servers = 0;
		Self::add_source(all_servers, &mut total_servers, Source::Official, official)?;
		for (src, servers) in publications {
			Self::add_source(all_servers, &mut total_servers, Source::Unofficial(src), servers)?;
		}
		Ok(())
	}

	fn add_source(all_servers: &mut BTreeMap<Source, Vec<DiscoveryServer>>, total_servers: &mut usize, src: Source, mut servers: Vec<DiscoveryServer>) -> Result<(), ()> {
		let count = servers.len();
		servers.truncate(Self::MAX_GAME_SERVERS - *total_servers);
		let new_count = servers.len();

		all_servers.insert(src, servers);
		*total_servers += new_count;

		if new_count < count { Err(()) }
		else { Ok(()) }
	}

	#[cfg(feature = "client")]
	pub fn into_official_servers(mut self) -> Vec<DiscoveryServer> {
		self.0.remove(&Source::Official).unwrap_or_default()
	}

	#[cfg(feature = "client")]
	pub fn into_pairs(self) -> Vec<(Source, DiscoveryServer)> {
		self.0
			.into_iter()
			.flat_map(|(src, servers)| servers.into_iter().map(move |server| (src.clone(), server)))
			.collect()
	}
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Source {
	Official,
	Unofficial(UnofficialSource),
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum UnofficialSource {
	Ip(IpAddr),
	Hostname(Hostname),
}

const MAX_GAME_ID_LEN: usize = 64;
pub type GameId = MaxLenArcStr<MAX_GAME_ID_LEN>;
#[cfg(feature = "client")] pub type GameIdMut = MaxLenString<MAX_GAME_ID_LEN>;
pub type Name = MaxLenArcStr<64>;
pub type Description = MaxLenArcStr<512>;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscoveryServer {
	pub protocol_type: Vu30,
	pub protocol_version: Vu30,
	pub port: u16,
	pub player_count: usize,
	pub spectator_count: usize,
	pub max_clients: Option<NonZeroUsize>,
	pub game_id: GameId,
	pub name: Name,
	pub desc: Description,
}

impl DiscoveryServer {
	#[cfg(feature = "client")]
	pub fn format_players(&self) -> String {
		Self::format_players_with(self.player_count, self.spectator_count, self.max_clients)
	}

	pub fn format_players_with(players: usize, spectators: usize, max_clients: Option<NonZeroUsize>) -> String {
		let clients = players.saturating_add(spectators);

		let mut s = match (clients, max_clients) {
			(cur, Some(max)) => format!("{cur}/{max} players"),
			(1, None) => String::from("1 player"),
			(cur, None) => format!("{cur} players"),
		};

		if spectators > 0 {
			if players > 0 {
				let _ = write!(s, " ({players} playing, {spectators} spectating)");
			} else {
				s.push_str(" (all spectating)");
			}
		}

		s
	}
}
