use std::fmt::{Debug, Display};
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};

use rust_decimal::Decimal;

pub trait Unit {
    const PRECISION: u8;
    const SUFFIX: &str;

    const ONE_BTC: u64 = 10u64.pow(Self::PRECISION as u32);
    const ONE_BTC_DEC: Decimal = Decimal::from_parts(Self::ONE_BTC as u32, 0, 0, false, 0);
    const BTC_PER_UNIT: Decimal = Decimal::from_parts(1, 0, 0, false, Self::PRECISION as u32);
    const SAT_PER_UNIT: u64 = 10u64.pow(8 - Self::PRECISION as u32);
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Btc;
impl Unit for Btc {
    const PRECISION: u8 = 0;
    const SUFFIX: &str = "BTC";
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Sat;
impl Unit for Sat {
    const PRECISION: u8 = 8;
    const SUFFIX: &str = "SAT";
}

pub type AmountDenom = Sat;

#[derive(Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Amount(pub u64);

impl<X: Into<u64>> From<X> for Amount {
    fn from(value: X) -> Self {
        Self(value.into())
    }
}

impl Debug for Amount {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple(&format!("Amount<{}>", AmountDenom::SUFFIX)).field(&self.0).finish()
    }
}

impl Display for Amount {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Display::fmt(&self.0, f)
    }
}

impl Amount {
    pub const ZERO: Self = Self(0);
    pub const ONE: Self = Self(1);
    pub const ONE_BTC: Self = Self(AmountDenom::ONE_BTC);
    pub const MIN: Self = Self(0);
    pub const MAX: Self = Self(21_000_000 * Sat::ONE_BTC);

    pub fn as_decimal(self) -> Decimal {
        self.0.into()
    }

    pub fn try_from_decimal(v: Decimal) -> Option<Self> {
        Some(Self(v.try_into().ok()?))
    }
    pub fn from_decimal(v: Decimal) -> Self {
        Self(v.try_into().expect("Amount must be in range 0..u64::MAX"))
    }

    pub const fn as_sats(self) -> u64 {
        self.0 * AmountDenom::SAT_PER_UNIT
    }

    pub fn as_btc(self) -> Decimal {
        Decimal::from(self.0) * AmountDenom::BTC_PER_UNIT
    }

    pub fn try_from_btc(v: Decimal) -> Option<Self> {
        Self::try_from_decimal(v * AmountDenom::ONE_BTC_DEC)
    }
    pub fn from_btc(v: Decimal) -> Self {
        Self::from_decimal(v * AmountDenom::ONE_BTC_DEC)
    }
    pub const fn from_sat(v: u64) -> Self {
        Self(v / AmountDenom::SAT_PER_UNIT)
    }

    pub const fn min(self, other: Self) -> Self {
        Self(if self.0 <= other.0 { self.0 } else { other.0 })
    }

    pub const fn max(self, other: Self) -> Self {
        Self(if self.0 >= other.0 { self.0 } else { other.0 })
    }

    pub const fn is_zero(self) -> bool {
        self.0 == 0
    }
}

impl Add for Amount {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0 + rhs.0)
    }
}
impl AddAssign for Amount {
    fn add_assign(&mut self, rhs: Self) {
        self.0 += rhs.0;
    }
}

impl Sub for Amount {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0 - rhs.0)
    }
}

impl SubAssign for Amount {
    fn sub_assign(&mut self, rhs: Self) {
        self.0 -= rhs.0;
    }
}

impl<X> Mul<X> for Amount
where
    Decimal: TryFrom<X>,
{
    type Output = Self;

    fn mul(self, rhs: X) -> Self::Output {
        Amount::from_btc(self.as_btc() * Decimal::try_from(rhs).unwrap_or_default())
    }
}

impl<X> MulAssign<X> for Amount
where
    Decimal: TryFrom<X>,
{
    fn mul_assign(&mut self, rhs: X) {
        *self = Amount::from_btc(self.as_btc() * Decimal::try_from(rhs).unwrap_or_default());
    }
}

impl<X> Div<X> for Amount
where
    Decimal: TryFrom<X>,
{
    type Output = Self;

    fn div(self, rhs: X) -> Self::Output {
        Amount::from_btc(self.as_btc() / Decimal::try_from(rhs).unwrap_or_default())
    }
}

impl<X> DivAssign<X> for Amount
where
    Decimal: TryFrom<X>,
{
    fn div_assign(&mut self, rhs: X) {
        *self = Amount::from_btc(self.as_btc() / Decimal::try_from(rhs).unwrap_or_default());
    }
}
