Documentation
use chrono::{DateTime, Utc};
use lsor_core::{
    column::{col, ColumnName},
    driver::{Driver, PushPrql},
    filter::Filterable,
    row::{upsert_into, IsPk, Row},
    sort::{Order, Sorting},
    table::table,
};
use sqlx::{postgres::PgRow, FromRow, Type};
use uuid::Uuid;

pub struct Metadata {
    pub name: Option<String>,
    pub description: Option<String>,
    pub tags: Vec<String>,
}

pub enum MetadataFilter {
    Name(<Option<String> as Filterable>::Filter),
    Description(<Option<String> as Filterable>::Filter),
    Tags(<Vec<String> as Filterable>::Filter),
}

pub enum MetadataSort {
    Name(Order),
    Description(Order),
}

impl PushPrql for MetadataSort {
    fn push_to_driver(&self, driver: &mut Driver) {
        match self {
            Self::Name(_) => {
                col("name").push_to_driver(driver);
            }
            Self::Description(_) => {
                col("description").push_to_driver(driver);
            }
        }
    }
}

impl Sorting for MetadataSort {
    fn order(&self) -> Order {
        match self {
            Self::Name(order) => *order,
            Self::Description(order) => *order,
        }
    }

    fn flip(&self) -> impl Sorting {
        match self {
            Self::Name(order) => Self::Name(order.flip()),
            Self::Description(order) => Self::Description(order.flip()),
        }
    }

    fn push_to_driver_with_order(&self, driver: &mut Driver) {
        match self {
            Self::Name(order) => {
                order.push_to_driver(driver);
                col("name").push_to_driver(driver);
            }
            Self::Description(order) => {
                order.push_to_driver(driver);
                col("description").push_to_driver(driver);
            }
        }
    }
}

impl Row for Metadata {
    fn column_names() -> impl Iterator<Item = (ColumnName, IsPk)> {
        (Some((col(stringify!(name)), false)).into_iter())
            .chain(Some((col(stringify!(description)), false)).into_iter())
            .chain(Some((col(stringify!(tags)), false)).into_iter())
    }

    fn push_column_values(&self, driver: &mut Driver) {
        self.name.push_to_driver(driver);
        driver.push(", ");
        self.description.push_to_driver(driver);
        driver.push(", ");
        self.tags.push_to_driver(driver);
    }
}

impl<'r> FromRow<'r, PgRow> for Metadata {
    fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
        use sqlx::Row;

        Ok(Self {
            name: row.try_get("name")?,
            description: row.try_get("description")?,
            tags: row.try_get("tags")?,
        })
    }
}

impl Filterable for Metadata {
    type Filter = MetadataFilter;
}

#[derive(Type)]
pub enum AccountTier {
    Free,
    Pro,
    Startup,
    Enterprise,
}

impl PushPrql for AccountTier {
    fn push_to_driver(&self, driver: &mut Driver) {
        driver.push_bind(self);
    }
}

pub enum AccountTierFilter {
    Eq(AccountTier),
    Ne(AccountTier),
}

impl Filterable for AccountTier {
    type Filter = AccountTierFilter;
}

pub struct Account {
    pub id: Uuid,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
    pub tier: AccountTier,
    pub metadata: Metadata,
}

pub struct AccountFilter {
    pub id: <Uuid as Filterable>::Filter,
    pub created_at: <DateTime<Utc> as Filterable>::Filter,
    pub updated_at: <DateTime<Utc> as Filterable>::Filter,
    pub deleted_at: <Option<DateTime<Utc>> as Filterable>::Filter,
    pub tier: <AccountTier as Filterable>::Filter,
    pub metadata: <Metadata as Filterable>::Filter,
}

impl Row for Account {
    fn column_names() -> impl Iterator<Item = (ColumnName, IsPk)> {
        (Some((col(stringify!(id)), true)).into_iter())
            .chain(Some((col(stringify!(created_at)), false)).into_iter())
            .chain(Some((col(stringify!(updated_at)), false)).into_iter())
            .chain(Some((col(stringify!(deleted_at)), false)).into_iter())
            .chain(Some((col(stringify!(tier)), false)).into_iter())
            .chain(Metadata::column_names())
    }

    fn push_column_values(&self, driver: &mut Driver) {
        self.id.push_to_driver(driver);
        driver.push(", ");
        self.created_at.push_to_driver(driver);
        driver.push(", ");
        self.updated_at.push_to_driver(driver);
        driver.push(", ");
        self.deleted_at.push_to_driver(driver);
        driver.push(", ");
        self.tier.push_to_driver(driver);
        driver.push(", ");
        self.metadata.push_column_values(driver);
    }
}

impl Row for &Account {
    fn column_names() -> impl Iterator<Item = (ColumnName, IsPk)> {
        Account::column_names()
    }

    fn push_column_values(&self, driver: &mut Driver) {
        (*self).push_column_values(driver);
    }
}

impl<'r> FromRow<'r, PgRow> for Account {
    fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
        use sqlx::Row;

        Ok(Self {
            id: row.try_get("id")?,
            created_at: row.try_get("created_at")?,
            updated_at: row.try_get("updated_at")?,
            deleted_at: row.try_get("deleted_at")?,
            tier: row.try_get("tier")?,
            metadata: Metadata::from_row(row)?,
        })
    }
}

impl Filterable for Account {
    type Filter = AccountFilter;
}

#[test]
fn test_column_names() {
    let column_names = Account::column_names().collect::<Vec<_>>();
    assert_eq!(
        column_names,
        vec![
            (col("id"), true),
            (col("created_at"), false),
            (col("updated_at"), false),
            (col("deleted_at"), false),
            (col("tier"), false),
            (col("name"), false),
            (col("description"), false),
            (col("tags"), false),
        ]
    );
}

#[test]
fn test_column_values() {
    let account = Account {
        id: Uuid::new_v4(),
        created_at: Utc::now(),
        updated_at: Utc::now(),
        deleted_at: None,
        tier: AccountTier::Free,
        metadata: Metadata {
            name: Some("name".to_string()),
            description: Some("description".to_string()),
            tags: vec!["tag".to_string()],
        },
    };
    let mut driver = Driver::new();
    account.push_column_values(&mut driver);
    assert_eq!(driver.prql(), "$1, $2, $3, $4, $5, $6, $7, $8");
}

#[test]
fn test_upsert() {
    let account = Account {
        id: Uuid::new_v4(),
        created_at: Utc::now(),
        updated_at: Utc::now(),
        deleted_at: None,
        tier: AccountTier::Free,
        metadata: Metadata {
            name: Some("name".to_string()),
            description: Some("description".to_string()),
            tags: vec!["tag".to_string()],
        },
    };
    let mut driver = Driver::new();
    upsert_into(table("accounts"), &account).push_to_driver(&mut driver);
    assert_eq!(driver.prql(), "INSERT INTO accounts (id, created_at, updated_at, deleted_at, tier, name, description, tags) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO UPDATE SET (created_at, updated_at, deleted_at, tier, name, description, tags) = ($2, $3, $4, $5, $6, $7, $8)");
}