use std::{net::SocketAddr, path::PathBuf, sync::Arc};

use clap::Parser;
use gm_quic::prelude::{handy::ToCertificate, *};
use http::{
    Uri,
    uri::{Authority, Parts},
};
use qevent::telemetry::handy::{LegacySeqLogger, NoopLogger};
use tokio::{
    fs,
    io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader},
};
use tracing_subscriber::prelude::*;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Options {
    #[arg(long, help = "Save the qlog to a dir", value_name = "PATH")]
    qlog: Option<PathBuf>,
    #[arg(
        long,
        short,
        value_delimiter = ',',
        default_value = "tests/keychain/localhost/ca.cert",
        help = "Certificates of CA who issues the server certificate"
    )]
    roots: Vec<PathBuf>,
    #[arg(long, help = "Skip verification of server certificate")]
    skip_verify: bool,
    #[arg(
        long,
        short,
        value_delimiter = ',',
        default_value = "quic",
        help = "ALPNs to use for the connection"
    )]
    alpns: Vec<Vec<u8>>,
    #[arg(
        long,
        default_value = "true",
        action = clap::ArgAction::Set,
        help = "Enable ANSI color output in logs"
    )]
    ansi: bool,
    #[arg(long, help = "Save the response to a dir", value_name = "PATH")]
    save: Option<PathBuf>,
    #[arg(
        value_delimiter = ',',
        default_value = "http://localhost:4433/",
        help = "Uri to request. If only one uri is present and path is not specified, enter process mode"
    )]
    uris: Vec<Uri>,
}

#[tokio::main]
async fn main() {
    let options = Options::parse();
    let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout());
    tracing_subscriber::registry()
        // .with(
        //     console_subscriber::ConsoleLayer::builder()
        //         .server_addr("127.0.0.1:6670".parse::<SocketAddr>().unwrap())
        //         .spawn(),
        // )
        .with(
            tracing_subscriber::fmt::layer()
                .with_writer(non_blocking)
                .with_ansi(options.ansi)
                .with_filter(
                    tracing_subscriber::EnvFilter::builder()
                        .with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
                        .from_env_lossy(),
                ),
        )
        .init();

    if let Err(error) = run(options).await {
        tracing::error!(?error);
        std::process::exit(1);
    }
}

type Error = Box<dyn std::error::Error + Send + Sync>;

async fn run(options: Options) -> Result<(), Error> {
    if options.uris.is_empty() {
        return Err("no uri specified".into());
    }

    let qlogger: Arc<dyn qevent::telemetry::Log + Send + Sync> = match options.qlog {
        Some(dir) => Arc::new(LegacySeqLogger::new(dir)),
        None => Arc::new(NoopLogger),
    };

    let client_builder = if options.skip_verify {
        tracing::warn!("Skip server verify");
        QuicClient::builder().without_verifier()
    } else {
        tracing::info!("Soad ca certs");
        let mut roots = rustls::RootCertStore::empty();
        roots.add_parsable_certificates(rustls_native_certs::load_native_certs().certs);
        roots
            .add_parsable_certificates(options.roots.iter().flat_map(|path| path.to_certificate()));
        QuicClient::builder().with_root_certificates(roots)
    };

    let client = client_builder
        .with_qlog(qlogger)
        .without_cert()
        .with_parameters(handy::client_parameters())
        .with_alpns(options.alpns)
        .enable_sslkeylog()
        .build();

    if options.uris.len() == 1 && options.uris[0].path() == "/" {
        return process(&client, &options.uris[0], options.save).await;
    } else {
        for uri in options.uris {
            download(&client, uri, options.save.as_ref()).await?;
        }
    }

    Ok(())
}

async fn process(client: &QuicClient, base_uri: &Uri, save: Option<PathBuf>) -> Result<(), Error> {
    let mut stdin = BufReader::new(io::stdin());
    eprintln!(
        "Enter interactive mode. Input content to request (e.g: Cargo.toml), input `exit` or `quit` to quit."
    );
    loop {
        let mut input = String::new();
        _ = stdin.read_line(&mut input).await?;

        let content = input.trim();
        if content.is_empty() {
            continue;
        }

        if content == "exit" || content == "quit" {
            return Ok(());
        }

        let mut uri_parts = Parts::default();
        uri_parts.scheme = base_uri.scheme().cloned();
        uri_parts.authority = base_uri.authority().cloned();
        uri_parts.path_and_query = Some(format!("/{content}").parse()?);
        download(client, Uri::from_parts(uri_parts)?, save.as_ref()).await?;
    }
}

async fn download(client: &QuicClient, uri: Uri, save: Option<&PathBuf>) -> Result<(), Error> {
    let (server_name, server_addrs) =
        lookup(uri.authority().ok_or("authority must be present in uri")?).await?;

    let file_path = uri.path().strip_prefix('/');
    let file_path = file_path.ok_or_else(|| format!("invalid path `{}`", uri.path()))?;

    let connection = client.connected_to(server_name, server_addrs)?;
    let (_sid, (mut response, mut request)) = connection
        .open_bi_stream()
        .await?
        .expect("very very hard to exhaust the available stream ids");
    request
        .write_all(format!("GET /{file_path}").as_bytes())
        .await?;
    request.shutdown().await?;

    match save.map(|dir| dir.join(file_path)) {
        Some(path) => io::copy(&mut response, &mut fs::File::create(path).await?).await?,
        None => io::copy(&mut response, &mut io::stdout()).await?,
    };

    _ = connection.close("Bye bye", 0);

    tracing::info!("Saved to file {file_path}");
    Ok(())
}

async fn lookup(auth: &Authority) -> Result<(&str, Vec<SocketAddr>), Error> {
    let mut addrs = tokio::net::lookup_host((auth.host(), auth.port_u16().unwrap_or(443)))
        .await?
        .collect::<Vec<_>>();
    // Sort addresses to ensure IPv6 addresses are preferred over IPv4.
    addrs.sort_by(|a, b| match (a, b) {
        (SocketAddr::V4(_), SocketAddr::V6(_)) => std::cmp::Ordering::Greater,
        (SocketAddr::V6(_), SocketAddr::V4(_)) => std::cmp::Ordering::Less,
        (a, b) => a.cmp(b),
    });
    tracing::info!("DNS lookup for {:?}: {:?}", auth.host(), addrs);
    Ok((auth.host(), addrs))
}
