//! Signing up, signing in, and signing out.
//!
//! Named `accounts` rather than `auth` so it does not read as the same thing
//! as [`crate::auth`], which holds the password hashing and the session
//! extraction these handlers lean on.

use std::sync::Arc;

use actix_session::Session;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, get, post, web};
use sea_orm::{ColumnTrait, DatabaseConnection, DbErr, EntityTrait, PaginatorTrait, QueryFilter};
use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;
use uuid::Uuid;

use crate::auth::{CurrentUser, log_in, log_out, verify_password, viewer_name};
use crate::entities::prelude::Users;
use crate::entities::users;
use crate::registration::{RegisterError, Registration};
use crate::views::{LoginForm, RegisterForm};
use crate::{AppState, state_read, views};

/// What the registration form posts.
#[derive(Debug, Deserialize)]
pub struct Register {
    pub name: String,
    pub email: String,
    pub password: String,
    pub invite: Option<String>,
}

/// What the `?invite=` query on the register page carries.
#[derive(Debug, Deserialize)]
pub struct RegisterQuery {
    pub invite: Option<String>,
}

/// What the login form posts.
#[derive(Debug, Deserialize)]
pub struct Login {
    name: String,
    password: String,
}

#[get("/register")]
#[tracing::instrument(skip_all)]
pub(crate) async fn get_register(
    data: web::Data<Arc<RwLock<AppState>>>,
    viewer: Option<CurrentUser>,
    query: web::Query<RegisterQuery>,
) -> HttpResponse {
    let state = state_read(&data).await;
    let db = &state.db;

    let mode = crate::registration::registration_mode();

    // In invite-only mode with a non-empty DB, check whether the invite is
    // acceptable before rendering the form.
    if mode == crate::registration::RegistrationMode::InviteOnly {
        let user_count = match Users::find().count(db).await {
            Ok(c) => c,
            Err(error) => {
                error!(%error, "counting users to decide the register page failed");
                return HttpResponse::InternalServerError().finish();
            }
        };

        if user_count > 0 {
            match query.invite.as_deref() {
                None => {
                    let mut response = views::register_closed(
                        viewer_name(viewer.as_ref()),
                        "Registration on this server is invite-only. Ask an existing user for an invite link.",
                    );
                    response.head_mut().status = StatusCode::FORBIDDEN;
                    return response;
                }
                Some(raw) => {
                    let parsed = match raw.parse::<Uuid>() {
                        Ok(u) => u,
                        Err(_) => {
                            let mut response = views::register_closed(
                                viewer_name(viewer.as_ref()),
                                "This invite is not valid (any more).",
                            );
                            response.head_mut().status = StatusCode::FORBIDDEN;
                            return response;
                        }
                    };
                    if crate::registration::find_valid_invite(db, parsed)
                        .await
                        .is_none()
                    {
                        let mut response = views::register_closed(
                            viewer_name(viewer.as_ref()),
                            "This invite is not valid (any more).",
                        );
                        response.head_mut().status = StatusCode::FORBIDDEN;
                        return response;
                    }
                }
            }
        }
    }

    views::register(
        viewer_name(viewer.as_ref()),
        &RegisterForm {
            name: None,
            email: None,
            invite: query.invite.clone(),
            error: None,
        },
    )
}

#[post("/register")]
#[tracing::instrument(skip_all)]
pub(crate) async fn post_register(
    data: web::Data<Arc<RwLock<AppState>>>,
    viewer: Option<CurrentUser>,
    session: Session,
    form: web::Form<Register>,
) -> HttpResponse {
    let form = form.into_inner();

    let state = state_read(&data).await;
    let db = &state.db;

    // Parse the invite if present; a malformed value counts as "no valid
    // invite", which the gate in `register()` will turn into the same 403 the
    // missing-invite path produces.
    let invite = form
        .invite
        .as_deref()
        .filter(|s| !s.trim().is_empty())
        .and_then(|s| s.parse::<Uuid>().ok());

    let registration = Registration {
        name: form.name.clone(),
        email: form.email.clone(),
        password: form.password.clone(),
        ssh_key: None,
        invite,
    };

    let user = match crate::registration::register(db, registration).await {
        Ok(user) => user,
        Err(error) => {
            let message = match &error {
                RegisterError::Invalid(reason) => reason.clone(),
                RegisterError::Taken => "That name or email is already taken.".to_string(),
                RegisterError::Database(_) => {
                    return HttpResponse::InternalServerError().finish();
                }
                RegisterError::InviteRequired => {
                    "Registration on this server is invite-only.".to_string()
                }
                RegisterError::InviteInvalid(reason) => reason.clone(),
            };

            let status = match error {
                RegisterError::Invalid(_) => StatusCode::BAD_REQUEST,
                RegisterError::Taken => StatusCode::CONFLICT,
                RegisterError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
                RegisterError::InviteRequired | RegisterError::InviteInvalid(_) => {
                    // Don't show the form when invite is required/invalid
                    let mut response =
                        views::register_closed(viewer_name(viewer.as_ref()), &message);
                    response.head_mut().status = StatusCode::FORBIDDEN;
                    return response;
                }
            };

            return refused_register(viewer_name(viewer.as_ref()), form, &message, status);
        }
    };

    if let Err(error) = log_in(&session, &user) {
        error!(%error, user = %user.name, "starting a session failed");
        return HttpResponse::InternalServerError().finish();
    }

    HttpResponse::SeeOther()
        .append_header(("Location", format!("/~{}", user.name)))
        .finish()
}

#[get("/login")]
#[tracing::instrument(skip_all)]
pub(crate) async fn get_login(viewer: Option<CurrentUser>) -> HttpResponse {
    views::login(
        viewer_name(viewer.as_ref()),
        &LoginForm {
            name: None,
            error: None,
        },
    )
}

#[post("/login")]
#[tracing::instrument(skip_all)]
pub(crate) async fn post_login(
    data: web::Data<Arc<RwLock<AppState>>>,
    viewer: Option<CurrentUser>,
    session: Session,
    form: web::Form<Login>,
) -> HttpResponse {
    let form = form.into_inner();
    let state = state_read(&data).await;
    let db = &state.db;

    let user = match find_by_name(db, &form.name).await {
        Ok(user) => user,
        Err(error) => {
            error!(%error, name = %form.name, "looking up the user to log in failed");
            return HttpResponse::InternalServerError().finish();
        }
    };

    // A user that is not there and a password that does not match are the
    // same answer on purpose: which of the two it was is not the caller's
    // business. Neither is an error on our side, so the span stays green.
    let Some(user) = user.filter(|user| verify_password(&form.password, &user.password_hash))
    else {
        let mut response = views::login(
            viewer_name(viewer.as_ref()),
            &LoginForm {
                name: Some(form.name),
                error: Some("Wrong name or password.".to_string()),
            },
        );
        response.head_mut().status = StatusCode::UNAUTHORIZED;
        return response;
    };

    if let Err(error) = log_in(&session, &user) {
        error!(%error, user = %user.name, "starting a session failed");
        return HttpResponse::InternalServerError().finish();
    }

    HttpResponse::SeeOther()
        .append_header(("Location", format!("/~{}", user.name)))
        .finish()
}

#[post("/logout")]
#[tracing::instrument(skip_all)]
pub(crate) async fn post_logout(session: Session) -> HttpResponse {
    log_out(&session);
    HttpResponse::SeeOther()
        .append_header(("Location", "/"))
        .finish()
}

/// Renders the registration form again, with what went wrong, under `status`.
fn refused_register(
    viewer: Option<&str>,
    form: Register,
    message: &str,
    status: StatusCode,
) -> HttpResponse {
    let mut response = views::register(
        viewer,
        &RegisterForm {
            name: Some(form.name),
            email: Some(form.email),
            invite: form.invite,
            error: Some(message.to_string()),
        },
    );
    response.head_mut().status = status;
    response
}

#[tracing::instrument(level = "debug", skip(db))]
async fn find_by_name(db: &DatabaseConnection, name: &str) -> Result<Option<users::Model>, DbErr> {
    Users::find()
        .filter(users::Column::Name.eq(name))
        .one(db)
        .await
}
