use crate::{EternaltwinSystem, Request};
use async_trait::async_trait;
use axum::body::{Bytes, HttpBody};
use axum::extract::rejection::{BytesRejection, TypedHeaderRejection, TypedHeaderRejectionReason};
use axum::extract::{Extension, FromRequest, FromRequestParts, TypedHeader};
use axum::http::request::Parts;
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::{BoxError, Json};
use axum_extra::extract::CookieJar;
use eternaltwin_core::auth::{AuthContext, AuthScope, Credentials, SessionId, SessionIdParseError, UserAuthContext};
use eternaltwin_core::types::AnyError;
use eternaltwin_log::Logger;
use headers::authorization::{Basic, Bearer};
use headers::Authorization;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer};
use serde_json::json;
use std::convert::Infallible;
use std::marker::PhantomData;
use std::ops::Deref;
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 `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<S> FromRequestParts<S> for Extractor<AuthContext>
where
  S: Send + Sync,
{
  type Rejection = Infallible;

  async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
    let Extension(api) = Extension::<EternaltwinSystem>::from_request_parts(parts, state)
      .await
      .expect("server configuration error: missing `RouterApi`");

    match auth(&api, parts).await {
      Ok(acx) => Ok(Extractor::new(acx)),
      Err(e) => {
        match Extension::<AuthLogger>::from_request_parts(parts, &()).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(api: &EternaltwinSystem, parts: &mut Parts) -> Result<AuthContext, ExtractAuthContextError> {
  Ok(if let Some(acx) = auth_header(api, parts).await? {
    acx
  } else if let Some(acx) = auth_cookie(api, parts).await? {
    acx
  } else {
    AuthContext::guest()
  })
}

async fn auth_header(
  api: &EternaltwinSystem,
  parts: &mut Parts,
) -> Result<Option<AuthContext>, ExtractAuthContextError> {
  match authorization_header_from_request(parts).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(
  parts: &mut Parts,
) -> Result<Option<EtwinAuthorizationHeader>, ExtractAuthContextError> {
  let basic: Result<_, TypedHeaderRejection> =
    TypedHeader::<Authorization<Basic>>::from_request_parts(parts, &()).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_parts(parts, &()).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: &EternaltwinSystem,
  header: TypedHeader<Authorization<Bearer>>,
) -> Result<Option<AuthContext>, ExtractAuthContextError> {
  let TypedHeader(Authorization(token)) = header;
  api
    .auth
    .authenticate_access_token(token.token())
    .await
    .map_err(ExtractAuthContextError::Internal)
    .map(Some)
}

async fn auth_basic(
  api: &EternaltwinSystem,
  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(
  api: &EternaltwinSystem,
  parts: &mut Parts,
) -> Result<Option<AuthContext>, ExtractAuthContextError> {
  let jar: Result<_, Infallible> = CookieJar::from_request_parts(parts, &()).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("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::Missing => StatusCode::UNAUTHORIZED,
      Self::BadKey => StatusCode::FORBIDDEN,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

#[async_trait]
impl<S> FromRequestParts<S> for Extractor<InternalAuth>
where
  S: Sync,
{
  type Rejection = InternalAuthRejection;

  async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
    let Extension(api) = Extension::<EternaltwinSystem>::from_request_parts(parts, &())
      .await
      .expect("server configuration error: missing `RouterApi`");

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

    if api.auth.as_ref().authenticate_internal(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)
  }
}

#[derive(Debug, Clone, Copy, Default)]
pub struct FormOrJson<T>(pub T);

#[derive(Debug, Error)]
pub enum FormOrJsonRejection {
  #[error("invalid `Content-Type`, expected `application/x-www-form-urlencoded` or `application/json`")]
  ContentType,
  #[error("failed to buffer body")]
  Bytes(#[from] BytesRejection),
  #[error("failed to deserialize form body")]
  Form(#[source] serde_urlencoded::de::Error),
  #[error("failed to deserialize json body")]
  Json(#[source] serde_json::Error),
}

impl IntoResponse for FormOrJsonRejection {
  fn into_response(self) -> Response {
    let status = match self {
      Self::ContentType => StatusCode::UNSUPPORTED_MEDIA_TYPE,
      Self::Form(_) | Self::Json(_) => StatusCode::UNPROCESSABLE_ENTITY,
      Self::Bytes(b) => return b.into_response(),
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

#[async_trait]
impl<T, S, B> FromRequest<S, B> for FormOrJson<T>
where
  T: DeserializeOwned,
  B: HttpBody + Send + 'static,
  B::Data: Send,
  B::Error: Into<BoxError>,
  S: Send + Sync,
{
  type Rejection = FormOrJsonRejection;

  async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
    if has_content_type(req.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) {
      let bytes = Bytes::from_request(req, state).await?;
      let value = serde_urlencoded::from_bytes(&bytes).map_err(FormOrJsonRejection::Form)?;
      Ok(FormOrJson(value))
    } else if has_content_type(req.headers(), &mime::APPLICATION_JSON) {
      let bytes = Bytes::from_request(req, state).await?;
      let value = serde_json::from_slice(&bytes).map_err(FormOrJsonRejection::Json)?;
      Ok(FormOrJson(value))
    } else {
      return Err(FormOrJsonRejection::ContentType);
    }
  }
}

impl<T> Deref for FormOrJson<T> {
  type Target = T;

  fn deref(&self) -> &Self::Target {
    &self.0
  }
}

// Extracted from `axum/src/extract/mod.rs`
fn has_content_type(headers: &HeaderMap, expected_content_type: &mime::Mime) -> bool {
  let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
    content_type
  } else {
    return false;
  };

  let content_type = if let Ok(content_type) = content_type.to_str() {
    content_type
  } else {
    return false;
  };

  content_type.starts_with(expected_content_type.as_ref())
}
