use bigdecimal::BigDecimal;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder};
use uuid::Uuid;

/// Enum representing all supported literal types, providing convenience
/// methods for working with them in [`sqlx`] queries, and defining a [`serde`]
/// encoding for use across the application stack.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "t", content = "c")]
pub enum Datum {
    // BigDecimal is used because a user may insert a value directly via SQL
    // which overflows the representational space of `rust_decimal::Decimal`.
    // Note that by default, [`BigDecimal`] serializes to JSON as a string. This
    // behavior can be modified, but it's a pain when paired with the [`Option`]
    // type. String representation should be acceptable for the UI, as [`Datum`]
    // values should always be parsed through Zod, which can coerce the value to
    // a number transparently.
    Numeric(Option<BigDecimal>),
    Text(Option<String>),
    Timestamp(Option<DateTime<Utc>>),
    Uuid(Option<Uuid>),
}

// TODO: Should sqlx helpers be moved to a separate crate?
impl Datum {
    // TODO: Can something similar be achieved with a generic return type?
    /// Bind this as a parameter to a sqlx query.
    pub fn bind_onto<'a>(
        self,
        query: sqlx::query::Query<'a, Postgres, <sqlx::Postgres as sqlx::Database>::Arguments<'a>>,
    ) -> sqlx::query::Query<'a, Postgres, <sqlx::Postgres as sqlx::Database>::Arguments<'a>> {
        match self {
            Self::Numeric(value) => query.bind(value),
            Self::Text(value) => query.bind(value),
            Self::Timestamp(value) => query.bind(value),
            Self::Uuid(value) => query.bind(value),
        }
    }

    /// Push this as a parameter to a [`QueryBuilder`].
    pub fn push_bind_onto(self, builder: &mut QueryBuilder<'_, Postgres>) {
        match self {
            Self::Numeric(value) => builder.push_bind(value),
            Self::Text(value) => builder.push_bind(value),
            Self::Timestamp(value) => builder.push_bind(value),
            Self::Uuid(value) => builder.push_bind(value),
        };
    }

    /// Transform the contained value into a serde_json::Value.
    pub fn inner_as_value(&self) -> serde_json::Value {
        let serialized = serde_json::to_value(self).unwrap();
        #[derive(Deserialize)]
        struct Tagged {
            c: serde_json::Value,
        }
        let deserialized: Tagged = serde_json::from_value(serialized).unwrap();
        deserialized.c
    }

    pub fn is_none(&self) -> bool {
        match self {
            Self::Numeric(None) | Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => {
                true
            }
            Self::Numeric(_) | Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false,
        }
    }
}
