use std::collections::HashSet;
use std::future::Future;
use std::net::SocketAddr;
use std::ops::RangeInclusive;
use std::pin::Pin;
use std::process::ExitCode;
use std::sync::Arc;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::Service;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use rand::prelude::*;
use rand_pcg::Pcg64;
use rand_seeder::Seeder;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<ExitCode, Box<dyn std::error::Error + Send + Sync>> {
	let args: Vec<String> = std::env::args().collect();

	if args.len() != 2 {
		eprintln!("must supply exactly one arg for the seed");
		return Ok(ExitCode::FAILURE);
	}
	let seed = args.get(1).unwrap();
	eprintln!("seed: {}", seed);

	tracing_subscriber::fmt::init();

	let svc = RandomPageService::new(seed.as_str());

	let addr = SocketAddr::from(([127, 0, 0, 1], 3200));

	let listener = TcpListener::bind(addr).await?;

	loop {
		let (stream, _) = listener.accept().await?;

		let io = TokioIo::new(stream);

		let svc = svc.clone();

		tokio::task::spawn(async move {
			if let Err(err) = http1::Builder::new().serve_connection(io, svc).await {
				eprintln!("Error serving connection: {:?}", err);
			}
		});
	}
}

#[derive(Debug, Clone)]
struct RandomPageService {
	pub seed: String,
	pub dict: Arc<Vec<&'static str>>,
	pub dict_set: Arc<HashSet<&'static str>>,
}

impl RandomPageService {
	pub fn new(seed: &str) -> Self {
		let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH"));
		let dictionary_string: &'static str =
			std::str::from_utf8(dictionary_data).unwrap();
		Self {
			seed: seed.to_string(),
			dict: Arc::new(dictionary_string.split_whitespace().collect()),
			dict_set: Arc::new(dictionary_string.split_whitespace().collect()),
		}
	}
}

impl Service<Request<hyper::body::Incoming>> for RandomPageService {
	type Response = Response<Full<Bytes>>;
	type Error = hyper::Error;

	type Future =
		Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

	fn call(&self, req: Request<hyper::body::Incoming>) -> Self::Future {
		fn mk_response(
			s: String,
			status_code: StatusCode,
		) -> Result<Response<Full<Bytes>>, hyper::Error> {
			Ok(
				Response::builder()
					.status(status_code)
					.body(Full::new(Bytes::from(s)))
					.unwrap(),
			)
		}

		let path = req.uri().path();

		tracing::info!(path, "path");

		let path_segments = path.split('/').filter(|v| v.len() > 0);

		for segment in path_segments {
			if !self.dict_set.contains(segment) {
				return Box::pin(async {
					mk_response("not found".to_string(), StatusCode::NOT_FOUND)
				});
			}
		}

		let res_body = build_page(&self.seed, path, &self.dict);

		let res = mk_response(res_body, StatusCode::OK);

		Box::pin(async { res })
	}
}

const MAX_ROUTE_SEGMENTS: usize = 6;
const PARAGRAPH_WORDS: RangeInclusive<usize> = 10..=200;
const N_LINKS: RangeInclusive<usize> = 3..=10;

pub fn build_page(
	base_seed: &str,
	route: &str,
	dictionary: &[&'static str],
) -> String {
	let mut rng: Pcg64 =
		Seeder::from(format!("{}---{}", base_seed, route)).make_rng();
	let n_words = rng.gen_range(PARAGRAPH_WORDS);
	let n_links = rng.gen_range(N_LINKS);

	let random_paragraph = (0..n_words)
		.map(|_| random_word(&mut rng, dictionary))
		.collect::<Vec<&str>>()
		.join(" ");

	let random_links = (0..n_links)
		.map(|_| random_route_link(&mut rng, dictionary))
		.map(|link| format!("<p>{}</p>", link))
		.collect::<Vec<String>>()
		.join("\n");

	format!(
		r#"
			<html>
				<head>
				</head>
				<body>
					<p>{}</p>
					{}
				</body>
			</html>
		"#,
		random_paragraph, random_links,
	)
}

fn random_route_link(rng: &mut Pcg64, dictionary: &[&'static str]) -> String {
	let n_segments = rng.gen_range(1..=MAX_ROUTE_SEGMENTS);
	let random_route = (0..n_segments)
		.map(|_| random_word(rng, dictionary))
		.collect::<Vec<&str>>()
		.join("/");
	let label = random_word(rng, dictionary);
	format!("<a href=\"/{}\">{}</a>", random_route, label)
}

pub fn random_word(
	rng: &mut Pcg64,
	dictionary: &[&'static str],
) -> &'static str {
	let i = rng.gen_range(0..dictionary.len());
	dictionary[i]
}

pub fn build_dict() -> Vec<&'static str> {
	let dictionary_data = include_bytes!(env!("DICTIONARY_FILE_PATH"));
	let dictionary_string: &'static str =
		std::str::from_utf8(dictionary_data).unwrap();
	dictionary_string.split_whitespace().collect()
}