use mongod::Model;
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::transaction::{Price, Transaction};

pub fn sort_by_timestamp() -> mongodb::bson::Document {
    doc! { "timestamp": mongod::Sort::Descending }
}

pub fn timestamp_range(year: i32, month: u32) -> (i64, i64) {
    let d = chrono::NaiveDate::from_ymd_opt(year, month, 0).unwrap();
    let t = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap();
    let start = chrono::NaiveDateTime::new(d, t).and_utc().timestamp();

    assert!(month <= 12);

    let end = if month == 12 {
        let d = chrono::NaiveDate::from_ymd_opt(year + 1, month, 0).unwrap();
        let t = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap();
        chrono::NaiveDateTime::new(d, t).and_utc().timestamp()
    } else {
        let d = chrono::NaiveDate::from_ymd_opt(year, month + 1, 0).unwrap();
        let t = chrono::NaiveTime::from_hms_milli_opt(0, 0, 0, 0).unwrap();
        chrono::NaiveDateTime::new(d, t).and_utc().timestamp()
    };

    (start, end)
}

/// Represents a specific instance of an item with potential variations.
///
/// This struct is used to describe a particular variation or instance of an item
/// in the real world. It may include attributes or properties that deviate from
/// the standard definition of the item. For example, different colors, sizes, or
/// configurations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Variant {
    /// Associated Item
    pub item: String,
    /// Variant ID
    pub variant: String,
    /// Variant Name
    pub name: String,
    /// Minimum amount
    pub min: Option<i64>,
    /// Days until expiry
    pub expiry: Option<i64>,
}

impl Variant {
    /// Create variant from itemdb yaml
    pub fn from_yml(json: &serde_yaml::Value, variant: &str, item: &str) -> Self {
        Self {
            item: item.to_string(),
            variant: variant.to_string(),
            name: json
                .as_mapping()
                .unwrap()
                .get("name")
                .unwrap()
                .as_str()
                .unwrap()
                .to_string(),
            min: json
                .as_mapping()
                .unwrap()
                .get("min")
                .map(|x| x.as_i64().unwrap()),
            expiry: json
                .as_mapping()
                .unwrap()
                .get("expiry")
                .map(|x| x.as_i64().unwrap()),
        }
    }

    pub async fn supply_log(&self) -> Vec<String> {
        let filter = doc! {
            "item": &self.item,
            "variant": &self.variant
        };

        let result = Transaction::find_partial(filter, json!({}), None, None)
            .await
            .unwrap();

        let mut ret = Vec::new();

        for doc in result {
            ret.push(doc._id);
        }

        ret
    }

    pub async fn inventory(&self) -> Vec<Transaction> {
        let filter = doc! {
            "item": &self.item,
            "variant": &self.variant,
            "consumed": { "$not": { "$type": "object" } }
        };

        Transaction::find(filter, None, None).await.unwrap()
    }

    pub async fn demand_log(&self) -> Vec<String> {
        let filter = doc! {
            "item": &self.item,
            "variant": &self.variant,
            "consumed": { "$type": "object" }
        };

        let result = Transaction::find_partial(filter, json!({}), None, None)
            .await
            .unwrap();

        let mut ret = Vec::new();

        for doc in result {
            ret.push(doc._id);
        }

        ret
    }

    pub async fn demand(uuid: &str, price: Price, destination: &str) -> Option<()> {
        // check if transaction exists
        let mut t = Transaction::get(uuid).await?;
        t.consume(price, destination).await;
        Some(())
    }

    /// Records a supply transaction in the database.
    ///
    /// # Arguments
    ///
    /// * `price` - The price of the supplied items.
    /// * `origin` - The origin or source of the supplied items.
    ///
    /// # Returns
    ///
    /// Returns a UUID string representing the transaction.
    pub async fn supply(
        &self,
        price: Price,
        origin: Option<&str>,
        location: Option<&str>,
    ) -> String {
        let t = Transaction::new(&self.item, &self.variant, price, origin, location).await;

        t.insert().await.unwrap();

        t._id
    }

    pub async fn get_all_transactions(&self) -> Vec<Transaction> {
        let filter = doc! {
            "item": &self.item,
            "variant": &self.variant
        };

        Transaction::find(filter, None, None).await.unwrap()
    }

    pub async fn get_transaction_timeslice(&self, year: i32, month: u32) -> Vec<Transaction> {
        let (start, end) = timestamp_range(year, month);

        Transaction::find(
            doc! {
                "timestamp": {
                    "$gte": start,
                    "$lte": end
                }
            },
            None,
            Some(sort_by_timestamp()),
        )
        .await
        .unwrap()
    }

    pub async fn get_unique_origins(&self) -> Vec<String> {
        Transaction::unique(
            doc! {
                "item": &self.item,
                "variant": &self.variant
            },
            "origin",
        )
        .await
    }

    pub async fn get_unique_destinations(&self) -> Vec<String> {
        Transaction::unique(
            doc! {
                "item": &self.item,
                "variant": &self.variant
            },
            "consumed.destination",
        )
        .await
    }

    pub async fn price_history_by_origin(&self, origin: &str) -> Vec<Price> {
        Transaction::find(
            doc! {
                "item": &self.item,
                "variant": &self.variant,
                "origin": origin
            },
            None,
            Some(sort_by_timestamp()),
        )
        .await
        .unwrap()
        .into_iter()
        .map(|x| x.price)
        .collect()
    }

    pub async fn get_latest_price(&self, origin: Option<String>) -> Price {
        let mut filter = doc! {
            "item": &self.item,
            "variant": &self.variant
        };

        if let Some(origin) = origin {
            filter.insert("origin", origin);
        }

        Transaction::find(filter, Some(1), Some(sort_by_timestamp()))
            .await
            .unwrap()
            .first()
            .unwrap()
            .price
            .clone()
    }

    pub async fn stat(&self) -> serde_json::Value {
        let active_transactions = self.inventory().await;

        // fix : ignores currency
        let total_price: f64 = active_transactions.iter().map(|x| x.price.value).sum();

        json!({
            "amount": active_transactions.len(),
            "total_price": total_price
        })
    }

    pub fn api_json(&self) -> serde_json::Value {
        json!({
            "item": self.item,
            "variant": self.variant,
            "name": self.name,
            "min": self.min,
            "expiry": self.expiry
        })
    }
}