//! The repository store: the rows behind a repository, and the lookups the
//! webui and the SSH server share.
//!
//! Split from [`super`], which holds the rules that do not need a database.

use sea_orm::ActiveValue::Set;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, DatabaseConnection, DbErr, EntityTrait, PaginatorTrait,
    QueryFilter, QueryOrder,
};
use uuid::Uuid;

use crate::entities::repositories::{self, MergePolicy, Visibility};
use crate::entities::users;

/// Looks up a user by name, the way a repository is addressed.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn find_user(db: &DatabaseConnection, name: &str) -> Result<Option<users::Model>, DbErr> {
    users::Entity::find()
        .filter(users::Column::Name.eq(name))
        .filter(users::Column::DeletedAt.is_null())
        .one(db)
        .await
}

/// Looks up one of `owner_id`'s repositories by slug, ignoring any that have
/// been deleted.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn find_repository(
    db: &DatabaseConnection,
    owner_id: i64,
    slug: &str,
) -> Result<Option<repositories::Model>, DbErr> {
    repositories::Entity::find()
        .filter(repositories::Column::OwnerId.eq(owner_id))
        .filter(repositories::Column::Slug.eq(slug))
        .filter(repositories::Column::DeletedAt.is_null())
        .one(db)
        .await
}

/// Any row for `owner_id` and `slug`, whether or not it has been deleted.
///
/// Creating a repository checks this rather than [`find_repository`]: the
/// unique constraint ignores `deleted_at`, so a soft-deleted repository still
/// holds its name. Refusing here says that plainly instead of letting the
/// insert fail on a constraint.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn find_any_repository(
    db: &DatabaseConnection,
    owner_id: i64,
    slug: &str,
) -> Result<Option<repositories::Model>, DbErr> {
    repositories::Entity::find()
        .filter(repositories::Column::OwnerId.eq(owner_id))
        .filter(repositories::Column::Slug.eq(slug))
        .one(db)
        .await
}

/// Resolves an owner name and slug to the user and the repository together,
/// which is what every address on the web and over SSH turns into.
///
/// A missing owner and a missing repository are both `None`: a client that
/// guesses which of the two it was wrong has learned nothing it should not.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn find_repository_by_path(
    db: &DatabaseConnection,
    owner: &str,
    slug: &str,
) -> Result<Option<(users::Model, repositories::Model)>, DbErr> {
    let Some(user) = find_user(db, owner).await? else {
        return Ok(None);
    };

    find_repository(db, user.id, slug)
        .await
        .map(|repository| repository.map(|repository| (user, repository)))
}

/// The repositories a user owns, most recently created first.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn list_repositories(
    db: &DatabaseConnection,
    owner_id: i64,
) -> Result<Vec<repositories::Model>, DbErr> {
    repositories::Entity::find()
        .filter(repositories::Column::OwnerId.eq(owner_id))
        .filter(repositories::Column::DeletedAt.is_null())
        .order_by_desc(repositories::Column::CreatedAt)
        .all(db)
        .await
}

/// Records a new repository for `owner_id`.
///
/// The row only - creating the bare repository on disk is [`super::init_bare`],
/// which the caller does first so that a row is never left pointing at a
/// directory that was never made.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn create_repository(
    db: &DatabaseConnection,
    owner_id: i64,
    slug: &str,
    description: Option<&str>,
    visibility: Visibility,
) -> Result<repositories::Model, DbErr> {
    repositories::ActiveModel {
        uuid: Set(Uuid::new_v4()),
        owner_id: Set(owner_id),
        slug: Set(slug.to_string()),
        description: Set(description.map(str::to_string)),
        visibility: Set(visibility),
        default_branch: Set(super::DEFAULT_BRANCH.to_string()),
        next_ticket_number: Set(1),
        next_patch_number: Set(1),
        default_merge_policy: Set(repositories::MergePolicy::PreferFastForward),
        ..Default::default()
    }
    .insert(db)
    .await
}

/// How many repositories a user owns, for the checks that want to refuse a
/// name without building anything.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn count_repositories(db: &DatabaseConnection, owner_id: i64) -> Result<u64, DbErr> {
    repositories::Entity::find()
        .filter(repositories::Column::OwnerId.eq(owner_id))
        .filter(repositories::Column::DeletedAt.is_null())
        .count(db)
        .await
}

/// Updates the visibility of a repository.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn update_visibility(
    db: &DatabaseConnection,
    repository_id: i64,
    visibility: Visibility,
) -> Result<(), DbErr> {
    let Some(repository) = repositories::Entity::find_by_id(repository_id)
        .one(db)
        .await?
    else {
        return Ok(());
    };

    let mut active: repositories::ActiveModel = repository.into();
    active.visibility = Set(visibility);
    active.update(db).await?;
    Ok(())
}

/// Updates the default branch of a repository.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn update_default_branch(
    db: &DatabaseConnection,
    repository_id: i64,
    default_branch: &str,
) -> Result<(), DbErr> {
    let Some(repository) = repositories::Entity::find_by_id(repository_id)
        .one(db)
        .await?
    else {
        return Ok(());
    };

    let mut active: repositories::ActiveModel = repository.into();
    active.default_branch = Set(default_branch.to_string());
    active.update(db).await?;
    Ok(())
}

/// Updates the default merge policy of a repository.
#[tracing::instrument(level = "debug", skip(db))]
pub async fn update_default_merge_policy(
    db: &DatabaseConnection,
    repository_id: i64,
    default_merge_policy: MergePolicy,
) -> Result<(), DbErr> {
    let Some(repository) = repositories::Entity::find_by_id(repository_id)
        .one(db)
        .await?
    else {
        return Ok(());
    };

    let mut active: repositories::ActiveModel = repository.into();
    active.default_merge_policy = Set(default_merge_policy);
    active.update(db).await?;
    Ok(())
}
