use crate::extract::{Extractor, FormOrJson};
use crate::EternaltwinSystem;
use ::url::Url;
use axum::body::Body;
use axum::extract::{OriginalUri, Query};
use axum::http::header::HeaderName;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use axum_extra::extract::CookieJar;
use eternaltwin_core::auth::{AuthContext, CreateAccessTokenOptions, GrantOauthAuthorizationOptions};
use eternaltwin_core::oauth::{EtwinOauthStateClaims, OauthAccessToken};
use eternaltwin_services::auth::{OauthCodeGrant, ReadOauthStateError};
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub fn router() -> Router<(), Body> {
  Router::new()
    .route("/authorize", get(grant_oauth_authorization))
    .route("/token", post(create_access_token))
    .route("/callback", get(on_oauth_callback))
}

#[derive(Debug, Error)]
enum GrantOauthAuthorizationError {
  #[error("inner error")]
  Inner(#[from] eternaltwin_services::auth::GrantOauthAuthorizationError),
}

impl IntoResponse for GrantOauthAuthorizationError {
  fn into_response(self) -> Response {
    use eternaltwin_services::auth::GrantOauthAuthorizationError as Inner;
    let (code, msg) = match self {
      Self::Inner(e @ Inner::MissingClientId) => (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()),
      Self::Inner(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string()),
    };
    (code, msg).into_response()
  }
}

async fn grant_oauth_authorization(
  Extension(api): Extension<EternaltwinSystem>,
  acx: Extractor<AuthContext>,
  Query(query): Query<GrantOauthAuthorizationOptions>,
  OriginalUri(original_uri): OriginalUri,
) -> Result<Redirect, GrantOauthAuthorizationError> {
  let acx = &acx.value();
  if matches!(acx, AuthContext::Guest(_)) {
    let mut target = Url::parse("https://localhost/login").expect("valid input url");
    if let Some(next) = original_uri.path_and_query() {
      target.query_pairs_mut().append_pair("next", next.as_str());
    }
    let target = target.as_str();
    let target = target
      .strip_prefix("https://localhost")
      .expect("the computed URL always starts with `https://localhost`");
    assert!(target.starts_with('/'));
    return Ok(Redirect::to(target));
  }

  let grant: OauthCodeGrant = api.auth.as_ref().grant_oauth_authorization(acx, &query).await?;
  Ok(Redirect::to(grant.redirect_uri().as_str()))
}

#[derive(Debug, Error)]
enum CreateAccessTokenError {
  #[error("inner error")]
  Inner(#[from] eternaltwin_services::auth::CreateAccessTokenError),
}

impl IntoResponse for CreateAccessTokenError {
  fn into_response(self) -> Response {
    use eternaltwin_services::auth::CreateAccessTokenError as Inner;
    let (code, msg) = match self {
      Self::Inner(e @ Inner::MissingCode) => (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()),
      Self::Inner(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string()),
    };
    (code, msg).into_response()
  }
}

// We use an extension to the OAuth spec (we accept JSON)
async fn create_access_token(
  Extension(api): Extension<EternaltwinSystem>,
  acx: Extractor<AuthContext>,
  options: FormOrJson<CreateAccessTokenOptions>,
) -> Result<Json<OauthAccessToken>, CreateAccessTokenError> {
  let acx = &acx.value();
  let access_token: OauthAccessToken = api.auth.as_ref().create_access_token(acx, &options).await?;
  Ok(Json(access_token))
}

#[derive(Debug, Error)]
enum CallbackError {
  #[error("inner error")]
  Inner(#[from] eternaltwin_services::auth::CreateAccessTokenError),
  #[error("invalid state {0:?}")]
  InvalidState(String, ReadOauthStateError),
  #[error("invalid action")]
  InvalidAction,
}

impl IntoResponse for CallbackError {
  fn into_response(self) -> Response {
    use eternaltwin_services::auth::CreateAccessTokenError as Inner;
    let (code, msg) = match dbg!(self) {
      Self::Inner(e @ Inner::MissingCode) => (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()),
      e @ Self::InvalidState(_, _) => (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()),
      _ => (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".to_string()),
    };
    (code, msg).into_response()
  }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
struct CallbackQuery {
  code: String,
  state: String,
}

// Called when returning from Twinoid for example.
async fn on_oauth_callback(
  Extension(api): Extension<EternaltwinSystem>,
  acx: Extractor<AuthContext>,
  Query(query): Query<CallbackQuery>,
  _jar: CookieJar,
) -> Result<
  (
    StatusCode,
    CookieJar,
    [(HeaderName, &'static str); 1],
    Json<serde_json::Value>,
  ),
  CallbackError,
> {
  let _acx = acx.value();
  let state = query.state;
  let _state: EtwinOauthStateClaims = api
    .auth
    .as_ref()
    .read_oauth_state(state.as_str())
    .map_err(|e| CallbackError::InvalidState(state, e))?;
  Err(CallbackError::InvalidAction)
}
