//! Repository settings: visibility and label management.
//!
//! Only the repository owner may reach these pages. A non-owner is answered
//! with 403; an anonymous visitor with 401. A private repository the visitor
//! cannot read is answered as though it did not exist (404), the same way
//! every other repository-scoped page does.

use std::sync::Arc;

use actix_web::{HttpResponse, get, post, web};
use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::error;

use crate::AppState;
use crate::auth::CurrentUser;
use crate::repositories;
use crate::repositories::store;
use crate::tickets;
use crate::views;

use super::readable_repository;

/// What the settings form posts.
#[derive(Debug, Deserialize)]
pub struct SettingsForm {
    #[serde(default)]
    visibility: Option<String>,
    #[serde(default)]
    default_branch: Option<String>,
    #[serde(default)]
    default_merge_policy: Option<String>,
}

/// What the label creation form posts.
#[derive(Debug, Deserialize)]
pub struct LabelForm {
    name: String,
}

#[get("/~{user_or_team}/{repository_slug}/settings")]
#[tracing::instrument(
    skip_all,
    fields(owner = tracing::field::Empty, repository_slug = tracing::field::Empty)
)]
pub async fn get_settings(
    data: web::Data<Arc<RwLock<AppState>>>,
    current: CurrentUser,
    path: web::Path<(String, String)>,
) -> HttpResponse {
    let (owner_name, repository_slug) = path.into_inner();

    let span = tracing::Span::current();
    span.record("owner", owner_name.as_str());
    span.record("repository_slug", repository_slug.as_str());

    let (owner, repository, _) =
        match readable_repository(&data, Some(&current), &owner_name, &repository_slug).await {
            Ok(found) => found,
            Err(response) => return *response,
        };

    if current.id != repository.owner_id {
        return HttpResponse::Forbidden().finish();
    }

    let db = {
        let state = crate::state_read(&data).await;
        state.db.clone()
    };

    let labels = match tickets::store::labels(&db, repository.id).await {
        Ok(labels) => labels,
        Err(error) => {
            error!(%error, owner = %owner.name, slug = %repository.slug, "reading labels failed");
            return HttpResponse::InternalServerError().finish();
        }
    };

    // Fetch branches for the default branch dropdown
    let repo_root = {
        let state = crate::state_read(&data).await;
        state.repo_root.clone()
    };
    let bare = repositories::RepositoryPath {
        owner: owner.name.clone(),
        slug: repository.slug.clone(),
    }
    .bare_path(&repo_root);

    let branches = match repositories::git::branches(&bare, None).await {
        Ok(branches) => branches.into_iter().map(|b| b.name).collect(),
        Err(error) => {
            error!(%error, owner = %owner.name, slug = %repository.slug, "reading branches failed");
            vec![]
        }
    };

    views::repository_settings(views::RepositorySettingsParams {
        viewer: Some(&current.name),
        owner: &owner.name,
        slug: &repository.slug,
        default_branch: &repository.default_branch,
        branches: &branches,
        default_merge_policy: repository.default_merge_policy.label(),
        visibility: repository.visibility.label(),
        labels: &labels,
        error: None,
    })
}

#[post("/~{user_or_team}/{repository_slug}/settings")]
#[tracing::instrument(
    skip_all,
    fields(owner = tracing::field::Empty, repository_slug = tracing::field::Empty)
)]
pub async fn post_settings(
    data: web::Data<Arc<RwLock<AppState>>>,
    current: CurrentUser,
    path: web::Path<(String, String)>,
    form: web::Form<SettingsForm>,
) -> HttpResponse {
    let (owner_name, repository_slug) = path.into_inner();

    let span = tracing::Span::current();
    span.record("owner", owner_name.as_str());
    span.record("repository_slug", repository_slug.as_str());

    let (owner, repository, _) =
        match readable_repository(&data, Some(&current), &owner_name, &repository_slug).await {
            Ok(found) => found,
            Err(response) => return *response,
        };

    if current.id != repository.owner_id {
        return HttpResponse::Forbidden().finish();
    }

    let db = {
        let state = crate::state_read(&data).await;
        state.db.clone()
    };

    // Update visibility if provided
    if let Some(visibility_str) = &form.visibility {
        let visibility = repositories::visibility_from_form(visibility_str);
        if let Err(error) = store::update_visibility(&db, repository.id, visibility).await {
            error!(%error, owner = %owner.name, slug = %repository.slug, "updating visibility failed");
            return HttpResponse::InternalServerError().finish();
        }
    }

    // Update default_branch if provided
    if let Some(default_branch) = &form.default_branch {
        let default_branch = default_branch.trim();
        if default_branch.is_empty() {
            let labels = match tickets::store::labels(&db, repository.id).await {
                Ok(labels) => labels,
                Err(error) => {
                    error!(%error, owner = %owner.name, slug = %repository.slug, "reading labels failed");
                    return HttpResponse::InternalServerError().finish();
                }
            };

            // Fetch branches for the dropdown
            let repo_root = {
                let state = crate::state_read(&data).await;
                state.repo_root.clone()
            };
            let bare = repositories::RepositoryPath {
                owner: owner.name.clone(),
                slug: repository.slug.clone(),
            }
            .bare_path(&repo_root);

            let branches = match repositories::git::branches(&bare, None).await {
                Ok(branches) => branches.into_iter().map(|b| b.name).collect(),
                Err(_) => vec![],
            };

            return views::repository_settings(views::RepositorySettingsParams {
                viewer: Some(&current.name),
                owner: &owner.name,
                slug: &repository.slug,
                default_branch: &repository.default_branch,
                branches: &branches,
                default_merge_policy: repository.default_merge_policy.label(),
                visibility: repository.visibility.label(),
                labels: &labels,
                error: Some("A default branch is required"),
            });
        }

        if let Err(error) = store::update_default_branch(&db, repository.id, default_branch).await {
            error!(%error, owner = %owner.name, slug = %repository.slug, "updating default branch failed");
            return HttpResponse::InternalServerError().finish();
        }
    }

    // Update default_merge_policy if provided
    if let Some(merge_policy_str) = &form.default_merge_policy {
        let default_merge_policy = repositories::merge_policy_from_form(merge_policy_str);

        if default_merge_policy.is_none() {
            let labels = match tickets::store::labels(&db, repository.id).await {
                Ok(labels) => labels,
                Err(error) => {
                    error!(%error, owner = %owner.name, slug = %repository.slug, "reading labels failed");
                    return HttpResponse::InternalServerError().finish();
                }
            };

            // Fetch branches for the dropdown
            let repo_root = {
                let state = crate::state_read(&data).await;
                state.repo_root.clone()
            };
            let bare = repositories::RepositoryPath {
                owner: owner.name.clone(),
                slug: repository.slug.clone(),
            }
            .bare_path(&repo_root);

            let branches = match repositories::git::branches(&bare, None).await {
                Ok(branches) => branches.into_iter().map(|b| b.name).collect(),
                Err(_) => vec![],
            };

            return views::repository_settings(views::RepositorySettingsParams {
                viewer: Some(&current.name),
                owner: &owner.name,
                slug: &repository.slug,
                default_branch: &repository.default_branch,
                branches: &branches,
                default_merge_policy: repository.default_merge_policy.label(),
                visibility: repository.visibility.label(),
                labels: &labels,
                error: Some(&format!("Invalid merge policy: {}", merge_policy_str)),
            });
        }

        if let Err(error) =
            store::update_default_merge_policy(&db, repository.id, default_merge_policy.unwrap())
                .await
        {
            error!(%error, owner = %owner.name, slug = %repository.slug, "updating default merge policy failed");
            return HttpResponse::InternalServerError().finish();
        }
    }

    HttpResponse::SeeOther()
        .append_header((
            "Location",
            format!("/~{owner_name}/{repository_slug}/settings"),
        ))
        .finish()
}

#[post("/~{user_or_team}/{repository_slug}/settings/labels")]
#[tracing::instrument(
    skip_all,
    fields(owner = tracing::field::Empty, repository_slug = tracing::field::Empty)
)]
pub async fn post_label(
    data: web::Data<Arc<RwLock<AppState>>>,
    current: CurrentUser,
    path: web::Path<(String, String)>,
    form: web::Form<LabelForm>,
) -> HttpResponse {
    let (owner_name, repository_slug) = path.into_inner();

    let span = tracing::Span::current();
    span.record("owner", owner_name.as_str());
    span.record("repository_slug", repository_slug.as_str());

    let (owner, repository, _) =
        match readable_repository(&data, Some(&current), &owner_name, &repository_slug).await {
            Ok(found) => found,
            Err(response) => return *response,
        };

    if current.id != repository.owner_id {
        return HttpResponse::Forbidden().finish();
    }

    let db = {
        let state = crate::state_read(&data).await;
        state.db.clone()
    };

    let name = tickets::checked_label_name(&form.name);
    let name = match name {
        Ok(name) => name,
        Err(error) => {
            let labels = match tickets::store::labels(&db, repository.id).await {
                Ok(labels) => labels,
                Err(error) => {
                    error!(%error, owner = %owner.name, slug = %repository.slug, "reading labels failed");
                    return HttpResponse::InternalServerError().finish();
                }
            };

            // Fetch branches for the dropdown
            let repo_root = {
                let state = crate::state_read(&data).await;
                state.repo_root.clone()
            };
            let bare = repositories::RepositoryPath {
                owner: owner.name.clone(),
                slug: repository.slug.clone(),
            }
            .bare_path(&repo_root);

            let branches = match repositories::git::branches(&bare, None).await {
                Ok(branches) => branches.into_iter().map(|b| b.name).collect(),
                Err(_) => vec![],
            };

            return views::repository_settings(views::RepositorySettingsParams {
                viewer: Some(&current.name),
                owner: &owner.name,
                slug: &repository.slug,
                default_branch: &repository.default_branch,
                branches: &branches,
                default_merge_policy: repository.default_merge_policy.label(),
                visibility: repository.visibility.label(),
                labels: &labels,
                error: Some(&error),
            });
        }
    };

    // Refuse a label that already exists: the filter would not be able to
    // tell them apart.
    if let Ok(Some(_)) = tickets::store::find_label(&db, repository.id, name).await {
        let labels = match tickets::store::labels(&db, repository.id).await {
            Ok(labels) => labels,
            Err(error) => {
                error!(%error, owner = %owner.name, slug = %repository.slug, "reading labels failed");
                return HttpResponse::InternalServerError().finish();
            }
        };

        // Fetch branches for the dropdown
        let repo_root = {
            let state = crate::state_read(&data).await;
            state.repo_root.clone()
        };
        let bare = repositories::RepositoryPath {
            owner: owner.name.clone(),
            slug: repository.slug.clone(),
        }
        .bare_path(&repo_root);

        let branches = match repositories::git::branches(&bare, None).await {
            Ok(branches) => branches.into_iter().map(|b| b.name).collect(),
            Err(_) => vec![],
        };

        return views::repository_settings(views::RepositorySettingsParams {
            viewer: Some(&current.name),
            owner: &owner.name,
            slug: &repository.slug,
            default_branch: &repository.default_branch,
            branches: &branches,
            default_merge_policy: repository.default_merge_policy.label(),
            visibility: repository.visibility.label(),
            labels: &labels,
            error: Some(&format!("`{name}` already exists")),
        });
    }

    if let Err(error) = tickets::store::create_label(&db, repository.id, name).await {
        error!(%error, owner = %owner.name, slug = %repository.slug, "creating label failed");
        return HttpResponse::InternalServerError().finish();
    }

    HttpResponse::SeeOther()
        .append_header((
            "Location",
            format!("/~{owner_name}/{repository_slug}/settings"),
        ))
        .finish()
}

#[post("/~{user_or_team}/{repository_slug}/settings/labels/{label_id}/delete")]
#[tracing::instrument(
    skip_all,
    fields(owner = tracing::field::Empty, repository_slug = tracing::field::Empty, label_id = tracing::field::Empty)
)]
pub async fn post_delete_label(
    data: web::Data<Arc<RwLock<AppState>>>,
    current: CurrentUser,
    path: web::Path<(String, String, i64)>,
) -> HttpResponse {
    let (owner_name, repository_slug, label_id) = path.into_inner();

    let span = tracing::Span::current();
    span.record("owner", owner_name.as_str());
    span.record("repository_slug", repository_slug.as_str());
    span.record("label_id", label_id);

    let (owner, repository, _) =
        match readable_repository(&data, Some(&current), &owner_name, &repository_slug).await {
            Ok(found) => found,
            Err(response) => return *response,
        };

    if current.id != repository.owner_id {
        return HttpResponse::Forbidden().finish();
    }

    let db = {
        let state = crate::state_read(&data).await;
        state.db.clone()
    };

    // Look up the label to verify it exists and belongs to this repository.
    match tickets::store::find_label_by_id(&db, label_id).await {
        Ok(Some(label)) if label.repository_id == repository.id => {
            // Label exists and belongs to this repository, proceed with deletion
        }
        Ok(_) => {
            // The label does not exist or belongs to another repository:
            // answered as though it were gone, which is what a delete wants.
            return HttpResponse::SeeOther()
                .append_header((
                    "Location",
                    format!("/~{owner_name}/{repository_slug}/settings"),
                ))
                .finish();
        }
        Err(error) => {
            error!(%error, owner = %owner.name, slug = %repository.slug, label_id, "looking up label failed");
            return HttpResponse::InternalServerError().finish();
        }
    };

    if let Err(error) = tickets::store::delete_label(&db, repository.id, label_id).await {
        error!(%error, owner = %owner.name, slug = %repository.slug, label_id, "deleting label failed");
        return HttpResponse::InternalServerError().finish();
    }

    HttpResponse::SeeOther()
        .append_header((
            "Location",
            format!("/~{owner_name}/{repository_slug}/settings"),
        ))
        .finish()
}
