use axum::{
    extract::{Json, Host},
    response::{IntoResponse, Response},
    http::StatusCode, Extension
};
use axum_extra::extract::cookie::{CookieJar, Cookie};
use axum_extra::{TypedHeader, headers::{authorization::Bearer, Authorization}};

use super::backend::AuthBackend;
use crate::database::Storage;

pub(crate) const CHALLENGE_ID_COOKIE: &str = "kittybox_webauthn_challenge_id";

macro_rules! bail {
    ($msg:literal, $err:expr) => {
        {
            ::tracing::error!($msg, $err);
            return ::axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}

pub async fn webauthn_pre_register<A: AuthBackend, D: Storage + 'static>(
    Host(host): Host,
    Extension(db): Extension<D>,
    Extension(auth): Extension<A>,
    cookies: CookieJar
) -> Response {
    let uid = format!("https://{}/", host.clone());
    let uid_url: url::Url = uid.parse().unwrap();
    // This will not find an h-card in onboarding!
    let display_name = match db.get_post(&uid).await {
        Ok(hcard) => match hcard {
            Some(mut hcard) => {
                match hcard["properties"]["uid"][0].take() {
                    serde_json::Value::String(name) => name,
                    _ => String::default()
                }
            },
            None => String::default()
        },
        Err(err) => bail!("Error retrieving h-card: {}", err)
    };

    let webauthn = webauthn::WebauthnBuilder::new(
        &host,
        &uid_url
    )
        .unwrap()
        .rp_name("Kittybox")
        .build()
        .unwrap();

    let (challenge, state) = match webauthn.start_passkey_registration(
        // Note: using a nil uuid here is fine
        // Because the user corresponds to a website anyway
        // We do not track multiple users
        webauthn::prelude::Uuid::nil(),
        &uid,
        &display_name,
        Some(vec![])
    ) {
        Ok((challenge, state)) => (challenge, state),
        Err(err) => bail!("Error generating WebAuthn registration data: {}", err)
    };

    match auth.persist_registration_challenge(&uid_url, state).await {
        Ok(challenge_id) => (
            cookies.add(
                Cookie::build((CHALLENGE_ID_COOKIE, challenge_id))
                    .secure(true)
                    .finish()
            ),
            Json(challenge)
        ).into_response(),
        Err(err) => bail!("Failed to persist WebAuthn challenge: {}", err)
    }
}

pub async fn webauthn_register<A: AuthBackend>(
    Host(host): Host,
    Json(credential): Json<webauthn::prelude::RegisterPublicKeyCredential>,
    // TODO determine if we can use a cookie maybe?
    user_credential: Option<TypedHeader<Authorization<Bearer>>>,
    Extension(auth): Extension<A>
) -> Response {
    let uid = format!("https://{}/", host.clone());
    let uid_url: url::Url = uid.parse().unwrap();

    let pubkeys = match auth.list_webauthn_pubkeys(&uid_url).await {
        Ok(pubkeys) => pubkeys,
        Err(err) => bail!("Error enumerating existing WebAuthn credentials: {}", err)
    };

    if !pubkeys.is_empty() {
        if let Some(TypedHeader(Authorization(token))) = user_credential {
            // TODO check validity of the credential
        } else {
            return StatusCode::UNAUTHORIZED.into_response()
        }
    }

    return StatusCode::OK.into_response()
}

pub(crate) async fn verify<A: AuthBackend>(
    auth: &A,
    website: &url::Url,
    credential: webauthn::prelude::PublicKeyCredential,
    challenge_id: &str
) -> std::io::Result<bool> {
    let host = website.host_str().unwrap();

    let webauthn = webauthn::WebauthnBuilder::new(
        host,
        website
    )
        .unwrap()
        .rp_name("Kittybox")
        .build()
        .unwrap();

    match webauthn.finish_passkey_authentication(
        &credential,
        &auth.retrieve_authentication_challenge(&website, challenge_id).await?
    ) {
        Err(err) => {
            tracing::error!("WebAuthn error: {}", err);
            Ok(false)
        },
        Ok(authentication_result) => {
            let counter = authentication_result.counter();
            let cred_id = authentication_result.cred_id();

            if authentication_result.needs_update() {
                todo!()
            }
            Ok(true)
        }
    }
}