//! Shared SQL rendering for the `set_metadata_option` catalog mutation.
//!
//! All catalog backends implement the same semantic operation: delete any
//! existing option row matching the key case-insensitively within the given
//! scope, then insert the replacement row with a lowercased key. The backends
//! differ only in placeholder style, null-safe equality form, table
//! qualification, and whether values are bound as parameters or rendered as
//! SQL literals (Quack). This module renders the two statements; binding and
//! execution stay in each backend.

#[cfg(feature = "quack")]
use crate::MetadataOptionScope;

pub(crate) enum MetadataPlaceholderStyle {
    /// `?1`, `?2`, ... (SQLite, DuckDB-file).
    #[cfg(any(feature = "sqlite", feature = "duckdb"))]
    Numbered,
    /// `$1`, `$2`, ... (Postgres).
    #[cfg(feature = "postgres")]
    Dollar,
}

pub(crate) enum NullSafeEquality {
    /// `((col IS NULL AND ? IS NULL) OR col = ?)` (SQLite, DuckDB-file).
    #[cfg(any(feature = "sqlite", feature = "duckdb"))]
    OrIsNull,
    /// `col IS NOT DISTINCT FROM ?` (Postgres, Quack).
    #[cfg(any(feature = "postgres", feature = "quack"))]
    IsNotDistinctFrom,
}

pub(crate) struct MetadataOptionSql {
    pub(crate) delete: String,
    pub(crate) insert: String,
}

/// Insert-side key normalization shared by all backends.
pub(crate) fn normalized_metadata_option_key(key: &str) -> String {
    key.to_ascii_lowercase()
}

/// Parameterized statements for backends that bind values.
///
/// Binding contract: `delete` binds `(key, scope, scope_id)` as parameters
/// 1..=3; `insert` binds `(normalized_key, value, scope, scope_id)` as
/// parameters 1..=4.
#[cfg(any(feature = "sqlite", feature = "duckdb", feature = "postgres"))]
pub(crate) fn render_set_metadata_option(
    table: &str,
    placeholders: MetadataPlaceholderStyle,
    null_eq: NullSafeEquality,
) -> MetadataOptionSql {
    let p: Vec<String> = (1..=4)
        .map(|index| match placeholders {
            #[cfg(any(feature = "sqlite", feature = "duckdb"))]
            MetadataPlaceholderStyle::Numbered => format!("?{index}"),
            #[cfg(feature = "postgres")]
            MetadataPlaceholderStyle::Dollar => format!("${index}"),
        })
        .collect();
    render(
        table,
        [&p[0], &p[1], &p[2]],
        [&p[0], &p[1], &p[2], &p[3]],
        null_eq,
    )
}

/// Fully rendered literal statements for backends without placeholder support
/// (Quack). String values are escaped via [`crate::catalog_ddl::sql_string`];
/// absent scope fields render as `NULL`.
#[cfg(feature = "quack")]
pub(crate) fn render_set_metadata_option_literals(
    key: &str,
    value: &str,
    scope: &MetadataOptionScope,
) -> MetadataOptionSql {
    let key_lit = crate::catalog_ddl::sql_string(key);
    let key_insert_lit = crate::catalog_ddl::sql_string(&normalized_metadata_option_key(key));
    let value_lit = crate::catalog_ddl::sql_string(value);
    let scope_lit = scope
        .scope
        .as_deref()
        .map(crate::catalog_ddl::sql_string)
        .unwrap_or_else(|| "NULL".to_string());
    let scope_id_lit = scope
        .scope_id
        .map(|scope_id| scope_id.to_string())
        .unwrap_or_else(|| "NULL".to_string());
    render(
        "ducklake_metadata",
        [&key_lit, &scope_lit, &scope_id_lit],
        [&key_insert_lit, &value_lit, &scope_lit, &scope_id_lit],
        NullSafeEquality::IsNotDistinctFrom,
    )
}

fn render(
    table: &str,
    delete_operands: [&str; 3],
    insert_operands: [&str; 4],
    null_eq: NullSafeEquality,
) -> MetadataOptionSql {
    let [key, scope, scope_id] = delete_operands;
    let delete = format!(
        "DELETE FROM {table} WHERE lower(key) = lower({key}) AND {} AND {}",
        null_safe_predicate("scope", scope, &null_eq),
        null_safe_predicate("scope_id", scope_id, &null_eq),
    );
    let [key, value, scope, scope_id] = insert_operands;
    let insert = format!(
        "INSERT INTO {table}(key, value, scope, scope_id) VALUES ({key}, {value}, {scope}, {scope_id})"
    );
    MetadataOptionSql { delete, insert }
}

fn null_safe_predicate(column: &str, operand: &str, null_eq: &NullSafeEquality) -> String {
    match null_eq {
        #[cfg(any(feature = "sqlite", feature = "duckdb"))]
        NullSafeEquality::OrIsNull => {
            format!("(({column} IS NULL AND {operand} IS NULL) OR {column} = {operand})")
        }
        #[cfg(any(feature = "postgres", feature = "quack"))]
        NullSafeEquality::IsNotDistinctFrom => {
            format!("{column} IS NOT DISTINCT FROM {operand}")
        }
    }
}

#[cfg(test)]
#[path = "option/tests.rs"]
mod tests;
