use crate::cents::Cents;
use chrono::{DateTime, Utc};
use serde_json::Value as JsonValue;
use uuid::Uuid;

#[derive(Debug, Clone, Copy, PartialEq, sqlx::Type)]
#[sqlx(rename = "order_state")]
pub enum OrderState {
    Created,
    Pending,
    Payed,
    Failed,
    Canceled,
}

#[derive(Debug, PartialEq, Clone, sqlx::FromRow)]
pub struct Order {
    pub id: i64,
    pub uuid: Uuid,
    pub origin: String,
    pub locale: String,
    pub state: OrderState,
    pub custom_status: String,
    pub date: DateTime<Utc>,
    pub payment_method: String,
    pub payment_id: String,
    pub payment_meta: JsonValue,
    pub currency: String,
    pub shipping_method: String,
    pub shipping_method_name: String,
    pub coupon: Option<String>,
    pub shipping: Cents,
    pub tax: Cents,
    pub discount: Cents,
    pub total: Cents,
    pub email: String,
    pub token: String,
    pub invoice_address_id: i64,
    pub shipping_address_id: i64,
    pub updated_at: DateTime<Utc>,
}

pub async fn fetch<'e>(
    id: i64,
    conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<Order>, sqlx::Error> {
    sqlx::query_as("SELECT * FROM orders WHERE id = $1")
        .bind(id)
        .fetch_optional(conn)
        .await
}

pub async fn fetch_for_update<'e>(
    id: i64,
    conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<Order>, sqlx::Error> {
    sqlx::query_as("SELECT * FROM orders WHERE id = $1 FOR UPDATE")
        .bind(id)
        .fetch_optional(conn)
        .await
}

pub async fn by_payment_for_update<'e>(
    payment_method: &str,
    payment_id: &str,
    conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<Order>, sqlx::Error> {
    sqlx::query_as("SELECT * FROM orders WHERE payment_method = $1 AND payment_id = $2 FOR UPDATE")
        .bind(payment_method)
        .bind(payment_id)
        .fetch_optional(conn)
        .await
}

pub async fn by_uuid<'e>(
    uuid: uuid::Uuid,
    conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<Order>, sqlx::Error> {
    sqlx::query_as("SELECT * FROM orders WHERE uuid = $1")
        .bind(uuid)
        .fetch_optional(conn)
        .await
}

#[derive(Debug)]
pub struct NewOrder<'a> {
    pub uuid: Uuid,
    pub origin: &'a str,
    pub locale: &'a str,
    pub state: OrderState,
    pub custom_status: &'a str,
    pub date: DateTime<Utc>,
    pub payment_method: &'a str,
    pub payment_id: &'a str,
    pub payment_meta: &'a JsonValue,
    pub currency: &'a str,
    pub shipping_method: &'a str,
    pub shipping_method_name: &'a str,
    pub coupon: Option<&'a str>,
    pub shipping: Cents,
    pub tax: Cents,
    pub discount: Cents,
    pub total: Cents,
    pub email: &'a str,
    pub token: &'a str,
    pub invoice_address_id: i64,
    pub shipping_address_id: i64,
}

pub async fn insert<'e>(
    order: &NewOrder<'_>,
    conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Order, sqlx::Error> {
    // deconstruct to not miss new properties in the future
    let NewOrder {
        uuid,
        origin,
        locale,
        state,
        custom_status,
        date,
        payment_method,
        payment_id,
        payment_meta,
        currency,
        shipping_method,
        shipping_method_name,
        coupon,
        shipping,
        tax,
        discount,
        total,
        email,
        token,
        invoice_address_id,
        shipping_address_id,
    } = order;

    sqlx::query_as(
        "
            INSERT INTO orders (
                uuid,
                origin,
                locale,
                state,
                custom_status,
                date,
                payment_method,
                payment_id,
                payment_meta,
                currency,
                shipping_method,
                shipping_method_name,
                coupon,
                shipping,
                tax,
                discount,
                total,
                email,
                token,
                invoice_address_id,
                shipping_address_id
            ) VALUES (
                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,
                $19, $20, $21
            )
            ON CONFLICT(payment_method, payment_id) DO NOTHING
            RETURNING *
        ",
    )
    .bind(uuid)
    .bind(origin)
    .bind(locale)
    .bind(state)
    .bind(custom_status)
    .bind(date)
    .bind(payment_method)
    .bind(payment_id)
    .bind(payment_meta)
    .bind(currency)
    .bind(shipping_method)
    .bind(shipping_method_name)
    .bind(coupon)
    .bind(shipping)
    .bind(tax)
    .bind(discount)
    .bind(total)
    .bind(email)
    .bind(token)
    .bind(invoice_address_id)
    .bind(shipping_address_id)
    .fetch_one(conn)
    .await
}

impl Order {
    pub async fn update_state(
        &mut self,
        new_state: OrderState,
        new_custom_status: &str,
        conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE orders SET state = $2, custom_status = $3 WHERE id = $1")
            .bind(self.id)
            .bind(new_state)
            .bind(new_custom_status)
            .execute(conn)
            .await?;

        self.state = new_state;
        self.custom_status = new_custom_status.to_string();
        Ok(())
    }

    pub async fn update_payment(
        &mut self,
        new_payment_id: &str,
        new_payment_meta: JsonValue,
        conn: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
    ) -> Result<(), sqlx::Error> {
        sqlx::query("UPDATE orders SET payment_id = $2, payment_meta = $3 WHERE id = $1")
            .bind(self.id)
            .bind(new_payment_id)
            .bind(&new_payment_meta)
            .execute(conn)
            .await?;
        self.payment_id = new_payment_id.to_string();
        self.payment_meta = new_payment_meta;
        Ok(())
    }
}
