use rust_decimal::prelude::ToPrimitive;

use cala_types::balance::{BalanceAmount, BalanceSnapshot};

use crate::primitives::*;

use crate::{
    account::*,
    account_set::*,
    entry::*,
    journal::*,
    outbox::event::{OutboxEvent, OutboxEventPayload},
    transaction::TransactionValues,
    tx_template::*,
};

use super::proto;

impl From<OutboxEvent> for proto::CalaLedgerEvent {
    fn from(
        OutboxEvent {
            id,
            sequence,
            payload,
            recorded_at,
        }: OutboxEvent,
    ) -> Self {
        let payload = match payload {
            OutboxEventPayload::AccountCreated { source, account } => {
                proto::cala_ledger_event::Payload::AccountCreated(proto::AccountCreated {
                    data_source_id: source.to_string(),
                    account: Some(proto::Account::from(account)),
                })
            }
            OutboxEventPayload::AccountUpdated {
                source,
                account,
                fields,
            } => proto::cala_ledger_event::Payload::AccountUpdated(proto::AccountUpdated {
                data_source_id: source.to_string(),
                account: Some(proto::Account::from(account)),
                fields,
            }),
            OutboxEventPayload::AccountSetCreated {
                source,
                account_set,
            } => proto::cala_ledger_event::Payload::AccountSetCreated(proto::AccountSetCreated {
                data_source_id: source.to_string(),
                account_set: Some(proto::AccountSet::from(account_set)),
            }),
            OutboxEventPayload::AccountSetUpdated {
                source,
                account_set,
                fields,
            } => proto::cala_ledger_event::Payload::AccountSetUpdated(proto::AccountSetUpdated {
                data_source_id: source.to_string(),
                account_set: Some(proto::AccountSet::from(account_set)),
                fields,
            }),
            OutboxEventPayload::AccountSetMemberCreated {
                source,
                account_set_id,
                member_id,
            } => proto::cala_ledger_event::Payload::AccountSetMemberCreated(
                proto::AccountSetMemberCreated {
                    data_source_id: source.to_string(),
                    member: Some(proto::AccountSetMember {
                        account_set_id: account_set_id.to_string(),
                        member: Some(match member_id {
                            AccountSetMemberId::Account(account_id) => {
                                proto::account_set_member::Member::MemberAccountId(
                                    account_id.to_string(),
                                )
                            }
                            AccountSetMemberId::AccountSet(account_set_id) => {
                                proto::account_set_member::Member::MemberAccountSetId(
                                    account_set_id.to_string(),
                                )
                            }
                        }),
                    }),
                },
            ),
            OutboxEventPayload::AccountSetMemberRemoved {
                source,
                account_set_id,
                member_id,
            } => proto::cala_ledger_event::Payload::AccountSetMemberRemoved(
                proto::AccountSetMemberRemoved {
                    data_source_id: source.to_string(),
                    member: Some(proto::AccountSetMember {
                        account_set_id: account_set_id.to_string(),
                        member: Some(match member_id {
                            AccountSetMemberId::Account(account_id) => {
                                proto::account_set_member::Member::MemberAccountId(
                                    account_id.to_string(),
                                )
                            }
                            AccountSetMemberId::AccountSet(account_set_id) => {
                                proto::account_set_member::Member::MemberAccountSetId(
                                    account_set_id.to_string(),
                                )
                            }
                        }),
                    }),
                },
            ),
            OutboxEventPayload::JournalCreated { source, journal } => {
                proto::cala_ledger_event::Payload::JournalCreated(proto::JournalCreated {
                    data_source_id: source.to_string(),
                    journal: Some(proto::Journal::from(journal)),
                })
            }
            OutboxEventPayload::JournalUpdated {
                source,
                journal,
                fields,
            } => proto::cala_ledger_event::Payload::JournalUpdated(proto::JournalUpdated {
                data_source_id: source.to_string(),
                journal: Some(proto::Journal::from(journal)),
                fields,
            }),
            OutboxEventPayload::TxTemplateCreated {
                source,
                tx_template,
            } => proto::cala_ledger_event::Payload::TxTemplateCreated(proto::TxTemplateCreated {
                data_source_id: source.to_string(),
                tx_template: Some(proto::TxTemplate::from(tx_template)),
            }),
            OutboxEventPayload::TransactionCreated {
                source,
                transaction,
            } => proto::cala_ledger_event::Payload::TransactionCreated(proto::TransactionCreated {
                data_source_id: source.to_string(),
                transaction: Some(proto::Transaction::from(transaction)),
            }),
            OutboxEventPayload::TransactionUpdated {
                source,
                transaction,
                fields,
            } => proto::cala_ledger_event::Payload::TransactionUpdated(proto::TransactionUpdated {
                data_source_id: source.to_string(),
                transaction: Some(proto::Transaction::from(transaction)),
                fields,
            }),
            OutboxEventPayload::EntryCreated { source, entry } => {
                proto::cala_ledger_event::Payload::EntryCreated(proto::EntryCreated {
                    data_source_id: source.to_string(),
                    entry: Some(proto::Entry::from(entry)),
                })
            }
            OutboxEventPayload::BalanceCreated { source, balance } => {
                proto::cala_ledger_event::Payload::BalanceCreated(proto::BalanceCreated {
                    data_source_id: source.to_string(),
                    balance: Some(proto::Balance::from(balance)),
                })
            }
            OutboxEventPayload::BalanceUpdated { source, balance } => {
                proto::cala_ledger_event::Payload::BalanceUpdated(proto::BalanceUpdated {
                    data_source_id: source.to_string(),
                    balance: Some(proto::Balance::from(balance)),
                })
            }
            OutboxEventPayload::Empty => proto::cala_ledger_event::Payload::Empty(true),
        };
        proto::CalaLedgerEvent {
            id: id.to_string(),
            sequence: u64::from(sequence),
            recorded_at: Some(recorded_at.into()),
            payload: Some(payload),
        }
    }
}

impl From<AccountValues> for proto::Account {
    fn from(
        AccountValues {
            id,
            version,
            code,
            name,
            external_id,
            normal_balance_type,
            status,
            description,
            metadata,
            config,
        }: AccountValues,
    ) -> Self {
        let normal_balance_type: proto::DebitOrCredit = normal_balance_type.into();
        let status: proto::Status = status.into();
        proto::Account {
            id: id.to_string(),
            version,
            code,
            name,
            external_id,
            normal_balance_type: normal_balance_type as i32,
            status: status as i32,
            description,
            metadata: metadata.map(|json| {
                serde_json::from_value(json).expect("Could not transfer json -> struct")
            }),
            config: Some(proto::AccountConfig::from(config)),
        }
    }
}

impl From<AccountConfig> for proto::AccountConfig {
    fn from(config: AccountConfig) -> Self {
        proto::AccountConfig {
            is_account_set: config.is_account_set,
            eventually_consistent: config.eventually_consistent,
        }
    }
}

impl From<AccountSetValues> for proto::AccountSet {
    fn from(
        AccountSetValues {
            id,
            version,
            journal_id,
            name,
            external_id,
            normal_balance_type,
            description,
            metadata,
        }: AccountSetValues,
    ) -> Self {
        let normal_balance_type: proto::DebitOrCredit = normal_balance_type.into();
        proto::AccountSet {
            id: id.to_string(),
            version,
            journal_id: journal_id.to_string(),
            name,
            external_id,
            normal_balance_type: normal_balance_type as i32,
            description,
            metadata: metadata.map(|json| {
                serde_json::from_value(json).expect("Could not transfer json -> struct")
            }),
        }
    }
}

impl From<JournalValues> for proto::Journal {
    fn from(
        JournalValues {
            id,
            version,
            name,
            code,
            status,
            description,
            config,
        }: JournalValues,
    ) -> Self {
        let status: proto::Status = status.into();
        proto::Journal {
            id: id.to_string(),
            version,
            name,
            code,
            status: status as i32,
            description,
            config: Some(proto::JournalConfig::from(config)),
        }
    }
}

impl From<JournalConfig> for proto::JournalConfig {
    fn from(config: JournalConfig) -> Self {
        proto::JournalConfig {
            enable_effective_balances: config.enable_effective_balances,
        }
    }
}

impl From<DebitOrCredit> for proto::DebitOrCredit {
    fn from(priority: DebitOrCredit) -> Self {
        match priority {
            DebitOrCredit::Credit => proto::DebitOrCredit::Credit,
            DebitOrCredit::Debit => proto::DebitOrCredit::Debit,
        }
    }
}

impl From<Status> for proto::Status {
    fn from(priority: Status) -> Self {
        match priority {
            Status::Active => proto::Status::Active,
            Status::Locked => proto::Status::Locked,
        }
    }
}

impl From<TxTemplateValues> for proto::TxTemplate {
    fn from(
        TxTemplateValues {
            id,
            version,
            code,
            params,
            transaction,
            entries,
            description,
            metadata,
        }: TxTemplateValues,
    ) -> Self {
        let params = params
            .unwrap_or_default()
            .into_iter()
            .map(|param| param.into())
            .collect();
        proto::TxTemplate {
            id: id.to_string(),
            version,
            code,
            params,
            transaction: Some(transaction.into()),
            entries: entries.into_iter().map(|entry| entry.into()).collect(),
            description,
            metadata: metadata.map(|json| {
                serde_json::from_value(json).expect("Could not transfer json -> struct")
            }),
        }
    }
}

impl From<TxTemplateEntry> for proto::TxTemplateEntry {
    fn from(
        TxTemplateEntry {
            entry_type,
            account_id,
            layer,
            direction,
            currency,
            units,
            description,
            metadata,
        }: TxTemplateEntry,
    ) -> Self {
        proto::TxTemplateEntry {
            entry_type: String::from(entry_type),
            account_id: String::from(account_id),
            layer: String::from(layer),
            direction: String::from(direction),
            currency: String::from(currency),
            units: String::from(units),
            description: description.map(String::from),
            metadata: metadata.map(String::from),
        }
    }
}

impl From<ParamDefinition> for proto::ParamDefinition {
    fn from(
        ParamDefinition {
            name,
            r#type,
            default,
            description,
        }: ParamDefinition,
    ) -> Self {
        let data_type: proto::ParamDataType = r#type.into();
        proto::ParamDefinition {
            name,
            data_type: data_type as i32,
            default: default.map(String::from),
            description,
        }
    }
}

impl From<ParamDataType> for proto::ParamDataType {
    fn from(param_data_type: ParamDataType) -> Self {
        match param_data_type {
            ParamDataType::String => proto::ParamDataType::String,
            ParamDataType::Integer => proto::ParamDataType::Integer,
            ParamDataType::Decimal => proto::ParamDataType::Decimal,
            ParamDataType::Boolean => proto::ParamDataType::Boolean,
            ParamDataType::Uuid => proto::ParamDataType::Uuid,
            ParamDataType::Date => proto::ParamDataType::Date,
            ParamDataType::Timestamp => proto::ParamDataType::Timestamp,
            ParamDataType::Json => proto::ParamDataType::Json,
        }
    }
}

impl From<TxTemplateTransaction> for proto::TxTemplateTransaction {
    fn from(
        TxTemplateTransaction {
            effective,
            journal_id,
            correlation_id,
            external_id,
            description,
            metadata,
        }: TxTemplateTransaction,
    ) -> Self {
        proto::TxTemplateTransaction {
            effective: String::from(effective),
            journal_id: String::from(journal_id),
            correlation_id: correlation_id.map(String::from),
            external_id: external_id.map(String::from),
            description: description.map(String::from),
            metadata: metadata.map(String::from),
        }
    }
}

impl From<TransactionValues> for proto::Transaction {
    fn from(
        TransactionValues {
            id,
            version,
            created_at,
            modified_at,
            journal_id,
            tx_template_id,
            correlation_id,
            external_id,
            effective,
            description,
            metadata,
            entry_ids,
            voided_by,
            void_of,
        }: TransactionValues,
    ) -> Self {
        proto::Transaction {
            id: id.to_string(),
            version,
            created_at: Some(created_at.into()),
            modified_at: Some(modified_at.into()),
            journal_id: journal_id.to_string(),
            tx_template_id: tx_template_id.to_string(),
            entry_ids: entry_ids.into_iter().map(|id| id.to_string()).collect(),
            correlation_id,
            external_id,
            void_of: void_of.map(|id| id.to_string()),
            voided_by: voided_by.map(|id| id.to_string()),
            effective: effective.to_string(),
            description,
            metadata: metadata.map(|json| {
                serde_json::from_value(json).expect("Could not transfer json -> struct")
            }),
        }
    }
}

impl From<EntryValues> for proto::Entry {
    fn from(
        EntryValues {
            id,
            version,
            journal_id,
            transaction_id,
            account_id,
            entry_type,
            sequence,
            layer,
            direction,
            currency,
            units,
            description,
            metadata,
        }: EntryValues,
    ) -> Self {
        let layer: proto::Layer = layer.into();
        let direction: proto::DebitOrCredit = direction.into();
        let units = units.to_f64().expect("could not convert units to f64");
        proto::Entry {
            id: id.to_string(),
            version,
            journal_id: journal_id.to_string(),
            transaction_id: transaction_id.to_string(),
            account_id: account_id.to_string(),
            entry_type: entry_type.to_string(),
            sequence,
            layer: layer.into(),
            direction: direction.into(),
            currency: currency.to_string(),
            units: units.to_string(),
            description,
            metadata: metadata.map(|json| {
                serde_json::from_value(json).expect("Could not transfer json -> struct")
            }),
        }
    }
}

impl From<BalanceSnapshot> for proto::Balance {
    fn from(
        BalanceSnapshot {
            journal_id,
            account_id,
            currency,
            version,
            created_at,
            modified_at,
            entry_id,
            settled,
            pending,
            encumbrance,
        }: BalanceSnapshot,
    ) -> Self {
        proto::Balance {
            journal_id: journal_id.to_string(),
            account_id: account_id.to_string(),
            currency: currency.to_string(),
            version,
            created_at: Some(created_at.into()),
            modified_at: Some(modified_at.into()),
            entry_id: entry_id.to_string(),
            settled: Some(proto::BalanceAmount::from(settled)),
            pending: Some(proto::BalanceAmount::from(pending)),
            encumbrance: Some(proto::BalanceAmount::from(encumbrance)),
        }
    }
}

impl From<BalanceAmount> for proto::BalanceAmount {
    fn from(
        BalanceAmount {
            dr_balance,
            cr_balance,
            entry_id,
            modified_at,
        }: BalanceAmount,
    ) -> Self {
        proto::BalanceAmount {
            dr_balance: dr_balance.to_string(),
            cr_balance: cr_balance.to_string(),
            entry_id: entry_id.to_string(),
            modified_at: Some(modified_at.into()),
        }
    }
}

impl From<Layer> for proto::Layer {
    fn from(layer: Layer) -> Self {
        match layer {
            Layer::Settled => proto::Layer::Settled,
            Layer::Pending => proto::Layer::Pending,
            Layer::Encumbrance => proto::Layer::Encumbrance,
        }
    }
}
