//! Registry of flexible field definitions.

use domain::{
    AuditAction, AuditActor, AuthorityKind, FieldDefinition, FieldDefinitionId, FieldType,
    LocalizedLabel, NewAuditEvent, NewFieldDefinition, VocabularyId,
};
use sqlx::Row;

use crate::audit;

const FIELD_DEFINITION_ENTITY_TYPE: &str = "field_definition";

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

const SELECT_COLUMNS: &str =
    "fd.id, fd.key, fd.data_type, fd.vocabulary_id, fd.authority_kind, fd.required, fd.group_key";

/// Create a field definition and its labels. Multiple statements — pass a
/// transaction connection (`&mut *tx`) for atomicity.
pub async fn create_field_definition(
    conn: &mut sqlx::PgConnection,
    new: &NewFieldDefinition,
) -> Result<FieldDefinitionId, sqlx::Error> {
    let id = FieldDefinitionId::new();
    let (data_type, vocabulary_id, authority_kind) = new.field_type.to_parts();

    sqlx::query(
        "INSERT INTO field_definition \
         (id, key, data_type, vocabulary_id, authority_kind, required, group_key) \
         VALUES ($1, $2, $3, $4, $5, $6, $7)",
    )
    .bind(id.to_uuid())
    .bind(&new.key)
    .bind(data_type)
    .bind(vocabulary_id.map(|v| v.to_uuid()))
    .bind(authority_kind.map(|k| k.as_str()))
    .bind(new.required)
    .bind(new.group_key.as_deref())
    .execute(&mut *conn)
    .await?;

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

    Ok(id)
}

/// Look up a field definition by its key (with labels).
pub async fn field_definition_by_key<'e, E>(
    executor: E,
    key: &str,
) -> Result<Option<FieldDefinition>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let sql = format!(
        "SELECT {SELECT_COLUMNS}, {LABELS_JSON} AS labels \
         FROM field_definition fd \
         LEFT JOIN field_definition_label fdl ON fdl.field_definition_id = fd.id \
         WHERE fd.key = $1 GROUP BY fd.id"
    );

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

    row.map(map_field_definition).transpose()
}

/// List all field definitions (with labels), ordered by key.
pub async fn list_field_definitions<'e, E>(executor: E) -> Result<Vec<FieldDefinition>, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    let sql = format!(
        "SELECT {SELECT_COLUMNS}, {LABELS_JSON} AS labels \
         FROM field_definition fd \
         LEFT JOIN field_definition_label fdl ON fdl.field_definition_id = fd.id \
         GROUP BY fd.id ORDER BY fd.key"
    );

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

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

fn map_field_definition(row: sqlx::postgres::PgRow) -> Result<FieldDefinition, sqlx::Error> {
    let data_type: String = row.try_get("data_type")?;
    let vocabulary_id: Option<uuid::Uuid> = row.try_get("vocabulary_id")?;
    let authority_kind: Option<String> = row.try_get("authority_kind")?;

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

    let field_type = FieldType::from_parts(
        &data_type,
        vocabulary_id.map(VocabularyId::from_uuid),
        authority_kind,
    )
    .ok_or_else(|| {
        sqlx::Error::Decode(format!("inconsistent field type stored: {data_type}").into())
    })?;

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

    Ok(FieldDefinition {
        id: FieldDefinitionId::from_uuid(row.try_get("id")?),
        key: row.try_get("key")?,
        field_type,
        required: row.try_get("required")?,
        group_key: row.try_get("group_key")?,
        labels: labels.0,
    })
}

/// Update a field definition's mutable attributes (`required`, `group_key`, labels);
/// `key`, `data_type`, and binding are immutable and untouched. Records an `updated`
/// audit entry. Returns `false` if no such key. Pass a transaction connection.
pub async fn update_field_definition(
    conn: &mut sqlx::PgConnection,
    actor: AuditActor,
    key: &str,
    required: bool,
    group_key: Option<&str>,
    labels: &[LocalizedLabel],
) -> Result<bool, sqlx::Error> {
    let id: Option<uuid::Uuid> =
        sqlx::query_scalar("SELECT id FROM field_definition WHERE key = $1")
            .bind(key)
            .fetch_optional(&mut *conn)
            .await?;

    let Some(id) = id else { return Ok(false) };

    sqlx::query("UPDATE field_definition SET required = $2, group_key = $3 WHERE id = $1")
        .bind(id)
        .bind(required)
        .bind(group_key)
        .execute(&mut *conn)
        .await?;

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

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

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

    Ok(true)
}

/// Count catalogue objects that store a value under field `key`.
pub async fn count_objects_using_field<'e, E>(executor: E, key: &str) -> Result<i64, sqlx::Error>
where
    E: sqlx::PgExecutor<'e>,
{
    sqlx::query_scalar("SELECT count(*) FROM object WHERE jsonb_exists(fields, $1)")
        .bind(key)
        .fetch_one(executor)
        .await
}

/// Delete a field definition (labels cascade) unless catalogue objects use its key,
/// recording a `deleted` audit entry. Pass a transaction connection.
pub async fn delete_field_definition(
    conn: &mut sqlx::PgConnection,
    actor: AuditActor,
    key: &str,
) -> Result<crate::DeleteOutcome, sqlx::Error> {
    let id: Option<uuid::Uuid> =
        sqlx::query_scalar("SELECT id FROM field_definition WHERE key = $1")
            .bind(key)
            .fetch_optional(&mut *conn)
            .await?;

    let Some(id) = id else {
        return Ok(crate::DeleteOutcome::NotFound);
    };

    let count = count_objects_using_field(&mut *conn, key).await?;

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

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

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

    Ok(crate::DeleteOutcome::Deleted)
}
