use crate::models::ids::{
    ChargeId, ProductId, ProductPriceId, UserSubscriptionId,
};
use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize)]
pub struct Product {
    pub id: ProductId,
    pub metadata: ProductMetadata,
    pub prices: Vec<ProductPrice>,
    pub unitary: bool,
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ProductMetadata {
    Midas,
    Pyro {
        cpu: u32,
        ram: u32,
        swap: u32,
        storage: u32,
    },
    Medal {
        cpu: u32,
        ram: u32,
        swap: u32,
        storage: u32,
        region: String,
    },
}

impl ProductMetadata {
    pub fn is_pyro(&self) -> bool {
        matches!(self, ProductMetadata::Pyro { .. })
    }

    pub fn is_medal(&self) -> bool {
        matches!(self, ProductMetadata::Medal { .. })
    }

    pub fn is_midas(&self) -> bool {
        matches!(self, ProductMetadata::Midas)
    }
}

#[derive(Serialize, Deserialize)]
pub struct ProductPrice {
    pub id: ProductPriceId,
    pub product_id: ProductId,
    pub prices: Price,
    pub currency_code: String,
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum Price {
    OneTime {
        price: i32,
    },
    Recurring {
        intervals: HashMap<PriceDuration, i32>,
    },
}

impl Price {
    pub fn get_interval(&self, interval: PriceDuration) -> Option<i32> {
        match self {
            Price::OneTime { .. } => None,
            Price::Recurring { intervals } => intervals.get(&interval).copied(),
        }
    }
}

#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum PriceDuration {
    FiveDays,
    Monthly,
    Quarterly,
    Yearly,
}

impl PriceDuration {
    pub fn duration(&self) -> chrono::Duration {
        match self {
            PriceDuration::FiveDays => chrono::Duration::days(5),
            PriceDuration::Monthly => chrono::Duration::days(30),
            PriceDuration::Quarterly => chrono::Duration::days(90),
            PriceDuration::Yearly => chrono::Duration::days(365),
        }
    }

    pub fn from_string(string: &str) -> PriceDuration {
        match string {
            "five-days" => PriceDuration::FiveDays,
            "monthly" => PriceDuration::Monthly,
            "quarterly" => PriceDuration::Quarterly,
            "yearly" => PriceDuration::Yearly,
            _ => PriceDuration::Monthly,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            PriceDuration::Monthly => "monthly",
            PriceDuration::Quarterly => "quarterly",
            PriceDuration::Yearly => "yearly",
            PriceDuration::FiveDays => "five-days",
        }
    }

    pub fn iterator() -> impl Iterator<Item = PriceDuration> {
        vec![
            PriceDuration::Monthly,
            PriceDuration::Quarterly,
            PriceDuration::Yearly,
            PriceDuration::FiveDays,
        ]
        .into_iter()
    }
}

#[derive(Serialize, Deserialize)]
pub struct UserSubscription {
    pub id: UserSubscriptionId,
    pub user_id: UserId,
    pub price_id: ProductPriceId,
    pub interval: PriceDuration,
    pub status: SubscriptionStatus,
    pub created: DateTime<Utc>,
    pub metadata: Option<SubscriptionMetadata>,
}

impl From<crate::database::models::user_subscription_item::DBUserSubscription>
    for UserSubscription
{
    fn from(
        x: crate::database::models::user_subscription_item::DBUserSubscription,
    ) -> Self {
        Self {
            id: x.id.into(),
            user_id: x.user_id.into(),
            price_id: x.price_id.into(),
            interval: x.interval,
            status: x.status,
            created: x.created,
            metadata: x.metadata,
        }
    }
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum SubscriptionStatus {
    Provisioned,
    Unprovisioned,
}

impl SubscriptionStatus {
    pub fn from_string(string: &str) -> SubscriptionStatus {
        match string {
            "provisioned" => SubscriptionStatus::Provisioned,
            "unprovisioned" => SubscriptionStatus::Unprovisioned,
            _ => SubscriptionStatus::Provisioned,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            SubscriptionStatus::Provisioned => "provisioned",
            SubscriptionStatus::Unprovisioned => "unprovisioned",
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum SubscriptionMetadata {
    Pyro { id: String, region: Option<String> },
    Medal { id: String },
}

impl SubscriptionMetadata {
    pub fn is_medal(&self) -> bool {
        matches!(self, SubscriptionMetadata::Medal { .. })
    }

    pub fn is_pyro(&self) -> bool {
        matches!(self, SubscriptionMetadata::Pyro { .. })
    }
}

#[derive(Serialize, Deserialize)]
pub struct Charge {
    pub id: ChargeId,
    pub user_id: UserId,
    pub price_id: ProductPriceId,
    pub amount: i64,
    pub currency_code: String,
    pub status: ChargeStatus,
    pub due: DateTime<Utc>,
    pub last_attempt: Option<DateTime<Utc>>,
    #[serde(flatten)]
    pub type_: ChargeType,
    pub subscription_id: Option<UserSubscriptionId>,
    pub subscription_interval: Option<PriceDuration>,
    pub platform: PaymentPlatform,

    pub parent_charge_id: Option<ChargeId>,
    pub net: Option<i64>,
}

#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ChargeType {
    OneTime,
    Subscription,
    Proration,
    Refund,
}

impl ChargeType {
    pub fn as_str(&self) -> &'static str {
        match self {
            ChargeType::OneTime => "one-time",
            ChargeType::Subscription => "subscription",
            ChargeType::Proration => "proration",
            ChargeType::Refund => "refund",
        }
    }

    pub fn from_string(string: &str) -> ChargeType {
        match string {
            "one-time" => ChargeType::OneTime,
            "subscription" => ChargeType::Subscription,
            "proration" => ChargeType::Proration,
            "refund" => ChargeType::Refund,
            _ => ChargeType::OneTime,
        }
    }
}

#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub enum ChargeStatus {
    /// Open charges are for the next billing interval
    Open,
    Processing,
    Succeeded,
    Failed,
    Cancelled,
    /// Expiring charges are charges that aren't expected to be processed
    /// but can be promoted to a full charge, like for trials/freebies. When
    /// due, the underlying subscription is unprovisioned.
    Expiring,
}

impl ChargeStatus {
    pub fn from_string(string: &str) -> ChargeStatus {
        match string {
            "processing" => ChargeStatus::Processing,
            "succeeded" => ChargeStatus::Succeeded,
            "failed" => ChargeStatus::Failed,
            "open" => ChargeStatus::Open,
            "cancelled" => ChargeStatus::Cancelled,
            "expiring" => ChargeStatus::Expiring,
            _ => ChargeStatus::Failed,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            ChargeStatus::Processing => "processing",
            ChargeStatus::Succeeded => "succeeded",
            ChargeStatus::Failed => "failed",
            ChargeStatus::Open => "open",
            ChargeStatus::Cancelled => "cancelled",
            ChargeStatus::Expiring => "expiring",
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentPlatform {
    Stripe,
    None,
}

impl PaymentPlatform {
    pub fn from_string(string: &str) -> PaymentPlatform {
        match string {
            "stripe" => PaymentPlatform::Stripe,
            "none" => PaymentPlatform::None,
            _ => PaymentPlatform::Stripe,
        }
    }

    pub fn as_str(&self) -> &'static str {
        match self {
            PaymentPlatform::Stripe => "stripe",
            PaymentPlatform::None => "none",
        }
    }
}
