//! Admin authority-record management. Reads require `ViewInternal`; writes `EditCatalogue`.

use auth::{Authorized, EditCatalogue, ViewInternal};
use axum::{
    Json, Router,
    extract::{Path, Query, State},
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::get,
};
use domain::{AuditActor, AuthorityId, AuthorityKind, LocalizedLabel, NewAuthority};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::{
    AppState,
    admin_objects::LabelView,
    admin_vocab::{CreatedId, InUseView, LabelInput},
};

#[derive(Serialize, ToSchema)]
pub(crate) struct AuthorityView {
    pub id: String,
    #[schema(value_type = domain::AuthorityKind)]
    pub kind: String,
    pub external_uri: Option<String>,
    pub labels: Vec<LabelView>,
}

#[derive(Deserialize, ToSchema)]
pub(crate) struct NewAuthorityRequest {
    /// "person" | "organisation" | "place".
    pub kind: String,
    pub external_uri: Option<String>,
    pub labels: Vec<LabelInput>,
}

#[derive(Deserialize)]
pub(crate) struct KindQuery {
    kind: String,
}

#[utoipa::path(
    get, path = "/api/admin/authorities",
    params(("kind" = String, Query, description = "person | organisation | place")),
    responses(
        (status = 200, body = [AuthorityView]),
        (status = 401),
        (status = 403),
        (status = 422)
    )
)]
pub(crate) async fn list_authorities(
    _auth: Authorized<ViewInternal>,
    State(state): State<AppState>,
    Query(q): Query<KindQuery>,
) -> Result<Json<Vec<AuthorityView>>, StatusCode> {
    let kind = AuthorityKind::from_db(&q.kind).ok_or(StatusCode::UNPROCESSABLE_ENTITY)?;

    let authorities = db::authority::list_by_kind(state.db.pool(), kind)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok(Json(
        authorities
            .into_iter()
            .map(|authority| AuthorityView {
                id: authority.id.to_string(),
                kind: authority.kind.as_str().to_owned(),
                external_uri: authority.external_uri,
                labels: authority
                    .labels
                    .into_iter()
                    .map(|label| LabelView {
                        lang: label.lang,
                        label: label.label,
                    })
                    .collect(),
            })
            .collect(),
    ))
}

#[utoipa::path(
    post, path = "/api/admin/authorities",
    request_body = NewAuthorityRequest,
    responses(
        (status = 201, body = CreatedId),
        (status = 401),
        (status = 403),
        (status = 422)
    )
)]
pub(crate) async fn create_authority(
    auth: Authorized<EditCatalogue>,
    State(state): State<AppState>,
    Json(req): Json<NewAuthorityRequest>,
) -> Result<(StatusCode, Json<CreatedId>), StatusCode> {
    let kind = AuthorityKind::from_db(&req.kind).ok_or(StatusCode::UNPROCESSABLE_ENTITY)?;

    let new = NewAuthority {
        kind,
        external_uri: req.external_uri,
        labels: req
            .labels
            .into_iter()
            .map(|label| LocalizedLabel {
                lang: label.lang,
                label: label.label,
            })
            .collect(),
    };

    let mut tx = state
        .db
        .pool()
        .begin()
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let id =
        db::authority::create_authority(&mut tx, AuditActor::User(auth.user.id.to_uuid()), &new)
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    tx.commit()
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    Ok((StatusCode::CREATED, Json(CreatedId { id: id.to_string() })))
}

#[derive(Deserialize, ToSchema)]
pub(crate) struct UpdateAuthorityRequest {
    pub external_uri: Option<String>,
    pub labels: Vec<LabelInput>,
}

#[utoipa::path(
    patch, path = "/api/admin/authorities/{id}",
    request_body = UpdateAuthorityRequest,
    params(("id" = String, Path, description = "Authority id (UUID)")),
    responses(
        (status = 204),
        (status = 401),
        (status = 403),
        (status = 404)
    )
)]
pub(crate) async fn update_authority(
    auth: Authorized<EditCatalogue>,
    State(state): State<AppState>,
    Path(id): Path<String>,
    Json(req): Json<UpdateAuthorityRequest>,
) -> Result<StatusCode, StatusCode> {
    let id = id
        .parse::<AuthorityId>()
        .map_err(|_| StatusCode::NOT_FOUND)?;

    let labels: Vec<LocalizedLabel> = req
        .labels
        .into_iter()
        .map(|l| LocalizedLabel {
            lang: l.lang,
            label: l.label,
        })
        .collect();

    let mut tx = state
        .db
        .pool()
        .begin()
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    let existed = db::authority::update_authority(
        &mut tx,
        AuditActor::User(auth.user.id.to_uuid()),
        id,
        req.external_uri.as_deref(),
        &labels,
    )
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if existed {
        tx.commit()
            .await
            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

        Ok(StatusCode::NO_CONTENT)
    } else {
        let _ = tx.rollback().await;

        Err(StatusCode::NOT_FOUND)
    }
}

#[utoipa::path(
    delete, path = "/api/admin/authorities/{id}",
    params(("id" = String, Path, description = "Authority id (UUID)")),
    responses(
        (status = 204),
        (status = 401),
        (status = 403),
        (status = 404),
        (status = 409, body = InUseView, description = "Referenced by catalogue objects")
    )
)]
pub(crate) async fn delete_authority(
    auth: Authorized<EditCatalogue>,
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Response {
    let Ok(id) = id.parse::<AuthorityId>() else {
        return StatusCode::NOT_FOUND.into_response();
    };

    let Ok(mut tx) = state.db.pool().begin().await else {
        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
    };

    match db::authority::delete_authority(&mut tx, AuditActor::User(auth.user.id.to_uuid()), id)
        .await
    {
        Ok(db::DeleteOutcome::Deleted) => match tx.commit().await {
            Ok(()) => StatusCode::NO_CONTENT.into_response(),
            Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
        },
        Ok(db::DeleteOutcome::InUse { count }) => {
            let _ = tx.rollback().await;

            (StatusCode::CONFLICT, Json(InUseView { count })).into_response()
        }
        Ok(db::DeleteOutcome::NotFound) => {
            let _ = tx.rollback().await;

            StatusCode::NOT_FOUND.into_response()
        }
        Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
    }
}

pub(crate) fn routes() -> Router<AppState> {
    Router::new()
        .route(
            "/api/admin/authorities",
            get(list_authorities).post(create_authority),
        )
        .route(
            "/api/admin/authorities/{id}",
            axum::routing::patch(update_authority).delete(delete_authority),
        )
}
