//! Authority records (person / organisation / place).

use domain::{
    AuditAction, AuditActor, Authority, AuthorityId, AuthorityKind, AuthorityRef, LocalizedLabel,
    NewAuditEvent, NewAuthority,
};
use sqlx::Row;

use crate::audit;

const AUTHORITY_ENTITY_TYPE: &str = "authority";

/// Labels aggregated per row as JSON, to read an authority and its labels in one query.
const LABELS_JSON: &str = "COALESCE(json_agg(json_build_object('lang', al.lang, 'label', al.label) \
     ORDER BY al.lang) FILTER (WHERE al.authority_id IS NOT NULL), '[]'::json)";

/// Insert an authority and its labels, then record a `created` audit entry. Multiple
/// statements — pass a transaction connection (`&mut *tx`) so everything commits
/// atomically.
pub async fn create_authority(
    conn: &mut sqlx::PgConnection,
    actor: AuditActor,
    new: &NewAuthority,
) -> Result<AuthorityId, sqlx::Error> {
    let id = AuthorityId::new();

    sqlx::query("INSERT INTO authority (id, kind, external_uri) VALUES ($1, $2, $3)")
        .bind(id.to_uuid())
        .bind(new.kind.as_str())
        .bind(new.external_uri.as_deref())
        .execute(&mut *conn)
        .await?;

    for label in &new.labels {
        sqlx::query("INSERT INTO authority_label (authority_id, lang, label) VALUES ($1, $2, $3)")
            .bind(id.to_uuid())
            .bind(&label.lang)
            .bind(&label.label)
            .execute(&mut *conn)
            .await?;
    }

    audit::record(
        &mut *conn,
        &NewAuditEvent {
            actor,
            action: AuditAction::Created,
            entity_type: AUTHORITY_ENTITY_TYPE.to_owned(),
            entity_id: id.to_uuid(),
            changes: Vec::new(),
        },
    )
    .await?;

    Ok(id)
}

/// Fetch one authority (with its labels).
pub async fn authority_by_id<'e, E>(
    executor: E,
    id: AuthorityId,
) -> Result<Option<Authority>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let sql = format!(
        "SELECT a.id, a.kind, a.external_uri, {LABELS_JSON} AS labels \
         FROM authority a LEFT JOIN authority_label al ON al.authority_id = a.id \
         WHERE a.id = $1 GROUP BY a.id"
    );

    let row = sqlx::query(&sql)
        .bind(id.to_uuid())
        .fetch_optional(executor)
        .await?;

    row.map(map_authority).transpose()
}

/// List authorities of a given kind (with labels), ordered by id.
pub async fn list_by_kind<'e, E>(
    executor: E,
    kind: AuthorityKind,
) -> Result<Vec<Authority>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let sql = format!(
        "SELECT a.id, a.kind, a.external_uri, {LABELS_JSON} AS labels \
         FROM authority a LEFT JOIN authority_label al ON al.authority_id = a.id \
         WHERE a.kind = $1 GROUP BY a.id ORDER BY a.id"
    );

    let rows = sqlx::query(&sql)
        .bind(kind.as_str())
        .fetch_all(executor)
        .await?;

    rows.into_iter().map(map_authority).collect()
}

/// Resolve an authority to an [`AuthorityRef`] (carrying its kind).
pub async fn resolve_authority<'e, E>(
    executor: E,
    id: AuthorityId,
) -> Result<Option<AuthorityRef>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let kind: Option<String> = sqlx::query_scalar("SELECT kind FROM authority WHERE id = $1")
        .bind(id.to_uuid())
        .fetch_optional(executor)
        .await?;

    match kind {
        Some(k) => {
            let kind = AuthorityKind::from_db(&k).ok_or_else(|| {
                sqlx::Error::Decode(format!("unknown authority kind: {k}").into())
            })?;

            Ok(Some(AuthorityRef::new(id, kind)))
        }
        None => Ok(None),
    }
}

/// Update an authority's `external_uri` and labels (full replace), recording an
/// `updated` audit entry. Returns `false` if no such authority. `kind` is immutable.
pub async fn update_authority(
    conn: &mut sqlx::PgConnection,
    actor: AuditActor,
    id: AuthorityId,
    external_uri: Option<&str>,
    labels: &[LocalizedLabel],
) -> Result<bool, sqlx::Error> {
    let updated = sqlx::query("UPDATE authority SET external_uri = $2 WHERE id = $1")
        .bind(id.to_uuid())
        .bind(external_uri)
        .execute(&mut *conn)
        .await?
        .rows_affected();

    if updated == 0 {
        return Ok(false);
    }

    sqlx::query("DELETE FROM authority_label WHERE authority_id = $1")
        .bind(id.to_uuid())
        .execute(&mut *conn)
        .await?;

    for label in labels {
        sqlx::query("INSERT INTO authority_label (authority_id, lang, label) VALUES ($1, $2, $3)")
            .bind(id.to_uuid())
            .bind(&label.lang)
            .bind(&label.label)
            .execute(&mut *conn)
            .await?;
    }

    audit::record(
        &mut *conn,
        &NewAuditEvent {
            actor,
            action: AuditAction::Updated,
            entity_type: AUTHORITY_ENTITY_TYPE.to_owned(),
            entity_id: id.to_uuid(),
            changes: Vec::new(),
        },
    )
    .await?;

    Ok(true)
}

/// Count catalogue objects referencing `id` through an `authority`-typed field.
pub async fn count_objects_referencing_authority<'e, E>(
    executor: E,
    id: AuthorityId,
) -> Result<i64, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    sqlx::query_scalar(
        "SELECT count(*) FROM object o WHERE EXISTS ( \
           SELECT 1 FROM field_definition fd \
           WHERE fd.data_type = 'authority' AND o.fields ->> fd.key = $1 )",
    )
    .bind(id.to_string())
    .fetch_one(executor)
    .await
}

/// Delete an authority (labels cascade) unless catalogue objects reference it,
/// recording a `deleted` audit entry.
pub async fn delete_authority(
    conn: &mut sqlx::PgConnection,
    actor: AuditActor,
    id: AuthorityId,
) -> Result<crate::DeleteOutcome, sqlx::Error> {
    let exists = sqlx::query_scalar::<_, i32>("SELECT 1 FROM authority WHERE id = $1")
        .bind(id.to_uuid())
        .fetch_optional(&mut *conn)
        .await?;

    if exists.is_none() {
        return Ok(crate::DeleteOutcome::NotFound);
    }

    let count = count_objects_referencing_authority(&mut *conn, id).await?;

    if count > 0 {
        return Ok(crate::DeleteOutcome::InUse { count });
    }

    sqlx::query("DELETE FROM authority WHERE id = $1")
        .bind(id.to_uuid())
        .execute(&mut *conn)
        .await?;

    audit::record(
        &mut *conn,
        &NewAuditEvent {
            actor,
            action: AuditAction::Deleted,
            entity_type: AUTHORITY_ENTITY_TYPE.to_owned(),
            entity_id: id.to_uuid(),
            changes: Vec::new(),
        },
    )
    .await?;

    Ok(crate::DeleteOutcome::Deleted)
}

fn map_authority(row: sqlx::postgres::PgRow) -> Result<Authority, sqlx::Error> {
    let kind_str: String = row.try_get("kind")?;
    let kind = AuthorityKind::from_db(&kind_str)
        .ok_or_else(|| sqlx::Error::Decode(format!("unknown authority kind: {kind_str}").into()))?;

    let labels: sqlx::types::Json<Vec<LocalizedLabel>> = row.try_get("labels")?;

    Ok(Authority {
        id: AuthorityId::from_uuid(row.try_get("id")?),
        kind,
        external_uri: row.try_get("external_uri")?,
        labels: labels.0,
    })
}
