use axum::{
    extract::{Query, State},
    http::StatusCode,
    response::{IntoResponse, Redirect},
    routing::{get, post},
    Form, Router,
};
use serde::{Deserialize, Serialize};
use surrealdb::{engine::remote::ws::Client, Surreal};

use crate::{
    error::AppError,
    user::{AuthSession, Credentials, Password, SecretPassword, User},
    util::ulid_to_suuid,
    AppJson, AppState,
};

// This allows us to extract the "next" field from the query string. We use this
// to redirect after log in.
#[derive(Debug, Deserialize)]
struct NextUrl {
    next: Option<String>,
}

#[derive(Debug, Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
enum UserStatus {
    Anonymous,
    LoggedIn { username: String, code: u64 },
}

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/login", post(login).get(status))
        .route("/logout", get(logout))
        .route("/signup", post(signup))
}

#[derive(Debug, Deserialize)]
struct Signup {
    email: String,
    username: String,
    password: String,
}

#[derive(Serialize)]
struct SignupSafe {
    email: String,
    username: String,
    password: SecretPassword,
}

impl From<Signup> for SignupSafe {
    fn from(value: Signup) -> Self {
        Self {
            email: value.email.to_ascii_lowercase(),
            username: value.username,
            password: SecretPassword::new(Password::new(value.password)),
        }
    }
}

#[tracing::instrument(skip(db, auth_session))]
async fn signup(
    mut auth_session: AuthSession,
    State(db): State<Surreal<Client>>,
    AppJson(signup): AppJson<Signup>,
) -> crate::error::Result<impl IntoResponse> {
    let id = ulid::Ulid::new();
    let user: Option<User> =
        db.create(("user", ulid_to_suuid(id))).content::<SignupSafe>(signup.into()).await?;
    let Some(user) = user else {
        return Err(AppError::Arbitrary(
            "could-not-create-user",
            "could not create user in db".to_string(),
        ));
    };

    if let Err(e) = auth_session.login(&user).await {
        tracing::error!("Failed to log in {}: {}", user.username, e);
        return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
    }

    Ok(StatusCode::NO_CONTENT.into_response())
}

#[tracing::instrument(skip(auth_session))]
async fn login(
    mut auth_session: AuthSession,
    Query(NextUrl { next }): Query<NextUrl>,
    Form(creds): Form<Credentials>,
) -> impl IntoResponse {
    let user = match auth_session.authenticate(creds.clone()).await {
        Ok(Some(user)) => user,
        Ok(None) => return AppError::InvalidCredentials.into_response(),
        Err(e) => {
            tracing::error!("{e}");
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
        }
    };

    if let Err(e) = auth_session.login(&user).await {
        tracing::error!("Failed to log in {}: {}", user.username, e);
        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
    }

    if let Some(ref next) = creds.next.or(next) {
        Redirect::to(next).into_response()
    } else {
        StatusCode::NO_CONTENT.into_response()
    }
}

async fn status(auth_session: AuthSession) -> AppJson<UserStatus> {
    match auth_session.user {
        Some(user) => AppJson(UserStatus::LoggedIn { code: user.code(), username: user.username }),
        None => AppJson(UserStatus::Anonymous),
    }
}

#[tracing::instrument(skip(auth_session))]
async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
    match auth_session.logout().await {
        Ok(_) => StatusCode::NO_CONTENT.into_response(),
        Err(e) => {
            tracing::error!("Failed to log out user: {}", e);
            StatusCode::INTERNAL_SERVER_ERROR.into_response()
        }
    }
}