// Copyright 2023-2026 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

use std::{
    future::Future,
    pin::Pin,
    sync::Arc,
    task::{Context, Poll},
};

use digest_auth::{AuthContext, WwwAuthenticateHeader};
use hyper::{
    Request, Response, StatusCode, Uri,
    body::Incoming,
    header::{AUTHORIZATION, HeaderValue, USER_AGENT, WWW_AUTHENTICATE},
};
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
use hyper_util::{
    client::legacy::{
        Client as HyperClient,
        connect::{Connection, HttpConnector, proxy::Tunnel},
    },
    rt::TokioExecutor,
};
use log::debug;
use rustls::ClientConfig;
use tower::Layer as _;
use tower_http::set_header::{SetRequestHeader, SetRequestHeaderLayer};
use tower_service::Service;

use crate::{config::DEFAULT_TIMEOUT, timeout::TimeoutConnector};

/// Hyper client wrapped with optional-auth and user-agent layers.
pub type HttpClient<C> =
    SetRequestHeader<AuthMiddleware<HyperClient<TimeoutConnector<C>, String>>, HeaderValue>;

/// Build a new HTTPS connector.
pub fn https_connector(tls_config: ClientConfig) -> HttpsConnector<HttpConnector> {
    let mut http = HttpConnector::new();
    http.set_connect_timeout(Some(DEFAULT_TIMEOUT));
    http.enforce_http(false); // Don't enforce http URL scheme.
    HttpsConnectorBuilder::new()
        .with_tls_config(tls_config)
        .https_or_http()
        .enable_http1()
        .wrap_connector(http)
}

/// Build a new HTTPS connector via an HTTP proxy tunnel.
pub fn https_connector_proxy(
    tls_config: ClientConfig,
    proxy_uri: Uri,
) -> HttpsConnector<Tunnel<HttpConnector>> {
    let mut http = HttpConnector::new();
    http.set_connect_timeout(Some(DEFAULT_TIMEOUT));
    http.enforce_http(false); // Don't enforce http URL scheme.
    HttpsConnectorBuilder::new()
        .with_tls_config(tls_config)
        .https_or_http()
        .enable_http1()
        .wrap_connector(Tunnel::new(proxy_uri, http))
}

/// Build an HTTP client from a connector, applying optional auth and user-agent layers.
pub fn build_http_client<C>(connector: C, auth: Auth, user_agent: HeaderValue) -> HttpClient<C>
where
    C: Service<Uri> + Clone + Send + 'static,
    C::Response: hyper::rt::Read + hyper::rt::Write + Connection + Unpin + Send + 'static,
    C::Future: Send + Unpin + 'static,
    C::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send + 'static,
{
    let connector = TimeoutConnector::new(connector, DEFAULT_TIMEOUT);
    let raw = HyperClient::builder(TokioExecutor::new()).build(connector);
    let with_auth = AuthMiddleware::new(raw, auth);
    SetRequestHeaderLayer::overriding(USER_AGENT, user_agent).layer(with_auth)
}

/// Authentication configuration.
#[derive(Clone, Debug)]
pub enum Auth {
    /// No authentication.
    None,
    /// HTTP Basic authentication with pre-computed Authorization header.
    Basic(HeaderValue),
    /// HTTP Digest authentication with credentials.
    Digest {
        username: Arc<str>,
        password: Arc<str>,
    },
}

/// Error from authentication middleware.
#[derive(Debug, thiserror::Error)]
pub enum AuthError<E> {
    /// Error from the underlying HTTP client.
    #[error(transparent)]
    Inner(E),
    /// Error computing digest auth header.
    #[error(transparent)]
    Digest(digest_auth::Error),
}

/// Future for Basic auth: simply wraps the inner future.
pub struct BasicAuthFuture<S>
where
    S: Service<Request<String>, Response = Response<Incoming>>,
{
    future: Pin<Box<S::Future>>,
}

impl<S> Future for BasicAuthFuture<S>
where
    S: Service<Request<String>, Response = Response<Incoming>> + Unpin,
{
    type Output = Result<Response<Incoming>, AuthError<S::Error>>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.get_mut()
            .future
            .as_mut()
            .poll(cx)
            .map_err(AuthError::Inner)
    }
}

/// State machine for Digest authentication.
enum DigestAuthState<S>
where
    S: Service<Request<String>, Response = Response<Incoming>>,
{
    /// Waiting for pre-auth (first) response.
    First {
        inner: S,
        future: Pin<Box<S::Future>>,
        request: Box<Request<String>>,
        username: Arc<str>,
        password: Arc<str>,
    },
    /// Executing second request with Authorization header.
    Second { future: Pin<Box<S::Future>> },
    /// Future has completed.
    Done,
}

/// Future for Digest authentication.
pub struct DigestAuthFuture<S>
where
    S: Service<Request<String>, Response = Response<Incoming>>,
{
    state: DigestAuthState<S>,
}

impl<S> Future for DigestAuthFuture<S>
where
    S: Service<Request<String>, Response = Response<Incoming>> + Unpin,
{
    type Output = Result<Response<Incoming>, AuthError<S::Error>>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();

        loop {
            // Temporarily replace.
            let state = std::mem::replace(&mut this.state, DigestAuthState::Done);

            match state {
                DigestAuthState::First {
                    mut inner,
                    mut future,
                    mut request,
                    username,
                    password,
                } => match future.as_mut().poll(cx) {
                    Poll::Pending => {
                        this.state = DigestAuthState::First {
                            inner,
                            future,
                            request,
                            username,
                            password,
                        };
                        return Poll::Pending;
                    }
                    Poll::Ready(Ok(response)) => {
                        debug!("Digest: first response: {}", response.status());

                        if response.status() != StatusCode::UNAUTHORIZED {
                            return Poll::Ready(Ok(response));
                        }

                        let Some(header_value) = response.headers().get(WWW_AUTHENTICATE) else {
                            return Poll::Ready(Ok(response));
                        };

                        let Ok(header_str) = header_value.to_str() else {
                            return Poll::Ready(Err(AuthError::Digest(
                                digest_auth::Error::InvalidHeaderSyntax(
                                    "WWW-Authenticate contains non-ASCII bytes".into(),
                                ),
                            )));
                        };

                        let mut challenge = match WwwAuthenticateHeader::parse(header_str) {
                            Ok(c) => c,
                            Err(e) => return Poll::Ready(Err(AuthError::Digest(e))),
                        };

                        let context = AuthContext::new_with_method(
                            username.as_ref(),
                            password.as_ref(),
                            request.uri().path(),
                            None::<&[u8]>,
                            request.method().into(),
                        );

                        let auth_header = match challenge.respond(&context) {
                            Ok(h) => h,
                            Err(e) => return Poll::Ready(Err(AuthError::Digest(e))),
                        };

                        let Ok(mut value) = HeaderValue::try_from(auth_header.to_string()) else {
                            // Should not happen.
                            return Poll::Ready(Err(AuthError::Digest(
                                digest_auth::Error::InvalidHeaderSyntax(
                                    "internal error: generated invalid header value".into(),
                                ),
                            )));
                        };
                        value.set_sensitive(true);

                        request.headers_mut().insert(AUTHORIZATION, value);

                        this.state = DigestAuthState::Second {
                            future: Box::pin(inner.call(*request)),
                        };
                    }
                    Poll::Ready(Err(e)) => {
                        return Poll::Ready(Err(AuthError::Inner(e)));
                    }
                },
                DigestAuthState::Second { mut future } => match future.as_mut().poll(cx) {
                    Poll::Pending => {
                        this.state = DigestAuthState::Second { future };
                        return Poll::Pending;
                    }
                    Poll::Ready(Ok(response)) => {
                        debug!("Digest: second response: {}", response.status());
                        return Poll::Ready(Ok(response));
                    }
                    Poll::Ready(Err(e)) => {
                        return Poll::Ready(Err(AuthError::Inner(e)));
                    }
                },
                DigestAuthState::Done => panic!("DigestAuthFuture polled after completion"),
            }
        }
    }
}

/// Future returned by [`AuthMiddleware`].
pub enum AuthFuture<S>
where
    S: Service<Request<String>, Response = Response<Incoming>>,
{
    /// Basic authentication (or no auth).
    Basic(BasicAuthFuture<S>),
    /// Digest authentication.
    Digest(DigestAuthFuture<S>),
}

impl<S> Future for AuthFuture<S>
where
    S: Service<Request<String>, Response = Response<Incoming>> + Unpin,
{
    type Output = Result<Response<Incoming>, AuthError<S::Error>>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        match self.get_mut() {
            AuthFuture::Basic(f) => Pin::new(f).poll(cx),
            AuthFuture::Digest(f) => Pin::new(f).poll(cx),
        }
    }
}

/// Middleware that applies authentication headers to requests.
#[derive(Clone, Debug)]
pub struct AuthMiddleware<S> {
    inner: S,
    auth: Auth,
}

impl<S> AuthMiddleware<S> {
    fn new(inner: S, auth: Auth) -> Self {
        Self { inner, auth }
    }
}

impl<S> Service<Request<String>> for AuthMiddleware<S>
where
    S: Service<Request<String>, Response = Response<Incoming>>
        + Clone
        + Send
        + Sync
        + Unpin
        + 'static,
    S::Future: Send + Sync + 'static,
    S::Error: Send + 'static,
{
    type Response = Response<Incoming>;
    type Error = AuthError<S::Error>;
    type Future = AuthFuture<S>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx).map_err(AuthError::Inner)
    }

    fn call(&mut self, req: Request<String>) -> Self::Future {
        let mut inner = self.inner.clone();

        match &self.auth {
            Auth::None => {
                let future = inner.call(req);
                AuthFuture::Basic(BasicAuthFuture {
                    future: Box::pin(future),
                })
            }
            Auth::Basic(value) => {
                let mut req = req;
                req.headers_mut().insert(AUTHORIZATION, value.clone());
                let future = inner.call(req);
                AuthFuture::Basic(BasicAuthFuture {
                    future: Box::pin(future),
                })
            }
            Auth::Digest { username, password } => {
                let future = inner.call(req.clone());
                AuthFuture::Digest(DigestAuthFuture {
                    state: DigestAuthState::First {
                        inner,
                        future: Box::pin(future),
                        request: Box::new(req),
                        username: username.clone(),
                        password: password.clone(),
                    },
                })
            }
        }
    }
}
