mod database;
mod filters;
mod scrambler;

use anyhow::anyhow;
use clap::Parser;
use log::info;
use tokio;
use url::Url;
use warp::Filter;

use crate::scrambler::Scrambler;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
	#[command(subcommand)]
	action: Action,

	/// path prefix
	#[arg(short, long)]
	path_prefix: Option<String>,

	/// bind address
	#[arg(short, long, default_value = "http://0.0.0.0:3003")]
	bind_url: Url,

	/// database url
	#[arg(short, long)]
	database_url: Option<Url>,

	//TODO: make this deliverable via an env var and/or a config file
	/// AES-256 key for the state key scrambling, in hex format (64 hex characters). NEVER CHANGE IT ACROSS INVOCATIONS!
	#[arg(short, long)]
	scrambler_key: Option<String>,
}

#[derive(Parser, Debug)]
enum Action {
	Serve { services: Vec<String> },
}

#[tokio::main()]
async fn main() -> anyhow::Result<()> {
	env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).format_timestamp_millis().init();
	let args = Args::parse();

	match args.action {
		Action::Serve { services } => {
			let mut run_shortener = services.len() == 0;
			for service_arg in services {
				match service_arg.as_str() {
					"shortener" | "s" => {
						run_shortener = true;
					}
					_ => return Err(anyhow!("Unknown service {}", &service_arg)),
				}
			}
			if run_shortener {
				let aes256_key = args.scrambler_key.ok_or(anyhow!("--scrambler-key argument is mandatory when running the url shortener"))?;
				let aes256_key_bytes = hex::decode(&aes256_key).map_err(|err| anyhow!("Failed to decode AES-256 key as hex: {}", err))?;
				if aes256_key_bytes.len() != 32 {
					return Err(anyhow!("AES-256 key must be exactly 32 bytes (64 hex characters), got {} bytes", aes256_key_bytes.len()));
				}
				let scrambler = Scrambler::new(aes256_key_bytes.as_slice().try_into().unwrap());

				if args.bind_url.scheme() != "http" {
					return Err(anyhow!("The only supported bind address scheme is 'http'. Provided URL's scheme is '{}'", args.bind_url.scheme()));
				};
				if args.bind_url.cannot_be_a_base() {
					return Err(anyhow!("Provided bind URL is cannot-be-a-base, no."));
				};
				let bind_host = args.bind_url.host_str().ok_or_else(|| anyhow!("Missing bind host".to_string()))?.to_string();
				let bind_port = args.bind_url.port().ok_or_else(|| anyhow!("Missing bind port".to_string()))?;
				let tcp_listener = tokio::net::TcpListener::bind((bind_host.clone(), bind_port)).await.map_err(|err| anyhow!("Failed to bind: {}", err))?;
				let final_address = tcp_listener.local_addr().unwrap();
				let mut final_url = args.bind_url.clone();
				final_url.set_port(Some(final_address.port())).unwrap();
				final_url.set_ip_host(final_address.ip()).unwrap();
				if bind_port > 0 && bind_port != final_address.port() {
					panic!("bind bound port other than requested");
				}
				info!("Listening on {}", final_url.as_str());

				sqlx::any::install_default_drivers();
				let database_connection =
					database::DatabaseConnection::new(args.database_url.ok_or(anyhow!("--database-url argument is mandatory when running the url shortener"))?)
						.await?;

				let filters = match args.path_prefix {
					Some(prefix_path) => warp::path(prefix_path).and(filters::all(database_connection, "*".into(), scrambler)).boxed(),
					None => filters::all(database_connection, "*".into(), scrambler).boxed(),
				};

				//TODO maybe make CORS configurable?
				warp::serve(filters.with(warp::log("webapp_state_manager"))).incoming(tcp_listener).run().await;
			}
		}
	}

	Ok(())
}
