// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{path::Path, collections::BTreeMap, sync::Arc, fmt::Write as _};

use glium::Frame;
use egui::{ComboBox, CursorIcon, Ui, Color32, RichText, CollapsingHeader};
use tokio::{task::JoinHandle, fs};

use super::{common::{Common, CommonGs, ConnectionStarter}, discovery};

use crate::app::{App, config::{self, Config as GameConfig}, filesystem::Filesystem, final_tasks::FinalTasks};
use crate::net::{server::{PORT, init_gui::{self, GuiServerHandle}}, serp::request::PlayRequest};
use crate::protocol::discovery::GameId;
use crate::server::{multiplayer::config::Config, common::config as common_config};
use crate::app::gui::{GuiState, style::{UiExt, FontSize, H3_SIZE, H4_SIZE}, layout_job_helper::LayoutJobHelper};
use crate::utils::{task::Task, string_list};

struct ConfigEntry {
	config_res: Result<Config, String>,
	filename: Box<str>,
}

impl ConfigEntry {
	fn get_name(&self) -> Box<str> {
		match &self.config_res {
			Ok(config) if !config.discovery.name.is_empty() => format!("{} ({})", config.discovery.name, self.filename).into_boxed_str(),
			Ok(_) => self.filename.clone(),
			Err(_) => format!("{} (error)", self.filename).into_boxed_str(),
		}
	}
}

type ConfigMap = BTreeMap<Arc<Path>, ConfigEntry>;

struct Configs {
	map: ConfigMap,
	selected: Option<Arc<Path>>,
}

impl Configs {
	fn selected_text(&self) -> Option<&str> {
		self.selected.as_ref().map(|path| self.map[path].filename.as_ref())
	}
}

pub struct PsCreateLocal {
	common: Common,
	configs: Result<Option<Configs>, ()>,
	config_task: Task<ConfigMap>,
	create_server_task: Task<(GuiServerHandle, JoinHandle<()>)>,
	created: Option<GuiServerHandle>,
}

impl PsCreateLocal {
	pub fn new(app: &App) -> PsCreateLocal {
		PsCreateLocal {
			common: Common::new(),
			configs: Ok(None),
			config_task: Self::create_config_task(&app.fs),
			create_server_task: Task::new(),
			created: None,
		}
	}

	fn create_config_task(fs: &Filesystem) -> Task<ConfigMap> {
		let config_dir_paths = fs.server_config_dirs();
		Task::run(Self::load_configs(config_dir_paths))
	}

	async fn load_configs(config_dir_paths: Vec<Box<Path>>) -> Result<ConfigMap, String> {
		let mut configs = BTreeMap::new();

		for dir_path in config_dir_paths {
			let mut dir = fs::read_dir(&dir_path).await.map_err(|err| format!("failed opening config directory (path = \"{}\"): {err}", dir_path.display()))?;
			loop {
				match dir.next_entry().await {
					Ok(Some(entry)) => {
						let (path, Ok(filename)) = (entry.path(), entry.file_name().into_string().map(String::into_boxed_str)) else { continue; };
						if !path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("ron")) { continue; }

						let config_res = common_config::load_async(&path).await;

						if configs.insert(Arc::from(path.as_ref()), ConfigEntry { config_res, filename }).is_some() {
							return Err(format!("duplicate path (path = \"{}\") found when traversing directory, this shouldn't happen", path.display()));
						}
					},
					Ok(None) => break,
					Err(err) => return Err(format!("failed reading directory contents: {err}")),
				}
			}
		}

		Ok(configs)
	}

	fn creating_state(&mut self, ui: &mut Ui, fs: &Filesystem, final_tasks: &mut FinalTasks) {
		let not_creating = self.create_server_task.empty();

		if ui.b2_enabled("Refresh Servers", self.config_task.empty() && not_creating).clicked() {
			self.configs = Ok(None);
			self.config_task = Self::create_config_task(fs);
		}

		ui.add_space(30.0);

		if let Ok(configs) = &mut self.configs {
			ui.add_enabled_ui(configs.is_some() && not_creating, |ui| {
				ComboBox::new("create-local-config", "Config")
					.selected_text(configs.as_ref().and_then(Configs::selected_text).unwrap_or("Select a Config"))
					.show_ui(ui, |ui| {
						if let Some(configs) = configs {
							for (path, entry) in &configs.map {
								let response = ui.selectable_label(configs.selected.as_deref() == Some(path.as_ref()), entry.get_name().as_ref());
								if response.clicked() && entry.config_res.is_ok() {
									configs.selected = Some(Arc::clone(path));
								}

								if let Err(err) = &entry.config_res {
									response.on_hover_ui_at_pointer(|ui| _ = ui.label(format!("error: {err}")));
								}
							}
						}
					});
			});

			if let Some(Configs { map, selected: Some(path) }) = configs {
				let config = map[path].config_res.as_ref().unwrap();
				let discovery = &config.discovery;
				discovery::name_and_desc(ui, discovery.name.get(), discovery.desc.get(), true);

				if ui.b2_enabled("Create", not_creating).clicked() {
					self.create_server_task.set(init_gui::run(config.clone(), Arc::clone(path)));
				}

				match self.create_server_task.result() {
					Some(Ok((server, task))) => {
						self.created = Some(server);
						final_tasks.add(task);
					},
					Some(Err(err)) => self.common.set_error(err),
					None => (),
				}
			}
		}
	}

	fn created_state(server: &GuiServerHandle, ui: &mut Ui, config: &mut GameConfig, connection_starter: Option<ConnectionStarter>) -> bool {
		const SUCCESS_COLOUR: Color32 = Color32::from_rgb(0x7f, 0xff, 0x7f);
		const PARTIAL_SUCCESS_COLOUR: Color32 = Color32::from_rgb(0xff, 0xff, 0x7f);
		const DEFAULT_COLOUR: Color32 = Color32::WHITE; // Default label colour

		let job = LayoutJobHelper::new(H3_SIZE);
		let job = if server.lan_discovery {
			if server.port == PORT {
				job.add("Success! ", SUCCESS_COLOUR).add("Server has been successfully created!", DEFAULT_COLOUR)
			} else {
				job.add("Partial Success! ", PARTIAL_SUCCESS_COLOUR).add(&format!("Server created on non-default port of {}. If players need to enter the address, make sure they use this port.", server.port), DEFAULT_COLOUR)
			}
		} else {
			let mut msg = String::from("Server has been created");
			if server.port != PORT {
				let _ = write!(msg, " with non-default port, and");
			} else {
				let _ = write!(msg, ", but");
			}
			let _ = write!(msg, " failed creating LAN discovery server. Players need to manually enter the address");
			if server.port != PORT {
				let _ = write!(msg, ", and they must set the port to {}", server.port);
			}
			msg.push('.');
			job.add("Partial Success! ", PARTIAL_SUCCESS_COLOUR).add(&msg, DEFAULT_COLOUR)
		}.build();
		ui.label(job);

		ui.add_space(30.0);

		CollapsingHeader::new(RichText::new("If searching the LAN fails").font_size(H3_SIZE)).open(if server.lan_discovery { None } else { Some(true) }).show(ui, |ui| {
			let mut msg = String::from("To manually enter the server's address, from the title screen click \"Play\" and then click \"Enter Address\" under the \"Multiplayer\" section.\n\n");

			if !server.hosts.is_empty() {
				let _ = write!(msg, "Set the host to ");
				string_list::create(&mut msg, &server.hosts, "or");
				if server.hosts.len() >= 2 {
					let _ = write!(msg, ", whichever works,");
				}
				let _ = write!(msg, " and set the port to {}.\n\n", server.port);
			} else {
				let _ = write!(msg, "You don't seem to be connected to the network. Once you do get connected, set the host to your private IP address and the port to {}.\n\n", server.port);
			}

			let _ = write!(msg, "The game id can be anything.");
			ui.label(RichText::new(msg).font_size(H4_SIZE));
		});

		discovery::name_and_desc(ui, server.discovery.name.get(), server.discovery.desc.get(), true);

		ui.add_space(30.0);
		config::set_changed!(config, ui.checkbox(&mut config.gui.spectate, "Join as Spectator").changed());
		ui.add_space(15.0);

		if ui.b2_enabled(config.gui.play_button_text(), connection_starter.is_some()).clicked() && let Some(starter) = connection_starter {
			let request = PlayRequest::new(GameId::empty(), config.gui.spectate, config.get_style());
			starter.connect_local(request, server.stream_sender.clone());
		}

		ui.add_space(30.0);
		ui.b2("Stop Server").clicked()
	}
}

impl CommonGs for PsCreateLocal {
	fn common(&mut self) -> &mut Common { &mut self.common }

	fn loop_iter(&mut self, app: &mut App, frame: &mut Frame) -> bool {
		if let Some(res) = self.config_task.result() {
			self.configs = match res {
				Ok(map) => Ok(Some(Configs { map, selected: None })),
				Err(err) => {
					self.common.set_error(err);
					Err(())
				},
			};
		}

		let res = GuiState::update(&mut app.gui, "ps-create-local", &app.window, |ctx, ui| {
			ui.h1("Create Local Server").add();

			if let Some(created) = &self.created {
				let stop_server = Self::created_state(created, ui, &mut app.config.borrow_mut(), self.common.connection_starter());
				if stop_server {
					self.created = None;
				}
			} else {
				self.creating_state(ui, &app.fs, &mut app.final_tasks);
			}

			self.common.finish_update(ctx, ui, (self.config_task.running() || self.create_server_task.running()).then_some(CursorIcon::Wait));
		});
		self.common.after_gui_update(res);

		app.gui.render(&app.display, frame);

		self.config_task.running() || self.create_server_task.running()
	}
}
