mod ops;

use configinator::AdminCapabilities;
use flate2::read::ZlibDecoder;
use hashbrown::HashMap;

use axum::Extension;
use axum::Router;
use axum::extract::{ConnectInfo, State};
use axum::http::{HeaderName, Request, StatusCode};
use axum::http::{HeaderValue, Uri};
use axum::response::Redirect;
use axum::response::Response;
use axum::routing::{delete, get, post, put};
use axum_extra::extract::Host;
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::io::Read;
use std::net::SocketAddr;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::{fmt, process};
use tokio::net::TcpStream;
use tokio_rustls::LazyConfigAcceptor;
use tokio_rustls::StartHandshake;
use tokio_rustls::rustls::crypto::aws_lc_rs;
use tokio_rustls::server::TlsStream;
use tower::Layer;

use bytes::Bytes;
use cbwaw::Auth;
use gobetween::Integration;
use hyper::body::Incoming;
use hyper_util::rt::{TokioExecutor, TokioIo};
use ops::assets::get_asset_to_res;

use axum::http::header::CONTENT_TYPE;
use stewball::ArtifactKind;
use tokio::net::TcpListener;

use configinator::{ActionTrigger, AppConfig, HttpMethod};
use notatype::ContentObject;
use serde::{Deserialize, Serialize};
use startmeup::{Action, Engine};
use stencilr::Template;
use stewball::Storage;

use tower::ServiceBuilder;
use tower_http::classify::ServerErrorsFailureClass;
use tower_http::compression::CompressionLayer;
use tower_http::decompression::RequestDecompressionLayer;
use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
use tower_http::trace::TraceLayer;
use tower_service::Service;
use tracing::Span;
use uuid::Uuid;

use rustls_acme::caches::DirCache;
use rustls_acme::{AcmeConfig, is_tls_alpn_challenge};
use tokio::io::AsyncWriteExt;
use tokio_stream::StreamExt;

use std::error::Error;
use std::fmt::Display;
use tokio_rustls::rustls::server::Acceptor;
use tokio_rustls::{
    TlsAcceptor,
    rustls::ServerConfig,
    rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
};

struct LatencyDisplay(f64);
impl Display for LatencyDisplay {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut t = self.0;

        for unit in ["ns", "µs", "ms", "s"] {
            if t < 10.0 {
                return write!(f, "{t:.2}{unit}");
            } else if t < 100.0 {
                return write!(f, "{t:.1}{unit}");
            } else if t < 1000.0 {
                return write!(f, "{t:.0}{unit}");
            }
            t /= 1000.0;
        }
        write!(f, "{:.0}s", t * 1000.0)
    }
}

const REQUEST_ID_HEADER: &str = "request-id";
const REDACTED: &str = "REDACTED";

fn rustls_server_config(
    key: impl AsRef<Path>,
    cert: impl AsRef<Path>,
) -> Result<Arc<ServerConfig>, Box<dyn Error>> {
    let key = PrivateKeyDer::from_pem_file(key)?;

    let certs = CertificateDer::pem_file_iter(cert)?.flatten().collect();

    let mut config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, key)?;

    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

    Ok(Arc::new(config))
}

#[derive(Deserialize, Debug)]
struct ClientEvent {
    correlation: String,
    ts: String,
    lvl: String,
    version: String,
    msg: String,
    path: String,
    query: Option<String>,
    fragment: Option<String>,
    flags: Option<BTreeMap<String, String>>,
    fields: Option<BTreeMap<String, String>>,
}

#[derive(PartialEq)]
pub enum ProvisionMode {
    Localhost,
    Staging,
    Production,
}

pub enum SecurityMode<T: AsRef<Path>> {
    Insecure,
    Secure(T, ProvisionMode),
}

#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Asset(String, Vec<u8>);

pub struct App {
    pub config: Arc<AppConfig>,
    state: Arc<AppState>,
    pub service: Router,
    pub log_ips: bool,
    pub log_headers: bool,
}

#[derive(Clone)]
struct AppState {
    schema: String,
    config: Arc<AppConfig>,

    auth: Arc<Auth>,
    storage: Arc<Storage>,
    templates: Vec<Template>,
    actions: Arc<Vec<Action>>,
    template_route_map: HashMap<String, usize>,
    action_route_map: HashMap<String, usize>,
    registration_actions: Vec<u8>,

    error_template_idx: Option<u8>,
    mfa_totp_template_idx: Option<u8>,

    secure_cookies: bool,
}

impl App {
    #[allow(clippy::too_many_arguments, clippy::too_many_lines)]
    pub fn new(
        config: AppConfig,
        _capabilities: AdminCapabilities,
        auth: Arc<Auth>,
        storage: Arc<Storage>,
        secure_cookies: bool,
        log_headers: bool,
        log_ips: bool,
        server_span: Option<Span>,
    ) -> Result<App, Box<dyn Error>> {
        let mut registration_actions = vec![];

        let schema = serde_json::to_string(&config)?;

        let mut secrets = BTreeMap::new();

        if let Some(secret_configs) = &config.secrets {
            for secret in secret_configs {
                secrets.insert(secret.name.clone(), secret);
            }
        }

        let mut integrations = vec![];

        if let Some(mut integration_configs) = config.integrations.clone() {
            integration_configs.sort_by(|a, b| a.idx.cmp(&b.idx));

            for (i, config) in integration_configs.iter().enumerate() {
                if i != config.idx as usize {
                    tracing::error!("gap in action indexes");
                }

                if let Ok(integration) = Integration::new(config.clone(), storage.clone(), &secrets)
                {
                    integrations.push(integration);
                }
            }
        }

        let integrations = Arc::new(integrations);

        let engine = Engine::default();

        let mut actions = vec![];
        let mut templates = vec![];
        let mut template_route_map = HashMap::new();
        let mut action_route_map = HashMap::new();

        let mut model_map = BTreeMap::new();
        let mut content_map = BTreeMap::new();

        if let Some(models) = &config.models {
            for model in models {
                model_map.insert(model.name.clone(), model);
            }
        }

        if let Some(content) = &config.content {
            for content_def in &content.definitions {
                content_map.insert(content_def.name.clone(), content_def);
            }
        }

        if let Some(mut action_configs) = config.actions.clone() {
            action_configs.sort_by(|a, b| a.idx.cmp(&b.idx));

            for (i, action_config) in action_configs.iter().enumerate() {
                if i != action_config.idx as usize {
                    tracing::error!("gap in action indexes");
                }

                for trigger in &action_config.triggered_by {
                    match trigger {
                        ActionTrigger::Registration => {
                            registration_actions.push(action_config.idx);
                        }
                        ActionTrigger::Rest { route, method: _ }
                        | ActionTrigger::Form {
                            route,
                            method: _,
                            redirect: _,
                        } => {
                            action_route_map.insert(route.clone(), action_config.idx as usize);
                        }
                        _ => {}
                    }
                }

                let src = match storage.get_artifact(action_config.idx, ArtifactKind::Action) {
                    Ok(val) => Some(val),
                    Err(err) => {
                        tracing::warn!("action '{}' not stored - {err}", action_config.name);
                        None
                    }
                };

                if let Ok(action) = Action::new(
                    src,
                    engine.clone(),
                    action_config.clone(),
                    auth.clone(),
                    storage.clone(),
                    integrations.clone(),
                    &model_map,
                    &content_map,
                    &config.actions,
                ) {
                    actions.push(action);
                }
            }
        }

        let mut error_template_idx = None;
        let mut mfa_totp_template_idx = None;

        if let Some(mut template_configs) = config.templates.clone() {
            template_configs.sort_by(|a, b| a.idx.cmp(&b.idx));

            for (i, template_config) in template_configs.iter().enumerate() {
                if i != template_config.idx as usize {
                    tracing::error!("gap in template indexes");
                }

                template_route_map
                    .insert(template_config.route.clone(), template_config.idx as usize);

                let src = match storage.get_artifact(template_config.idx, ArtifactKind::Template) {
                    Ok(val) => Some(val),
                    Err(err) => {
                        tracing::warn!("template '{}' not stored - {err}", template_config.name);
                        None
                    }
                };

                match Template::new(
                    src,
                    template_config.clone(),
                    auth.clone(),
                    &model_map,
                    &content_map,
                    engine.clone(),
                    storage.clone(),
                ) {
                    Ok(template) => {
                        if let Some(auth) = &config.auth
                            && let Some(totp_template) = &auth.mfa.totp.template
                            && &template.config.name == totp_template
                        {
                            mfa_totp_template_idx = Some(template.idx);
                        }

                        if let Some(error) = &config.error
                            && template.config.name == error.template
                        {
                            error_template_idx = Some(template.idx);
                        }

                        templates.push(template);
                    }
                    Err(err) => tracing::error!("{err}"),
                }
            }
        }

        let config = Arc::new(config);

        let state = Arc::new(AppState {
            config: config.clone(),
            schema,
            auth,
            storage,
            templates,
            template_route_map,
            action_route_map,
            registration_actions,
            actions: Arc::new(actions),
            error_template_idx,
            mfa_totp_template_idx,
            secure_cookies,
        });

        let request_id_header = HeaderName::from_static(REQUEST_ID_HEADER);

        let mut router = Router::new();

        router = router
            .route(
                "/favicon.ico",
                get(|State(state): State<Arc<AppState>>| async {
                    get_asset_to_res(state, "favicon.ico".into())
                }),
            )
            // display the config of the service
            .route(
                "/events",
                post(|body: Bytes| async move {
                    let span = tracing::info_span!("client");

                    let mut deflater = ZlibDecoder::new(&body[..]);
                    let mut json = String::new();

                    if let Err(err) = deflater.read_to_string(&mut json) {
                        tracing::error!(%err);
                        return StatusCode::INTERNAL_SERVER_ERROR;
                    }

                    let events: Vec<ClientEvent> = match serde_json::from_str(&json) {
                        Ok(parsed) => parsed,
                        Err(err) => {
                            tracing::error!(%err);
                            return StatusCode::INTERNAL_SERVER_ERROR;
                        }
                    };

                    span.in_scope(|| {
                        for event in events {
                            let ClientEvent {
                                lvl,
                                ts,
                                version,
                                path,
                                query,
                                fragment,
                                correlation,
                                fields,
                                flags,
                                msg,
                            } = event;

                            let fields = fields.map(tracing::field::debug);

                            let flags = flags.map(tracing::field::debug);

                            match lvl.as_ref() {
                                "trace" => tracing::trace!(
                                    ts,
                                    version,
                                    path,
                                    query,
                                    fragment,
                                    correlation,
                                    fields,
                                    flags,
                                    "{msg}"
                                ),
                                "debug" => tracing::debug!(
                                    ts,
                                    version,
                                    path,
                                    query,
                                    fragment,
                                    correlation,
                                    fields,
                                    flags,
                                    "{msg}"
                                ),
                                "info" => tracing::info!(
                                    ts,
                                    version,
                                    path,
                                    query,
                                    fragment,
                                    correlation,
                                    fields,
                                    flags,
                                    "{msg}"
                                ),
                                "warn" => tracing::warn!(
                                    ts,
                                    version,
                                    path,
                                    query,
                                    fragment,
                                    correlation,
                                    fields,
                                    flags,
                                    "{msg}"
                                ),
                                _ => tracing::error!(
                                    ts,
                                    version,
                                    path,
                                    query,
                                    fragment,
                                    correlation,
                                    fields,
                                    flags,
                                    "{msg}"
                                ),
                            }
                        }
                    });

                    StatusCode::OK
                }),
            );

        if state.config.hide_schema != Some(true) {
            router = router.route(
                "/.ordinary/schema",
                get(|State(state): State<Arc<AppState>>| async move {
                    (
                        StatusCode::OK,
                        [("content-type", "application/json")],
                        state.schema.clone(),
                    )
                }),
            );
        }

        if state.config.auth.is_some() {
            router = router // token
                .route("/access", put(ops::token::fetch_access))
                // auth
                .route("/registration_start", put(ops::registration::start))
                .route("/registration_finish", put(ops::registration::finish))
                .route("/login_start", put(ops::login::start))
                .route("/login_finish", put(ops::login::finish))
                // for javascript-only
                .route("/register", put(ops::registration::hash_only))
                .route("/login", put(ops::login::hash_only))
                // reset password
                .route(
                    "/reset_password_login_start",
                    put(ops::password::reset_login_start),
                )
                .route(
                    "/reset_password_login_finish",
                    put(ops::password::reset_login_finish),
                )
                .route(
                    "/reset_password_registration_start",
                    put(ops::password::reset_registration_start),
                )
                .route(
                    "/reset_password_registration_finish",
                    put(ops::password::reset_registration_finish),
                )
                // forgot password
                .route("/forgot_password_start", put(ops::password::forgot_start))
                .route("/forgot_password_finish", put(ops::password::forgot_finish))
                // reset totp mfa
                .route("/reset_totp_mfa_start", put(ops::mfa::reset_totp_start))
                .route("/reset_totp_mfa_finish", put(ops::mfa::reset_totp_finish))
                // lost totp mfa
                .route("/lost_totp_mfa_start", put(ops::mfa::lost_totp_start))
                .route("/lost_totp_mfa_finish", put(ops::mfa::lost_totp_finish))
                // delete account
                .route("/delete_account_start", put(ops::account::delete_start))
                .route("/delete_account_finish", put(ops::account::delete_finish))
                // reset recovery codes
                .route(
                    "/reset_recovery_codes_start",
                    put(ops::recovery::reset_codes_start),
                )
                .route(
                    "/reset_recovery_codes_finish",
                    put(ops::recovery::reset_codes_finish),
                );

            if state.auth.config.cookies_enabled {
                router = router
                    .route("/register", post(ops::registration::form))
                    .route("/login", post(ops::login::form))
                    .route("/access", get(ops::token::redirect_access))
                    .route("/access", post(ops::token::fetch_access_cookies));
            }
        }

        if let Some(action_configs) = &state.config.actions {
            if !action_configs.is_empty() {
                // action calls
                router = router.route("/invoke", put(ops::actions::invoke));
            }

            for config in action_configs {
                for trigger in &config.triggered_by {
                    match trigger {
                        ActionTrigger::Form {
                            route,
                            method,
                            redirect: _,
                        } => match method {
                            HttpMethod::GET => {
                                router = router.route(route, get(ops::actions::form));
                            }
                            HttpMethod::PUT => {
                                router = router.route(route, put(ops::actions::form));
                            }
                            HttpMethod::POST => {
                                router = router.route(route, post(ops::actions::form));
                            }
                            HttpMethod::DELETE => {
                                router = router.route(route, delete(ops::actions::form));
                            }
                        },
                        ActionTrigger::Rest { route, method } => match method {
                            HttpMethod::GET => {
                                router = router.route(route, get(ops::actions::rest));
                            }
                            HttpMethod::PUT => {
                                router = router.route(route, put(ops::actions::rest));
                            }
                            HttpMethod::POST => {
                                router = router.route(route, post(ops::actions::rest));
                            }
                            HttpMethod::DELETE => {
                                router = router.route(route, delete(ops::actions::rest));
                            }
                        },
                        _ => {}
                    }
                }
            }
        }

        if let Some(template_configs) = &state.config.templates {
            if !template_configs.is_empty() {
                // bare-bones template data queries
                router = router.route("/query/{idx}/{*path}", get(ops::templates::query));
            }

            for config in template_configs {
                // templates
                router = router.route(&config.route, get(ops::templates::get));
            }
        }

        // assets
        router = router
            .route("/assets/{*path}", get(ops::assets::get))
            .fallback(|State(state): State<Arc<AppState>>| async move {
                if let Some(idx) = state.error_template_idx
                    && let Some(err_template) = &state.templates.get(idx as usize)
                {
                    // todo: base this on accept header

                    match err_template.render(
                        "/404".into(),
                        None,
                        Some(("Not found".into(), 404)),
                        None,
                        &None,
                    ) {
                        Ok((mime, res)) => {
                            return (StatusCode::NOT_FOUND, [(CONTENT_TYPE, mime)], res);
                        }
                        Err(err) => tracing::warn!("{err}"),
                    }
                }

                (
                    StatusCode::NOT_FOUND,
                    [(CONTENT_TYPE, "text/plain".into())],
                    Bytes::copy_from_slice(b"Not found"),
                )
            });

        let domain = Arc::new(config.domain.clone());

        let service = router
            .with_state(state.clone())
            .layer(
                ServiceBuilder::new()
                    .layer(RequestDecompressionLayer::new())
                    .layer(CompressionLayer::new()),
            )
            .layer(
                ServiceBuilder::new()
                    .layer(SetRequestIdLayer::new(
                        request_id_header.clone(),
                        MakeRequestUuid,
                    ))
                    .layer(
                        TraceLayer::new_for_http()
                            .make_span_with(move |req: &Request<_>| {
                                let request_id = req.headers().get(REQUEST_ID_HEADER);

                                if let Some(server_span) = &server_span {
                                    server_span.in_scope(|| match request_id {
                                        Some(rid) => {
                                            tracing::info_span!(
                                                "app",
                                                domain = *domain,
                                                id = match rid.to_str() {
                                                    Ok(s) => s.to_string(),
                                                    Err(_) => Uuid::new_v4().to_string(),
                                                },
                                            )
                                        }
                                        None => {
                                            tracing::info_span!(
                                                "app",
                                                domain = *domain,
                                                id = Uuid::new_v4().to_string(),
                                            )
                                        }
                                    })
                                } else {
                                    match request_id {
                                        Some(rid) => {
                                            tracing::info_span!(
                                                "app",
                                                domain = *domain,
                                                id = match rid.to_str() {
                                                    Ok(s) => s.to_string(),
                                                    Err(_) => Uuid::new_v4().to_string(),
                                                },
                                            )
                                        }
                                        None => {
                                            tracing::info_span!(
                                                "app",
                                                domain = *domain,
                                                id = Uuid::new_v4().to_string(),
                                            )
                                        }
                                    }
                                }
                            })
                            .on_request(move |req: &Request<_>, _: &Span| {
                                let headers = log_headers.then(|| {
                                    let mut log_headers = req.headers().clone();

                                    if log_headers.contains_key("authorization") {
                                        log_headers.insert(
                                            "authorization",
                                            HeaderValue::from_static(REDACTED),
                                        );
                                    }

                                    if log_headers.contains_key("proxy-authorization") {
                                        log_headers.insert(
                                            "proxy-authorization",
                                            HeaderValue::from_static(REDACTED),
                                        );
                                    }

                                    if log_headers.contains_key("cookie") {
                                        log_headers
                                            .insert("cookie", HeaderValue::from_static(REDACTED));
                                    }

                                    tracing::field::debug(log_headers)
                                });

                                let ip = log_ips.then(|| {
                                    req.extensions()
                                        .get::<ConnectInfo<SocketAddr>>()
                                        .map(|addr| tracing::field::display(addr.ip()))
                                });

                                tracing::info!(
                                    ip,
                                    version = ?req.version(),
                                    method = %req.method(),
                                    uri = %req.uri(),
                                    headers,
                                    "req"
                                );
                            })
                            .on_response(move |res: &Response<_>, latency: Duration, _: &Span| {
                                let headers = log_headers.then(|| {
                                    let mut log_headers = res.headers().clone();

                                    if log_headers.contains_key("set-cookie") {
                                        log_headers.insert(
                                            "set-cookie",
                                            HeaderValue::from_static(REDACTED),
                                        );
                                    }

                                    tracing::field::debug(log_headers)
                                });

                                let status = res.status().as_u16();
                                let latency = LatencyDisplay(latency.as_nanos() as f64);

                                if status >= 500 {
                                    tracing::error!(status, headers, %latency, "res");
                                } else if status >= 400 {
                                    tracing::warn!(status, headers, %latency, "res");
                                } else {
                                    tracing::info!(status, headers, %latency, "res");
                                }
                            })
                            .on_failure(
                                |error: ServerErrorsFailureClass, latency: Duration, _: &Span| {
                                    let latency = LatencyDisplay(latency.as_nanos() as f64);

                                    tracing::error!(
                                        err = %error,
                                        %latency,
                                        "fail"
                                    );
                                },
                            ),
                    )
                    .layer(PropagateRequestIdLayer::new(request_id_header)),
            );

        Ok(App {
            config,
            state,
            service,
            log_ips,
            log_headers,
        })
    }

    pub fn get_invite(
        &self,
        admin_domain: &str,
        admin_account: &str,
        custom_claims: Option<&[u8]>,
    ) -> Result<Bytes, Box<dyn Error>> {
        self.state
            .auth
            .admin_invite_get(admin_domain, admin_account, custom_claims)
    }

    pub fn list_accounts(&self) -> Result<Bytes, Box<dyn Error>> {
        self.state.auth.list_accounts()
    }

    pub fn list_items(
        &self,
        model_idx: u8,
        cursor: Option<&[u8]>,
    ) -> Result<Bytes, Box<dyn Error>> {
        self.state.storage.list_model(model_idx, cursor)
    }

    pub fn update_content(&self, objects: Vec<ContentObject>) -> Result<(), Box<dyn Error>> {
        self.state.storage.update_content(objects)?;
        Ok(())
    }

    pub fn put_asset(&self, path: &str, content: &[u8]) -> Result<(), Box<dyn Error>> {
        let asset = if let Some(ext) = Path::new(path).extension() {
            let mime = match ext.to_str() {
                Some("txt") => mime::TEXT_PLAIN_UTF_8,
                Some("xml") => mime::TEXT_XML,
                Some("html") => mime::TEXT_HTML_UTF_8,
                Some("css") => mime::TEXT_CSS_UTF_8,
                Some("csv") => mime::TEXT_CSV_UTF_8,
                Some("js") => mime::TEXT_JAVASCRIPT,
                Some("png") => mime::IMAGE_PNG,
                Some("gif") => mime::IMAGE_GIF,
                Some("svg") => mime::IMAGE_SVG,
                Some("jpg" | "jpeg") => mime::IMAGE_JPEG,
                Some("pdf") => mime::APPLICATION_PDF,
                Some("json") => mime::APPLICATION_JSON,
                Some("wasm") => mime::Mime::from_str("application/wasm")?,
                _ => return Err("unsupported file type".into()),
            };

            Asset(mime.as_ref().to_string(), content.into())
        } else {
            Asset(
                mime::APPLICATION_OCTET_STREAM.as_ref().to_string(),
                content.into(),
            )
        };

        let mut serializer = flexbuffers::FlexbufferSerializer::new();
        asset.serialize(&mut serializer)?;

        self.state
            .storage
            .put_asset(path, Bytes::copy_from_slice(serializer.view()))?;

        Ok(())
    }

    pub fn set_template(&self, pos: u8, bin: &[u8]) -> Result<String, Box<dyn Error>> {
        let span = tracing::info_span!(
            "template",
            nm = tracing::field::Empty,
            i = tracing::field::Empty
        );

        span.in_scope(|| {
            if let Some(template) = self.state.templates.get(pos as usize) {
                span.record("nm", &template.config.name);
                span.record("i", template.config.idx);

                template.set_wasm(bin)?;

                Ok(template.config.name.clone())
            } else {
                Err(format!("no template with idx: {pos}").into())
            }
        })
    }

    pub fn set_action(&self, pos: u8, bin: &[u8]) -> Result<String, Box<dyn Error>> {
        let span = tracing::info_span!(
            "action",
            nm = tracing::field::Empty,
            i = tracing::field::Empty
        );

        span.in_scope(|| {
            if let Some(action) = self.state.actions.get(pos as usize) {
                span.record("nm", &action.config.name);
                span.record("i", action.config.idx);

                action.set_wasm(bin)?;

                Ok(action.config.name.clone())
            } else {
                Err(format!("no action with idx: {pos}").into())
            }
        })
    }

    pub async fn handle_stream(
        span: &Span,
        tower_service: Router,
        addr: SocketAddr,
        stream: TokioIo<TlsStream<TcpStream>>,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        let hyper_service = hyper::service::service_fn(move |request: Request<Incoming>| {
            let mut tower_service = Extension(ConnectInfo(addr)).layer(tower_service.clone());
            tower_service.call(request)
        });

        match hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
            .serve_connection_with_upgrades(stream, hyper_service)
            .await
        {
            Ok(()) => span.in_scope(|| tracing::info!("shutdown")),
            Err(err) => span.in_scope(|| tracing::warn!(%err, "disconnected")),
        }

        Ok(())
    }

    pub async fn handle_handshake(
        tls_span: &Span,
        acme_span: &Span,
        start_handshake: StartHandshake<TcpStream>,
        tower_service: Router,
        addr: SocketAddr,
        challenge_rustls_config: Arc<ServerConfig>,
        default_rustls_config: Arc<ServerConfig>,
    ) -> Result<(), Box<dyn Error + Send + Sync>> {
        if is_tls_alpn_challenge(&start_handshake.client_hello()) {
            acme_span.in_scope(|| tracing::info!("TLS-ALPN-01 validation"));
            let mut tls = start_handshake.into_stream(challenge_rustls_config).await?;

            tls.shutdown().await?;
        } else {
            tls_span.in_scope(|| tracing::info!("connecting"));
            let as_stream = start_handshake.into_stream(default_rustls_config).await?;

            let stream = TokioIo::new(as_stream);

            Self::handle_stream(tls_span, tower_service, addr, stream).await?;
        }

        Ok(())
    }

    #[allow(clippy::too_many_lines)]
    pub async fn start<P>(
        &self,
        listener: TcpListener,
        redirect_listener: Option<TcpListener>,
        mode: SecurityMode<P>,
        configs: Option<(Arc<ServerConfig>, Arc<ServerConfig>)>,
    ) -> Result<(), Box<dyn Error>>
    where
        P: AsRef<Path>,
    {
        let span = tracing::info_span!("server", pid = process::id(), domain = &self.config.domain);
        let span_clone = span.clone();

        if let Some(listener) = redirect_listener {
            let domain = self.config.domain.clone();

            let log_ips = self.log_ips;
            let log_headers = self.log_headers;

            let request_id_header = HeaderName::from_static(REQUEST_ID_HEADER);

            let local_addr = listener.local_addr()?;

            tokio::spawn(async move {
                span_clone.in_scope(|| tracing::info!("listening on {}", local_addr));

                let service = get(
                    async move |uri: Uri, Host(host): Host| -> Result<Redirect, StatusCode> {
                        // todo: handle localhost redirecting
                        if host == domain {
                            Ok(Redirect::permanent(&format!(
                                "https://{domain}{}",
                                uri.path()
                            )))
                        } else {
                            Err(StatusCode::NOT_FOUND)
                        }
                    },
                )
                .layer::<_, Infallible>(
                    ServiceBuilder::new()
                        .layer(SetRequestIdLayer::new(
                            request_id_header.clone(),
                            MakeRequestUuid,
                        ))
                        .layer(
                            TraceLayer::new_for_http()
                                .make_span_with(move |req: &Request<_>| {
                                    let request_id = req.headers().get(REQUEST_ID_HEADER);

                                    span_clone.in_scope(|| match request_id {
                                        Some(rid) => {
                                            tracing::warn_span!(
                                                "redirect",
                                                id = match rid.to_str() {
                                                    Ok(s) => s.to_string(),
                                                    Err(_) => Uuid::new_v4().to_string(),
                                                },
                                            )
                                        }
                                        None => {
                                            tracing::warn_span!(
                                                "redirect",
                                                id = Uuid::new_v4().to_string(),
                                            )
                                        }
                                    })
                                })
                                .on_request(move |req: &Request<_>, _: &Span| {
                                    let headers = log_headers.then(|| {
                                        let mut log_headers = req.headers().clone();

                                        if log_headers.contains_key("authorization") {
                                            log_headers.insert(
                                                "authorization",
                                                HeaderValue::from_static(REDACTED),
                                            );
                                        }

                                        if log_headers.contains_key("proxy-authorization") {
                                            log_headers.insert(
                                                "proxy-authorization",
                                                HeaderValue::from_static(REDACTED),
                                            );
                                        }

                                        if log_headers.contains_key("cookie") {
                                            log_headers.insert(
                                                "cookie",
                                                HeaderValue::from_static(REDACTED),
                                            );
                                        }

                                        tracing::field::debug(log_headers)
                                    });

                                    let ip = log_ips.then(|| {
                                        req.extensions()
                                            .get::<ConnectInfo<SocketAddr>>()
                                            .map(|addr| tracing::field::display(addr.ip()))
                                    });

                                    tracing::warn!(
                                        ip,
                                        version = ?req.version(),
                                        method = %req.method(),
                                        uri = %req.uri(),
                                        headers,
                                        "req"
                                    );
                                })
                                .on_response(
                                    move |res: &Response<_>, latency: Duration, _: &Span| {
                                        let headers = log_headers.then(|| {
                                            let mut log_headers = res.headers().clone();

                                            if log_headers.contains_key("set-cookie") {
                                                log_headers.insert(
                                                    "set-cookie",
                                                    HeaderValue::from_static(REDACTED),
                                                );
                                            }

                                            tracing::field::debug(log_headers)
                                        });

                                        let status = res.status().as_u16();
                                        let latency = LatencyDisplay(latency.as_nanos() as f64);

                                        if status >= 500 {
                                            tracing::error!(status, headers, %latency, "res");
                                        } else if status >= 400 {
                                            tracing::warn!(status, headers, %latency, "res");
                                        } else {
                                            tracing::info!(status, headers, %latency, "res");
                                        }
                                    },
                                )
                                .on_failure(
                                    |error: ServerErrorsFailureClass,
                                     latency: Duration,
                                     _: &Span| {
                                        let latency = LatencyDisplay(latency.as_nanos() as f64);

                                        tracing::error!(
                                            err = %error,
                                            %latency,
                                            "fail"
                                        );
                                    },
                                ),
                        )
                        .layer(PropagateRequestIdLayer::new(request_id_header)),
                );

                if let Err(err) = axum::serve(
                    listener,
                    service.into_make_service_with_connect_info::<SocketAddr>(),
                )
                .await
                {
                    tracing::error!(%err, "server closed");
                }
            });
        }

        let local_addr = listener.local_addr()?;

        match mode {
            SecurityMode::Insecure => {
                span.in_scope(|| {
                    tracing::info!(provision = "none", "listening on {}", local_addr);
                });

                let service = self.service.clone();

                axum::serve(
                    listener,
                    service.into_make_service_with_connect_info::<SocketAddr>(),
                )
                .await?;

                Ok(())
            }
            SecurityMode::Secure(cert_dir_path, provision) => match provision {
                ProvisionMode::Localhost => {
                    let cert_dir_path = cert_dir_path.as_ref();

                    certifiably::generate_self_signed_localhost_certs(cert_dir_path)?;

                    let rustls_config = rustls_server_config(
                        cert_dir_path.join("key.pem"),
                        cert_dir_path.join("crt.pem"),
                    )?;

                    let tls_acceptor = TlsAcceptor::from(rustls_config);
                    span.in_scope(|| {
                        tracing::info!(provision = "localhost", "listening on {}", local_addr);
                    });

                    let log_ips = self.log_ips;

                    loop {
                        let tower_service = self.service.clone();
                        let tls_acceptor = tls_acceptor.clone();

                        let (cnx, addr) = listener.accept().await?;

                        let span = span.clone();

                        tokio::spawn(async move {
                            let Ok(stream) = tls_acceptor.accept(cnx).await else {
                                if log_ips {
                                    span.in_scope(|| {
                                        tracing::warn!(ip = %addr.ip(), port = addr.port(), "accept");
                                    });
                                } else {
                                    span.in_scope(|| {
                                        tracing::warn!("accept");
                                    });
                                }

                                return;
                            };

                            let stream = TokioIo::new(stream);

                            let ret = Self::handle_stream(&span, tower_service, addr, stream).await;

                            if let Err(err) = ret {
                                span.in_scope(|| {
                                    if log_ips {
                                        tracing::warn!(ip = %addr.ip(), port = addr.port(), %err, "stream");
                                    } else {
                                        tracing::warn!(%err, "stream");
                                    }
                                });
                            }
                        });
                    }
                }
                ProvisionMode::Staging | ProvisionMode::Production => {
                    let (state, (challenge_rustls_config, default_rustls_config)) =
                        if let Some(configs) = configs {
                            (None, configs)
                        } else {
                            let contacts = self
                                .config
                                .contacts
                                .iter()
                                .map(|c| format!("mailto:{c}"))
                                .collect::<Vec<String>>();

                            let cache_path = Path::new(cert_dir_path.as_ref()).join("cache");

                            let state = AcmeConfig::new([&self.config.domain])
                                .contact(contacts)
                                .cache_option(Some(DirCache::new(cache_path)))
                                .directory_lets_encrypt(provision == ProvisionMode::Production)
                                .state();

                            let challenge_rustls_config = state.challenge_rustls_config();
                            let mut default_rustls_config = ServerConfig::builder_with_provider(
                                Arc::new(aws_lc_rs::default_provider()),
                            )
                            .with_safe_default_protocol_versions()?
                            .with_no_client_auth()
                            .with_cert_resolver(state.resolver());

                            default_rustls_config.alpn_protocols =
                                vec![b"h2".to_vec(), b"http/1.1".to_vec()];

                            let default_rustls_config = Arc::new(default_rustls_config);

                            (
                                Some(state),
                                (challenge_rustls_config, default_rustls_config),
                            )
                        };

                    let provision_str = match provision {
                        ProvisionMode::Production => "production",
                        ProvisionMode::Staging => "staging",
                        _ => unreachable!(),
                    };

                    let acme_span = span.in_scope(|| {
                        tracing::info_span!(
                            "acme",
                            domain = &self.config.domain,
                            provision = provision_str
                        )
                    });

                    let acme_span_clone = acme_span.clone();
                    if let Some(mut state) = state {
                        tokio::spawn(async move {
                            loop {
                                match match state.next().await {
                                    Some(v) => v,
                                    None => break,
                                } {
                                    Ok(evt) => acme_span_clone.in_scope(|| tracing::info!(?evt)),
                                    Err(err) => acme_span_clone.in_scope(|| tracing::error!(?err)),
                                }
                            }
                        });
                    }

                    span.in_scope(|| {
                        tracing::info!(provision = provision_str, "listening on {}", local_addr);
                    });

                    let log_ips = self.log_ips;

                    loop {
                        let tower_service = self.service.clone();

                        let (cnx, addr) = listener.accept().await?;

                        let span = span.clone();

                        let challenge_rustls_config = challenge_rustls_config.clone();
                        let default_rustls_config = default_rustls_config.clone();
                        let acme_span = acme_span.clone();

                        tokio::spawn(async move {
                            let start_handshake = match LazyConfigAcceptor::new(
                                Acceptor::default(),
                                cnx,
                            )
                            .await
                            {
                                Ok(sh) => sh,
                                Err(err) => {
                                    if log_ips {
                                        span.in_scope(|| tracing::error!(%err, ip = %addr.ip(), port = addr.port(), "acceptor"));
                                    } else {
                                        span.in_scope(|| tracing::error!(%err, "acceptor"));
                                    }

                                    return;
                                }
                            };

                            if let Err(err) = Self::handle_handshake(
                                &span,
                                &acme_span,
                                start_handshake,
                                tower_service,
                                addr,
                                challenge_rustls_config,
                                default_rustls_config,
                            )
                            .await
                            {
                                span.in_scope(|| {
                                    if log_ips {
                                        tracing::error!(ip = %addr.ip(), port = addr.port(), %err, "handshake");
                                    } else {
                                        tracing::error!(%err, "handshake");
                                    }
                                });
                            }
                        });
                    }
                }
            },
        }
    }
}
