use crate::foundation::*;
use crate::merchandising::*;

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExchangeRate {
    pub(crate) source: Currency,
    pub(crate) target: Currency,
    pub(crate) numerator: Nat,
    pub(crate) denominator: Nat,
    pub(crate) observed_at: Timestamp,
}

impl ExchangeRate {
    pub fn try_new(
        source: Currency,
        target: Currency,
        numerator: Nat,
        denominator: Nat,
        observed_at: Timestamp,
    ) -> DomainResult<Self> {
        if denominator == 0 {
            return Err(ValidationError::Invariant(
                "exchange-rate denominator must be positive",
            ));
        }
        Ok(Self {
            source,
            target,
            numerator,
            denominator,
            observed_at,
        })
    }
}

pub fn fx_quote_fresh(now: Timestamp, max_age: Timestamp, rate: &ExchangeRate) -> bool {
    rate.observed_at <= now && now - rate.observed_at <= max_age
}

pub fn convert_money_floor(amount: Money, rate: &ExchangeRate) -> DomainResult<Money> {
    checked_div(
        checked_mul(amount, rate.numerator, "convert_money_floor multiply")?,
        rate.denominator,
        "convert_money_floor divide",
    )
}

domain_struct! {
    pub struct TaxRate {
        bps: BasisPoints,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TaxCalculation {
    pub(crate) taxable_amount: Money,
    pub(crate) tax: Money,
    pub(crate) total: Money,
}

impl TaxCalculation {
    pub fn try_new(taxable_amount: Money, tax: Money, total: Money) -> DomainResult<Self> {
        if total != checked_add(taxable_amount, tax, "tax calculation total")? {
            return Err(ValidationError::Invariant("tax total is incorrect"));
        }
        Ok(Self {
            taxable_amount,
            tax,
            total,
        })
    }
}

domain_struct! {
    pub struct ShippingZone {
        id: Id,
        name: String,
    }
}

domain_struct! {
    pub struct CarrierService {
        carrier_id: Id,
        zone: ShippingZone,
        max_weight: Weight,
        base_cost: Money,
        promised_days: Days,
    }
}

domain_struct! {
    pub struct Package {
        weight: Weight,
        volume: Nat,
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CarrierQuote {
    pub(crate) service: CarrierService,
    pub(crate) package: Package,
    pub(crate) price: Money,
}

impl CarrierQuote {
    pub fn try_new(service: CarrierService, package: Package, price: Money) -> DomainResult<Self> {
        if package.weight > service.max_weight {
            return Err(ValidationError::Invariant(
                "package exceeds service max weight",
            ));
        }
        if price < service.base_cost {
            return Err(ValidationError::Invariant("quote price below base cost"));
        }
        Ok(Self {
            service,
            package,
            price,
        })
    }
}

pub fn abs_diff_nat(a: Nat, b: Nat) -> Nat {
    a.abs_diff(b)
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReconciliationWithinTolerance {
    pub(crate) expected: Money,
    pub(crate) actual: Money,
    pub(crate) tolerance: Money,
}

impl ReconciliationWithinTolerance {
    pub fn try_new(expected: Money, actual: Money, tolerance: Money) -> DomainResult<Self> {
        if abs_diff_nat(expected, actual) > tolerance {
            return Err(ValidationError::Invariant(
                "reconciliation diff exceeds tolerance",
            ));
        }
        Ok(Self {
            expected,
            actual,
            tolerance,
        })
    }
}

pub(crate) fn _merchandising_anchor(_: Option<PromotionStackingPolicy>) {}

impl_getters!(ExchangeRate {
    source: Currency,
    target: Currency,
    numerator: Nat,
    denominator: Nat,
    observed_at: Timestamp,
});

impl_getters!(TaxCalculation {
    taxable_amount: Money,
    tax: Money,
    total: Money,
});

impl_getters!(CarrierQuote {
    service: CarrierService,
    package: Package,
    price: Money,
});

impl_getters!(ReconciliationWithinTolerance {
    expected: Money,
    actual: Money,
    tolerance: Money,
});
