use crate::RouterApi;
use async_trait::async_trait;
use axum::extract::rejection::{TypedHeaderRejection, TypedHeaderRejectionReason};
use axum::extract::{Extension, FromRequest, RequestParts, TypedHeader};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use axum_extra::extract::CookieJar;
use etwin_core::auth::{
  AuthContext, AuthScope, Credentials, EtwinOauthAccessTokenKey, EtwinOauthAccessTokenKeyParseError, SessionId,
  SessionIdParseError, UserAuthContext,
};
use etwin_core::types::AnyError;
use etwin_log::Logger;
use headers::authorization::{Basic, Bearer};
use headers::Authorization;
use serde::{Deserialize, Deserializer};
use serde_json::json;
use std::convert::Infallible;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::Arc;
use thiserror::Error;

pub const SESSION_COOKIE: &str = "sid";
pub const INTERNAL_AUTH_HEADER: &str = "Etwin-Internal-Auth";

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Extractor<T>(T);

impl<T> Extractor<T> {
  pub fn new(value: T) -> Self {
    Self(value)
  }

  pub fn value(self) -> T {
    self.0
  }
}

#[derive(Debug, Error)]
pub enum ExtractAuthContextError {
  #[error("malformed authorization header: `bearer` and `basic` parsing failed")]
  InvalidAuthHeader(String, String),
  #[error("invalid `bearer` token value")]
  BearerTokenFormat(#[source] EtwinOauthAccessTokenKeyParseError),
  #[error("invalid `basic` login value")]
  EtwinLoginFormat,
  #[error("authentication service failure")]
  Internal(#[source] AnyError),
  #[error("invalid session coookie value")]
  SessionKeyFormat(#[source] SessionIdParseError),
}

impl IntoResponse for ExtractAuthContextError {
  fn into_response(self) -> Response {
    let (status, message) = match self {
      Self::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string()),
      e => (StatusCode::BAD_REQUEST, e.to_string()),
    };
    (status, Json(json!({ "error": message }))).into_response()
  }
}

#[derive(Clone)]
pub struct AuthLogger(pub Arc<dyn Logger<ExtractAuthContextError>>);

#[async_trait]
impl<B> FromRequest<B> for Extractor<AuthContext>
where
  B: Send,
{
  type Rejection = Infallible;

  async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
    let Extension(api) = Extension::<RouterApi>::from_request(req)
      .await
      .expect("server configuration error: missing `RouterApi`");

    match auth(&api, req).await {
      Ok(acx) => Ok(Extractor::new(acx)),
      Err(e) => {
        match Extension::<AuthLogger>::from_request(req).await {
          Ok(Extension(logger)) => logger.0.log(e),
          Err(logger_err) => {
            eprintln!("(fallback logger: {}) {:?}", logger_err, e)
          }
        }
        Ok(Extractor::new(AuthContext::guest()))
      }
    }
  }
}

async fn auth<B>(api: &RouterApi, req: &mut RequestParts<B>) -> Result<AuthContext, ExtractAuthContextError>
where
  B: Send,
{
  Ok(if let Some(acx) = auth_header(api, req).await? {
    acx
  } else if let Some(acx) = auth_cookie(api, req).await? {
    acx
  } else {
    AuthContext::guest()
  })
}

async fn auth_header<B>(
  api: &RouterApi,
  req: &mut RequestParts<B>,
) -> Result<Option<AuthContext>, ExtractAuthContextError>
where
  B: Send,
{
  match authorization_header_from_request(req).await? {
    None => Ok(None),
    Some(EtwinAuthorizationHeader::Basic(header)) => auth_basic(api, header).await,
    Some(EtwinAuthorizationHeader::Bearer(header)) => auth_bearer(api, header).await,
  }
}

#[derive(Debug, Clone)]
enum EtwinAuthorizationHeader {
  Basic(TypedHeader<Authorization<Basic>>),
  Bearer(TypedHeader<Authorization<Bearer>>),
}

async fn authorization_header_from_request<B>(
  req: &mut RequestParts<B>,
) -> Result<Option<EtwinAuthorizationHeader>, ExtractAuthContextError>
where
  B: Send,
{
  let basic: Result<_, TypedHeaderRejection> = TypedHeader::<Authorization<Basic>>::from_request(req).await;
  let invalid_basic = match basic {
    Ok(basic) => return Ok(Some(EtwinAuthorizationHeader::Basic(basic))),
    Err(rejection) => match rejection.reason() {
      TypedHeaderRejectionReason::Missing => return Ok(None),
      TypedHeaderRejectionReason::Error(e) => e.to_string(),
      _ => rejection.to_string(),
    },
  };

  let bearer: Result<_, TypedHeaderRejection> = TypedHeader::<Authorization<Bearer>>::from_request(req).await;
  let invalid_bearer = match bearer {
    Ok(bearer) => return Ok(Some(EtwinAuthorizationHeader::Bearer(bearer))),
    Err(rejection) => match rejection.reason() {
      TypedHeaderRejectionReason::Missing => return Ok(None),
      TypedHeaderRejectionReason::Error(e) => e.to_string(),
      _ => rejection.to_string(),
    },
  };

  Err(ExtractAuthContextError::InvalidAuthHeader(
    invalid_basic,
    invalid_bearer,
  ))
}

async fn auth_bearer(
  api: &RouterApi,
  header: TypedHeader<Authorization<Bearer>>,
) -> Result<Option<AuthContext>, ExtractAuthContextError> {
  let TypedHeader(Authorization(token)) = header;
  let token: EtwinOauthAccessTokenKey = token
    .token()
    .parse()
    .map_err(ExtractAuthContextError::BearerTokenFormat)?;
  api
    .auth
    .authenticate_access_token(&token)
    .await
    .map_err(ExtractAuthContextError::Internal)
    .map(Some)
}

async fn auth_basic(
  api: &RouterApi,
  header: TypedHeader<Authorization<Basic>>,
) -> Result<Option<AuthContext>, ExtractAuthContextError> {
  let TypedHeader(Authorization(credentials)) = header;
  let credentials = Credentials {
    login: credentials
      .username()
      .parse()
      .map_err(|()| ExtractAuthContextError::EtwinLoginFormat)?,
    password: credentials.password().into(),
  };
  api
    .auth
    .authenticate_credentials(credentials)
    .await
    .map_err(ExtractAuthContextError::Internal)
    .map(Some)
}

async fn auth_cookie<B>(
  api: &RouterApi,
  req: &mut RequestParts<B>,
) -> Result<Option<AuthContext>, ExtractAuthContextError>
where
  B: Send,
{
  let jar: Result<_, Infallible> = CookieJar::from_request(req).await;
  let jar = match jar {
    Ok(jar) => jar,
    Err(_) => unreachable!("`Infaillible` error"),
  };
  let cookie = match jar.get(SESSION_COOKIE) {
    Some(cookie) => cookie,
    None => return Ok(None),
  };
  let session_key: SessionId = cookie
    .value()
    .parse()
    .map_err(ExtractAuthContextError::SessionKeyFormat)?;
  let user_and_session = api
    .auth
    .authenticate_session(session_key)
    .await
    .map_err(ExtractAuthContextError::Internal)?;
  let user_and_session = match user_and_session {
    Some(uas) => uas,
    None => return Ok(None),
  };
  Ok(Some(AuthContext::User(UserAuthContext {
    scope: AuthScope::Default,
    user: user_and_session.user.into(),
    is_administrator: user_and_session.is_administrator,
  })))
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InternalAuthKey(pub String);

/// Used for internal requests such as those used during system boot.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InternalAuth(());

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
pub enum InternalAuthRejection {
  #[error("internal authentication is disabled (no servr key)")]
  Disabled,
  #[error("missing internal authentication header")]
  Missing,
  #[error("invalid internal authentication key")]
  BadKey,
}

impl IntoResponse for InternalAuthRejection {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::Disabled => StatusCode::FORBIDDEN,
      Self::Missing => StatusCode::UNAUTHORIZED,
      Self::BadKey => StatusCode::FORBIDDEN,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

#[async_trait]
impl<B> FromRequest<B> for Extractor<InternalAuth>
where
  B: Send,
{
  type Rejection = InternalAuthRejection;

  async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
    let Extension(InternalAuthKey(internal_key)) = Extension::<InternalAuthKey>::from_request(req)
      .await
      .map_err(|_| InternalAuthRejection::Disabled)?;

    let header = req
      .headers()
      .get(INTERNAL_AUTH_HEADER)
      .ok_or(InternalAuthRejection::Missing)?;

    if internal_key.as_bytes() == header.as_bytes() {
      Ok(Extractor::new(InternalAuth(())))
    } else {
      Err(InternalAuthRejection::BadKey)
    }
  }
}

/// Wrapper around path a parameter deserialized through its `FromStr` impl
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ParamFromStr<T>(pub T);

impl<'de, T> Deserialize<'de> for ParamFromStr<T>
where
  T: FromStr,
  <T as FromStr>::Err: std::error::Error,
{
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    struct SerdeVisitor<T>(PhantomData<fn() -> T>);
    impl<'de, T> ::serde::de::Visitor<'de> for SerdeVisitor<T>
    where
      T: FromStr,
      <T as FromStr>::Err: std::error::Error,
    {
      type Value = T;

      fn expecting(&self, fmt: &mut ::std::fmt::Formatter) -> std::fmt::Result {
        write!(fmt, "a string for a valid {}", std::any::type_name::<T>())
      }

      fn visit_str<E: ::serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
        value.parse().map_err(E::custom)
      }
    }

    deserializer
      .deserialize_str(SerdeVisitor::<T>(PhantomData))
      .map(ParamFromStr)
  }
}
