use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

use std::cmp::Ordering;

use cala_types::{
    balance::{BalanceAmount, BalanceSnapshot},
    entry::EntryValues,
    primitives::{AccountId, Currency, EntryId},
};

use crate::balance::snapshot::Snapshots;

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub(super) enum SnapshotOrEntry<'a> {
    Snapshot {
        effective: NaiveDate,
        values: BalanceSnapshot,
    },
    #[serde(skip_deserializing)]
    Entry {
        effective: NaiveDate,
        entry: &'a EntryValues,
    },
}

impl SnapshotOrEntry<'_> {
    pub fn effective(&self) -> &NaiveDate {
        match self {
            Self::Snapshot { effective, .. } => effective,
            Self::Entry { effective, .. } => effective,
        }
    }

    fn snapshot(&self) -> (BalanceSnapshot, NaiveDate) {
        match self {
            Self::Snapshot { values, effective } => (values.clone(), *effective),
            _ => unimplemented!(),
        }
    }

    fn entry(&self) -> (&EntryValues, NaiveDate) {
        match self {
            Self::Entry { entry, effective } => (entry, *effective),
            _ => unimplemented!(),
        }
    }
}

#[derive(Debug)]
pub(super) struct EffectiveBalanceData<'a> {
    account_id: AccountId,
    currency: Currency,
    last_snapshot: Option<(NaiveDate, BalanceSnapshot)>,
    latest_all_time_version: u32,
    updates: Vec<SnapshotOrEntry<'a>>,
}

impl<'a> EffectiveBalanceData<'a> {
    pub fn new(
        account_id: AccountId,
        currency: Currency,
        last_snapshot: Option<(NaiveDate, BalanceSnapshot)>,
        latest_all_time_version: u32,
        updates: Vec<SnapshotOrEntry<'a>>,
    ) -> Self {
        Self {
            account_id,
            currency,
            last_snapshot,
            latest_all_time_version,
            updates,
        }
    }

    pub fn into_updates(
        self,
    ) -> impl Iterator<Item = (AccountId, Currency, NaiveDate, BalanceSnapshot, u32)> + use<'a>
    {
        self.updates
            .into_iter()
            .enumerate()
            .map(move |(idx, update)| {
                let (values, effective) = update.snapshot();
                (
                    self.account_id,
                    self.currency,
                    effective,
                    values,
                    idx as u32 + 1 + self.latest_all_time_version,
                )
            })
    }

    pub fn push(&mut self, effective: NaiveDate, entry: &'a EntryValues) {
        self.updates
            .push(SnapshotOrEntry::Entry { effective, entry });
    }

    pub fn re_calculate_snapshots(&mut self, created_at: DateTime<Utc>) {
        self.updates.sort();
        let (mut last_balance, mut last_effective) = match self.last_snapshot.take() {
            Some((snapshot_date, snapshot)) => (snapshot, snapshot_date),
            None => {
                let (entry, effective) = self.updates[0].entry();
                (
                    Self::first_snapshot(created_at, self.account_id, entry),
                    effective,
                )
            }
        };

        let mut diff_snapshot = None;

        for update in self.updates.iter_mut() {
            if &last_effective != update.effective() {
                last_balance.version = 0;
            }
            match update {
                SnapshotOrEntry::Entry { effective, entry } => {
                    last_effective = *effective;
                    last_balance = Snapshots::update_snapshot(created_at, last_balance, entry);
                    diff_snapshot = if let Some(diff) = diff_snapshot {
                        Some(Snapshots::update_snapshot(created_at, diff, entry))
                    } else {
                        let mut initial = Self::first_snapshot(created_at, self.account_id, entry);
                        initial.entry_id = last_balance.entry_id;
                        initial.encumbrance.entry_id = last_balance.encumbrance.entry_id;
                        initial.pending.entry_id = last_balance.pending.entry_id;
                        initial.settled.entry_id = last_balance.settled.entry_id;
                        Some(Snapshots::update_snapshot(created_at, initial, entry))
                    };
                    *update = SnapshotOrEntry::Snapshot {
                        effective: *effective,
                        values: last_balance.clone(),
                    };
                }
                SnapshotOrEntry::Snapshot {
                    effective,
                    ref mut values,
                } => {
                    last_effective = *effective;
                    let diff = diff_snapshot.as_mut().expect("diff must be initialized");
                    values.modified_at = created_at;
                    if diff.encumbrance.cr_balance != Decimal::ZERO
                        || diff.encumbrance.dr_balance != Decimal::ZERO
                    {
                        values.encumbrance.cr_balance += diff.encumbrance.cr_balance;
                        values.encumbrance.dr_balance += diff.encumbrance.dr_balance;
                        values.encumbrance.modified_at = created_at;
                    }
                    if diff.pending.cr_balance != Decimal::ZERO
                        || diff.pending.dr_balance != Decimal::ZERO
                    {
                        values.pending.cr_balance += diff.pending.cr_balance;
                        values.pending.dr_balance += diff.pending.dr_balance;
                        values.pending.modified_at = created_at;
                    }
                    if diff.settled.cr_balance != Decimal::ZERO
                        || diff.settled.dr_balance != Decimal::ZERO
                    {
                        values.settled.cr_balance += diff.settled.cr_balance;
                        values.settled.dr_balance += diff.settled.dr_balance;
                        values.settled.modified_at = created_at;
                    }
                    if values.entry_id == values.encumbrance.entry_id {
                        diff.encumbrance.entry_id = values.entry_id;
                        values.pending.entry_id = diff.pending.entry_id;
                        values.settled.entry_id = diff.settled.entry_id;
                    }
                    if values.entry_id == values.pending.entry_id {
                        values.encumbrance.entry_id = diff.encumbrance.entry_id;
                        diff.pending.entry_id = values.entry_id;
                        values.settled.entry_id = diff.settled.entry_id;
                    }
                    if values.entry_id == values.settled.entry_id {
                        values.encumbrance.entry_id = diff.encumbrance.entry_id;
                        values.pending.entry_id = diff.pending.entry_id;
                        diff.settled.entry_id = values.entry_id;
                    }
                }
            }
        }
    }

    fn first_snapshot(
        time: DateTime<Utc>,
        account_id: AccountId,
        entry: &EntryValues,
    ) -> BalanceSnapshot {
        let entry_id = EntryId::from(crate::balance::snapshot::UNASSIGNED_ENTRY_ID);
        BalanceSnapshot {
            journal_id: entry.journal_id,
            account_id,
            entry_id,
            currency: entry.currency,
            settled: BalanceAmount {
                dr_balance: Decimal::ZERO,
                cr_balance: Decimal::ZERO,
                entry_id,
                modified_at: time,
            },
            pending: BalanceAmount {
                dr_balance: Decimal::ZERO,
                cr_balance: Decimal::ZERO,
                entry_id,
                modified_at: time,
            },
            encumbrance: BalanceAmount {
                dr_balance: Decimal::ZERO,
                cr_balance: Decimal::ZERO,
                entry_id,
                modified_at: time,
            },
            version: 0,
            modified_at: time,
            created_at: time,
        }
    }
}

impl PartialEq for SnapshotOrEntry<'_> {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Snapshot { values: v1, .. }, Self::Snapshot { values: v2, .. }) => {
                v1.entry_id == v2.entry_id
            }
            (Self::Entry { entry: en1, .. }, Self::Entry { entry: en2, .. }) => en1.id == en2.id,
            _ => false,
        }
    }
}
impl Eq for SnapshotOrEntry<'_> {}

impl PartialOrd for SnapshotOrEntry<'_> {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for SnapshotOrEntry<'_> {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.effective().cmp(other.effective()) {
            Ordering::Equal => {}
            ordering => return ordering,
        }

        match (self, other) {
            (Self::Entry { .. }, Self::Snapshot { .. }) => unreachable!(),
            (Self::Snapshot { .. }, Self::Entry { .. }) => unreachable!(),
            (Self::Snapshot { values: v1, .. }, Self::Snapshot { values: v2, .. }) => {
                v1.version.cmp(&v2.version)
            }
            (Self::Entry { entry: e1, .. }, Self::Entry { entry: e2, .. }) => {
                e1.sequence.cmp(&e2.sequence)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use cala_types::primitives::*;
    use rust_decimal_macros::dec;

    use super::*;

    fn entry_values() -> EntryValues {
        EntryValues {
            id: EntryId::new(),
            journal_id: JournalId::new(),
            transaction_id: TransactionId::new(),
            account_id: AccountId::new(),
            currency: Currency::USD,
            entry_type: "ENTRY_TYPE".to_string(),
            sequence: 1,
            version: 1,
            layer: Layer::Settled,
            units: Decimal::ONE,
            direction: DebitOrCredit::Credit,
            description: None,
            metadata: None,
        }
    }

    fn balance_amount(entry_id: EntryId, credit: Decimal) -> BalanceAmount {
        BalanceAmount {
            dr_balance: Decimal::ZERO,
            cr_balance: credit,
            entry_id,
            modified_at: Utc::now(),
        }
    }

    fn random_snapshot() -> BalanceSnapshot {
        let entry_id = EntryId::new();
        BalanceSnapshot {
            journal_id: JournalId::new(),
            account_id: AccountId::new(),
            currency: Currency::USD,
            version: 1,
            created_at: Utc::now(),
            modified_at: Utc::now(),
            entry_id,
            settled: balance_amount(entry_id, Decimal::ONE),
            pending: balance_amount(EntryId::new(), Decimal::ZERO),
            encumbrance: balance_amount(EntryId::new(), Decimal::ZERO),
        }
    }

    fn random_snapshot_with_pending() -> BalanceSnapshot {
        let entry_id = EntryId::new();
        BalanceSnapshot {
            journal_id: JournalId::new(),
            account_id: AccountId::new(),
            currency: Currency::USD,
            version: 1,
            created_at: Utc::now(),
            modified_at: Utc::now(),
            entry_id,
            settled: balance_amount(EntryId::new(), Decimal::ONE),
            pending: balance_amount(entry_id, Decimal::ONE),
            encumbrance: balance_amount(EntryId::new(), Decimal::ZERO),
        }
    }

    #[test]
    fn empty_data() {
        let account_id = AccountId::new();
        let mut data = EffectiveBalanceData::new(account_id, Currency::USD, None, 0, Vec::new());

        let effective = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let entry = entry_values();
        data.push(effective, &entry);

        let posted_at = Utc::now();
        data.re_calculate_snapshots(posted_at);

        assert_eq!(data.updates.len(), 1);
        assert!(matches!(data.updates[0], SnapshotOrEntry::Snapshot { .. }));

        let (snapshot, update_effective) = data.updates[0].snapshot();
        assert_eq!(update_effective, effective);
        assert_eq!(snapshot.entry_id, entry.id);
        assert_eq!(snapshot.version, 1);
        assert_eq!(snapshot.settled.cr_balance, Decimal::ONE);
    }

    #[test]
    fn existing_previous_balance() {
        let account_id = AccountId::new();
        let snapshot_date = NaiveDate::from_ymd_opt(2023, 9, 30).unwrap();
        let mut data = EffectiveBalanceData::new(
            account_id,
            Currency::USD,
            Some((snapshot_date, random_snapshot())),
            1,
            Vec::new(),
        );

        let effective = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let entry = entry_values();
        data.push(effective, &entry);

        let posted_at = Utc::now();
        data.re_calculate_snapshots(posted_at);

        assert_eq!(data.updates.len(), 1);
        assert!(matches!(data.updates[0], SnapshotOrEntry::Snapshot { .. }));

        let (snapshot, update_effective) = data.updates[0].snapshot();
        assert_eq!(update_effective, effective);
        assert_eq!(snapshot.entry_id, entry.id);
        assert_eq!(snapshot.version, 1);
        assert_eq!(snapshot.settled.cr_balance, dec!(2));
    }

    #[test]
    fn existing_previous_balance_same_day() {
        let account_id = AccountId::new();
        let snapshot_date = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let mut data = EffectiveBalanceData::new(
            account_id,
            Currency::USD,
            Some((snapshot_date, random_snapshot())),
            1,
            Vec::new(),
        );

        let effective = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let entry = entry_values();
        data.push(effective, &entry);

        let posted_at = Utc::now();
        data.re_calculate_snapshots(posted_at);

        assert_eq!(data.updates.len(), 1);
        assert!(matches!(data.updates[0], SnapshotOrEntry::Snapshot { .. }));

        let (snapshot, update_effective) = data.updates[0].snapshot();
        assert_eq!(update_effective, effective);
        assert_eq!(snapshot.entry_id, entry.id);
        assert_eq!(snapshot.version, 2);
        assert_eq!(snapshot.settled.cr_balance, dec!(2));
    }

    #[test]
    fn two_entries() {
        let account_id = AccountId::new();
        let mut data = EffectiveBalanceData::new(account_id, Currency::USD, None, 0, Vec::new());

        let effective = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let entry = entry_values();
        data.push(effective, &entry);
        let mut entry_two = entry_values();
        entry_two.sequence = 2;
        data.push(effective, &entry_two);

        let posted_at = Utc::now();
        data.re_calculate_snapshots(posted_at);

        assert_eq!(data.updates.len(), 2);
        assert!(matches!(data.updates[0], SnapshotOrEntry::Snapshot { .. }));

        let (snapshot, update_effective) = data.updates[0].snapshot();
        assert_eq!(update_effective, effective);
        assert_eq!(snapshot.entry_id, entry.id);
        assert_eq!(snapshot.version, 1);
        assert_eq!(snapshot.settled.cr_balance, dec!(1));

        assert!(matches!(data.updates[1], SnapshotOrEntry::Snapshot { .. }));

        let (snapshot, update_effective) = data.updates[1].snapshot();
        assert_eq!(update_effective, effective);
        assert_eq!(snapshot.entry_id, entry_two.id);
        assert_eq!(snapshot.version, 2);
        assert_eq!(snapshot.settled.cr_balance, dec!(2));
    }

    #[test]
    fn rewrite_future_snapshot_after_two_entries() {
        let account_id = AccountId::new();
        let future = NaiveDate::from_ymd_opt(2023, 10, 2).unwrap();
        let future_balance = random_snapshot_with_pending();
        let mut data = EffectiveBalanceData::new(
            account_id,
            Currency::USD,
            None,
            0,
            vec![SnapshotOrEntry::Snapshot {
                effective: future,
                values: future_balance.clone(),
            }],
        );
        let effective = NaiveDate::from_ymd_opt(2023, 10, 1).unwrap();
        let entry = entry_values();
        data.push(effective, &entry);
        let mut entry_two = entry_values();
        entry_two.sequence = 2;
        data.push(effective, &entry_two);

        let posted_at = Utc::now();
        data.re_calculate_snapshots(posted_at);

        assert_eq!(data.updates.len(), 3);

        let (snapshot, update_effective) = data.updates[2].snapshot();
        assert_eq!(update_effective, future);
        assert_eq!(snapshot.entry_id, future_balance.entry_id);
        assert_eq!(snapshot.version, 1);

        assert_eq!(snapshot.settled.cr_balance, dec!(3));
        assert_eq!(snapshot.settled.entry_id, entry_two.id);
        assert_eq!(snapshot.pending.cr_balance, dec!(1));
        assert_eq!(snapshot.entry_id, snapshot.pending.entry_id);
    }
}
