use core::fmt::Write;
use core::ops::Index;
use core::str::FromStr;

use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
use beancount_types::common_keys;
use beancount_types::AccountTemplate;
use beancount_types::Amount;
use beancount_types::Balance;
use beancount_types::Commodity;
use beancount_types::CostBasis;
use beancount_types::Directive;
use beancount_types::Link;
use beancount_types::Price;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use miette::Diagnostic;
use miette::IntoDiagnostic;
use miette::Report;
use rust_decimal::Decimal;
use serde::Deserialize;
use snafu::Backtrace;
use snafu::OptionExt;
use snafu::Snafu;
use tap::prelude::Tap;
use time::macros::format_description;
use time::Date;

mod dmy_short {
    pub mod opt {
        pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<time::Date>, D::Error>
        where
            D: serde::Deserializer<'de>,
        {
            struct Visitor;

            impl<'de> serde::de::Visitor<'de> for Visitor {
                type Value = Option<time::Date>;

                fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                    formatter.write_str("a date in dd.mm.yy format")
                }

                fn visit_none<E>(self) -> Result<Self::Value, E>
                where
                    E: serde::de::Error,
                {
                    Ok(None)
                }

                fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
                where
                    D: serde::Deserializer<'de>,
                {
                    super::deserialize(deserializer).map(Some)
                }
            }

            deserializer.deserialize_option(Visitor)
        }
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<time::Date, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct Visitor;

        impl serde::de::Visitor<'_> for Visitor {
            type Value = time::Date;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a date in dd.mm.yy format")
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                const BASE_YEAR: i32 = 2000;
                const DATE_FORMAT: &[time::format_description::FormatItem] =
                    time::macros::format_description!("[day].[month].[year repr:last_two]");

                let mut parsed = time::parsing::Parsed::new();
                parsed
                    .parse_items(v.as_bytes(), DATE_FORMAT)
                    .map_err(E::custom)?;

                let year_last_two = i32::from(parsed.year_last_two().unwrap());
                parsed.set_year(BASE_YEAR + year_last_two);

                parsed.try_into().map_err(E::custom)
            }
        }

        deserializer.deserialize_str(Visitor)
    }
}

#[derive(Builder, Clone, Debug, Deserialize)]
#[builder(
    build_fn(error = "ImporterBuilderError", skip),
    name = "ImporterBuilder",
    setter(into),
    try_setter
)]
pub struct Config {
    pub account_template: AccountTemplate<TemplateSelector>,

    pub capital_gains_account: AccountTemplate<TemplateSelector>,

    pub currency_account: AccountTemplate<CurrencyTemplateSelector>,

    pub distributions_template: AccountTemplate<TemplateSelector>,

    pub fee_template: AccountTemplate<TemplateSelector>,

    pub payee: String,

    pub portfolio_fee_template: AccountTemplate<TemplateSelector>,

    pub reference_account: AccountTemplate<TemplateSelector>,
}

#[derive(Clone, Copy, Debug)]
pub enum CurrencyTemplateSelector {
    Currency,
}

impl FromStr for CurrencyTemplateSelector {
    type Err = TemplateSelectorError;

    fn from_str(selector: &str) -> Result<Self, Self::Err> {
        let selector = match selector {
            "currency" => Self::Currency,
            _ => return TemplateSelectorSnafu { selector }.fail(),
        };

        Ok(selector)
    }
}

#[derive(Debug, Diagnostic, Snafu)]
pub enum Error {
    #[snafu(display("error while parsing date"))]
    DateFormat { source: time::Error },

    #[snafu(display("unsupported transaction kind: {value:?}"))]
    UnsupportedTransactionKind { value: String, backtrace: Backtrace },
}

impl From<time::error::TryFromParsed> for Error {
    fn from(source: time::error::TryFromParsed) -> Self {
        let source = source.into();
        Self::DateFormat { source }
    }
}

#[derive(Debug, Deserialize)]
pub struct Importer {
    #[serde(flatten)]
    pub config: Config,

    #[serde(default = "csv::Importer::semicolon_delimited", skip_deserializing)]
    pub importer: csv::Importer,
}

impl Importer {
    pub const NAME: &str = "ebase/transactions";

    pub fn new(config: Config) -> Self {
        let importer = csv::Importer::semicolon_delimited();
        Self { config, importer }
    }

    pub fn builder() -> ImporterBuilder {
        ImporterBuilder::default()
    }
}

impl beancount_importers_framework::ImporterProtocol for Importer {
    type Error = Report;

    fn account(&self, _file: &[u8]) -> Result<beancount_types::Account, Self::Error> {
        Ok(self.config.account_template.base().to_owned())
    }

    fn date(&self, file: &[u8]) -> Option<Result<Date, Self::Error>> {
        self.importer
            .date(file, self)
            .map(|result| result.map_err(Self::Error::from))
    }

    fn extract(&self, file: &[u8], existing: &[Directive]) -> Result<Vec<Directive>, Self::Error> {
        self.importer
            .extract(file, existing, self)
            .map_err(Report::from)
    }

    fn filename(&self, _file: &[u8]) -> Option<Result<String, Self::Error>> {
        Some(Ok(String::from("transactions.csv")))
    }

    fn identify(&self, file: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: [&str; 33] = [
            "Depotnummer",
            "Depotposition",
            "Ref. Nr.",
            "Buchungsdatum",
            "Umsatzart",
            "Teilumsatz",
            "Fonds",
            "ISIN",
            "Zahlungsbetrag in ZW",
            "Zahlungswährung (ZW)",
            "Anteile",
            "Abrechnungskurs in FW",
            "Fondswährung (FW)",
            "Kursdatum",
            "Devisenkurs  (ZW/FW)",
            "Anlagebetrag in ZW",
            "Vertriebsprovision in ZW (im Abrechnungskurs enthalten)",
            "KVG Einbehalt in ZW (im Abrechnungskurs enthalten)",
            "Gegenwert der Anteile in ZW",
            "Anteile zum Bestandsdatum",
            "Barausschüttung/Steuerliquidität je Anteil in EW",
            "Ertragswährung (EW)",
            "Bestandsdatum",
            "Devisenkurs (ZW/EW)",
            "Barausschüttung/Steuerliquidität in ZW",
            "Bruttobetrag VAP je Anteil in EUR",
            "Entgelt in ZW",
            "Entgelt in EUR",
            "Steuern in ZW",
            "Steuern in EUR",
            "Devisenkurs (EUR/ZW)",
            "Art des Steuereinbehalts",
            "Steuereinbehalt in EUR",
        ];

        self.importer
            .identify(file, &EXPECTED_HEADERS)
            .into_diagnostic()
    }

    fn name(&self) -> &'static str {
        Self::NAME
    }

    #[doc(hidden)]
    fn typetag_deserialize(&self) {}
}

impl csv::RecordImporter for Importer {
    type Error = Error;
    type Record<'de> = Record<'de>;

    fn date(&self, record: Record) -> Date {
        record.booking_date
    }

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        // TODO improve error handling

        let reference = record.reference.transaction_id;

        let depot_id = record.depot_id;
        let position = record.depot_position;
        let context = TemplateContext {
            depot_id: <&Seg>::try_from(depot_id).unwrap(),
            position: <&Seg>::try_from(position).unwrap(),
        };

        let mut directives = Vec::new();

        let mut transaction = Transaction::on(record.booking_date);
        transaction
            .set_payee(&self.config.payee)
            .set_narration(record.transaction_kind.format_narration(record.fund_name))
            .add_meta(common_keys::TRANSACTION_ID, record.reference.to_string());

        match record.transaction_kind {
            TransactionKind::Distribution => {
                let distribution_amount =
                    Amount::new(record.distribution_total, record.payment_currency);

                let balance_date = record.balance_date.unwrap();
                let main_account = self.config.account_template.render(&context);
                let balance_amount = Amount::new(record.balance_shares, record.isin);

                directives.push(Directive::from(Balance::new(
                    balance_date,
                    main_account,
                    balance_amount,
                )));

                transaction.build_posting(
                    self.config.reference_account.render(&context),
                    |posting| {
                        if record.payment_currency.as_ref() == "EUR" {
                            posting.set_amount(distribution_amount);
                        } else {
                            let rate = record.exchange_rate_eur_payment.unwrap();
                            let distribution_amount_eur = {
                                let amount_eur =
                                    (distribution_amount.amount / rate).tap_mut(|amount| {
                                        amount.rescale(2);
                                    });

                                Amount::new(amount_eur, Commodity::try_from("EUR").unwrap())
                            };
                            posting
                                .set_amount(distribution_amount_eur)
                                .set_price(Amount::new(rate, record.payment_currency));
                        }
                    },
                );

                transaction.build_posting(
                    self.config.distributions_template.render(&context),
                    |posting| {
                        posting.set_amount(-distribution_amount);
                    },
                );
            }

            TransactionKind::LumpSumTaxAdvance => {
                // No interesting information
                return Ok(vec![]);
            }

            TransactionKind::Purchase | TransactionKind::SavingsPlan => {
                let fund_currency = record.fund_currency.unwrap();
                let share_price = Amount::new(record.share_price, fund_currency);

                let shares_amount = Amount::new(record.shares, record.isin);

                let payment_currency = record.payment_currency;
                let payment_amount = Amount::new(record.payment_amount, payment_currency);

                let investment_amount_payment =
                    Amount::new(record.investment_amount, payment_currency);
                let investment_amount_fund = record.exchange_rate_payment_fund.map(|rate| {
                    let mut amount = investment_amount_payment.amount * rate;
                    amount.rescale(2);
                    Amount::new(amount, fund_currency)
                });

                transaction
                    .add_link(Link::try_from(format!("^ebase.{reference}")).unwrap())
                    .build_posting(self.config.account_template.render(&context), |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    });

                if !record.fees.is_zero() {
                    let fees = Amount::new(record.fees, payment_currency);

                    transaction.build_posting(
                        self.config.fee_template.render(&context),
                        |posting| {
                            posting.set_amount(fees);
                        },
                    );
                }

                transaction.build_posting(
                    self.config.reference_account.render(&context),
                    |posting| {
                        posting.set_amount(-payment_amount);
                    },
                );

                if let Some(investment_amount_fund) = investment_amount_fund {
                    transaction
                        .build_posting(
                            self.config
                                .currency_account
                                .render(&CurrencyTemplateContext {
                                    currency: {
                                        let currency: &str = &payment_currency;
                                        <&Seg>::try_from(currency)
                                            .expect("commodities are valid segments")
                                    },
                                }),
                            |posting| {
                                posting.set_amount(investment_amount_payment);
                            },
                        )
                        .build_posting(
                            self.config
                                .currency_account
                                .render(&CurrencyTemplateContext {
                                    currency: {
                                        let currency: &str = &fund_currency;
                                        <&Seg>::try_from(currency)
                                            .expect("commodities are valid segments")
                                    },
                                }),
                            |posting| {
                                posting.set_amount(-investment_amount_fund);
                            },
                        );
                }
            }

            TransactionKind::PortfolioFee => {
                // TODO we may have to handle currency accounts here
                let fund_currency = record.fund_currency.unwrap();
                let share_price = Amount::new(record.share_price, fund_currency);

                let shares_amount = Amount::new(record.shares, record.isin);
                let payment_currency = record.payment_currency;
                let sale_amount_payment = Amount::new(record.fees, payment_currency);

                let sale_amount_fund = record.exchange_rate_payment_fund.map(|rate| {
                    let mut amount = sale_amount_payment.amount * rate;
                    amount.rescale(2);
                    Amount::new(amount, fund_currency)
                });

                transaction
                    .build_posting(self.config.account_template.render(&context), |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::Empty)
                            .set_price(share_price);
                    })
                    .build_posting(
                        self.config.portfolio_fee_template.render(&context),
                        |posting| {
                            posting.set_amount(sale_amount_payment);
                        },
                    )
                    .build_posting(
                        self.config.capital_gains_account.render(&context),
                        |_posting| {},
                    );

                if let Some(sale_amount_fund) = sale_amount_fund {
                    transaction
                        .build_posting(
                            self.config
                                .currency_account
                                .render(&CurrencyTemplateContext {
                                    currency: {
                                        let currency: &str = &payment_currency;
                                        <&Seg>::try_from(currency)
                                            .expect("commodities are valid segments")
                                    },
                                }),
                            |posting| {
                                posting.set_amount(-sale_amount_payment);
                            },
                        )
                        .build_posting(
                            self.config
                                .currency_account
                                .render(&CurrencyTemplateContext {
                                    currency: {
                                        let currency: &str = &fund_currency;
                                        <&Seg>::try_from(currency)
                                            .expect("commodities are valid segments")
                                    },
                                }),
                            |posting| {
                                posting.set_amount(sale_amount_fund);
                            },
                        );
                }
            }

            TransactionKind::Reinvest => {
                let fund_currency = record.fund_currency.unwrap();
                let share_price = Amount::new(record.share_price, fund_currency);

                let shares_amount = Amount::new(record.shares, record.isin);

                let payment_currency = record.payment_currency;

                let investment_amount_payment =
                    Amount::new(record.investment_amount, payment_currency);

                transaction.build_posting(
                    self.config.account_template.render(&context),
                    |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    },
                );

                if !record.fees.is_zero() {
                    let fees = Amount::new(record.fees, payment_currency);

                    transaction
                        .build_posting(self.config.fee_template.render(&context), |posting| {
                            posting.set_amount(fees);
                        })
                        .build_posting(self.config.reference_account.render(&context), |posting| {
                            posting.set_amount(-fees);
                        });
                }

                transaction.build_posting(
                    self.config.reference_account.render(&context),
                    |posting| {
                        posting.set_amount(-investment_amount_payment);

                        if payment_currency != fund_currency {
                            // TODO instead of exchange rates, this could be tracked using Currency accounts
                            let exchange_rate = record
                                .exchange_rate_payment_fund
                                .expect("exchange rate is set when currencies differ");
                            posting.set_price(Amount::new(exchange_rate, fund_currency));
                        }
                    },
                );
            }

            TransactionKind::SavingsPlanRebooking => {
                let fund_currency = record.fund_currency.unwrap();
                let share_price = Amount::new(record.share_price, fund_currency);

                let shares_amount = Amount::new(record.shares, record.isin);

                let payment_currency = record.payment_currency;

                let investment_amount_payment =
                    Amount::new(record.investment_amount, payment_currency);

                transaction.build_posting(
                    self.config.account_template.render(&context),
                    |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    },
                );

                if !record.fees.is_zero() {
                    let fees = Amount::new(record.fees, payment_currency);

                    transaction
                        .build_posting(self.config.fee_template.render(&context), |posting| {
                            posting.set_amount(fees);
                        })
                        .build_posting(self.config.reference_account.render(&context), |posting| {
                            posting.set_amount(-fees);
                        });
                }

                transaction.build_posting(
                    self.config.reference_account.render(&context),
                    |posting| {
                        posting.set_amount(-investment_amount_payment);

                        if payment_currency != fund_currency {
                            // TODO instead of exchange rates, this could be tracked using Currency accounts
                            let exchange_rate = record
                                .exchange_rate_payment_fund
                                .expect("exchange rate is set when currencies differ");
                            posting.set_price(Amount::new(exchange_rate, fund_currency));
                        }
                    },
                );
            }

            TransactionKind::SavingsPlanReversal => {
                let fund_currency = record.fund_currency.unwrap();
                let share_price = Amount::new(record.share_price, fund_currency);

                let shares_amount = Amount::new(record.shares, record.isin);

                let payment_currency = record.payment_currency;
                let payment_amount = Amount::new(record.payment_amount, payment_currency);

                let investment_amount_payment =
                    Amount::new(record.investment_amount, payment_currency);
                let investment_amount_fund = record.exchange_rate_payment_fund.map(|rate| {
                    let mut amount = investment_amount_payment.amount * rate;
                    amount.rescale(2);
                    Amount::new(amount, fund_currency)
                });

                transaction.build_posting(
                    self.config.account_template.render(&context),
                    |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    },
                );

                if !record.fees.is_zero() {
                    let fees = Amount::new(record.fees, payment_currency);

                    transaction.build_posting(
                        self.config.fee_template.render(&context),
                        |posting| {
                            posting.set_amount(-fees);
                        },
                    );
                }

                transaction.build_posting(
                    self.config.reference_account.render(&context),
                    |posting| {
                        posting.set_amount(payment_amount);
                    },
                );

                if let Some(investment_amount_fund) = investment_amount_fund {
                    transaction
                        .build_posting(
                            self.config
                                .currency_account
                                .render(&CurrencyTemplateContext {
                                    currency: {
                                        let currency: &str = &payment_currency;
                                        <&Seg>::try_from(currency)
                                            .expect("commodities are valid segments")
                                    },
                                }),
                            |posting| {
                                posting.set_amount(-investment_amount_payment);
                            },
                        )
                        .build_posting(
                            self.config
                                .currency_account
                                .render(&CurrencyTemplateContext {
                                    currency: {
                                        let currency: &str = &fund_currency;
                                        <&Seg>::try_from(currency)
                                            .expect("commodities are valid segments")
                                    },
                                }),
                            |posting| {
                                posting.set_amount(investment_amount_fund);
                            },
                        );
                }
            }
        };

        directives.push(Directive::from(transaction));
        if let Some(date) = record.price_date {
            let share_price = Amount::new(record.share_price, record.fund_currency.unwrap());
            directives.push(Directive::from(Price::new(date, record.isin, share_price)));
        }

        Ok(directives)
    }
}

impl ImporterBuilder {
    pub fn build(&self) -> Result<Importer, ImporterBuilderError> {
        let config =
            Config {
                account_template: self.account_template.clone().context(
                    UninitializedFieldSnafu {
                        field: "account_template",
                        importer: Importer::NAME,
                    },
                )?,

                capital_gains_account: self.capital_gains_account.clone().context(
                    UninitializedFieldSnafu {
                        field: "capital_gains_account",
                        importer: Importer::NAME,
                    },
                )?,

                currency_account: self.currency_account.clone().context(
                    UninitializedFieldSnafu {
                        field: "currency_account",
                        importer: Importer::NAME,
                    },
                )?,

                distributions_template: self.distributions_template.clone().context(
                    UninitializedFieldSnafu {
                        field: "distributions_template",
                        importer: Importer::NAME,
                    },
                )?,

                fee_template: self.fee_template.clone().context(UninitializedFieldSnafu {
                    field: "fee_template",
                    importer: Importer::NAME,
                })?,

                payee: self.payee.clone().context(UninitializedFieldSnafu {
                    field: "payee",
                    importer: Importer::NAME,
                })?,

                portfolio_fee_template: self.portfolio_fee_template.clone().context(
                    UninitializedFieldSnafu {
                        field: "portfolio_fee_template",
                        importer: Importer::NAME,
                    },
                )?,

                reference_account: self.reference_account.clone().context(
                    UninitializedFieldSnafu {
                        field: "reference_account",
                        importer: Importer::NAME,
                    },
                )?,
            };

        Ok(Importer::new(config))
    }
}

#[derive(Clone, Copy, Debug, Deserialize)]
pub struct Record<'r> {
    #[serde(rename = "Bestandsdatum", with = "dmy_short::opt")]
    balance_date: Option<Date>,

    #[serde(
        rename = "Anteile zum Bestandsdatum",
        with = "decimal_parsers::german::serde::with_precision::<6>"
    )]
    balance_shares: Decimal,

    #[serde(rename = "Buchungsdatum", with = "dmy_short")]
    booking_date: Date,

    #[serde(rename = "Depotnummer")]
    depot_id: &'r str,

    #[serde(rename = "Depotposition")]
    depot_position: &'r str,

    #[serde(rename = "Ertragswährung (EW)")]
    distribution_currency: Option<Commodity>,

    #[serde(
        rename = "Barausschüttung/Steuerliquidität in ZW",
        with = "decimal_parsers::german::serde::with_precision::<2>"
    )]
    distribution_total: Decimal,

    #[serde(
        rename = "Devisenkurs  (ZW/FW)",
        with = "german_decimal::serde::opt::with_precision::<6>"
    )]
    exchange_rate_payment_fund: Option<Decimal>,

    #[serde(
        rename = "Devisenkurs (EUR/ZW)",
        with = "decimal_parsers::german::serde::opt::with_precision::<6>"
    )]
    exchange_rate_eur_payment: Option<Decimal>,

    #[serde(
        rename = "Entgelt in ZW",
        with = "german_decimal::serde::with_precision::<2>"
    )]
    fees: Decimal,

    #[serde(rename = "Fondswährung (FW)")]
    fund_currency: Option<Commodity>,

    #[serde(rename = "Fonds")]
    fund_name: &'r str,

    #[serde(
        rename = "Anlagebetrag in ZW",
        with = "german_decimal::serde::with_precision::<2>"
    )]
    investment_amount: Decimal,

    #[serde(rename = "ISIN")]
    isin: Commodity,

    #[serde(
        rename = "Zahlungsbetrag in ZW",
        with = "german_decimal::serde::with_precision::<2>"
    )]
    payment_amount: Decimal,

    #[serde(rename = "Zahlungswährung (ZW)")]
    payment_currency: Commodity,

    #[serde(rename = "Kursdatum", with = "dmy_short::opt")]
    price_date: Option<Date>,

    #[serde(rename = "Ref. Nr.")]
    reference: TransactionReference<'r>,

    #[serde(
        rename = "Abrechnungskurs in FW",
        with = "german_decimal::serde::with_precision::<6>"
    )]
    share_price: Decimal,

    #[serde(
        rename = "Anteile",
        with = "german_decimal::serde::with_precision::<6>"
    )]
    shares: Decimal,

    #[serde(rename = "Umsatzart")]
    transaction_kind: TransactionKind,
}

#[derive(Clone, Copy, Debug)]
pub enum TemplateSelector {
    DepotId,
    Position,
}

impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;

    fn from_str(selector: &str) -> Result<Self, Self::Err> {
        let selector = match selector {
            "depot_id" => Self::DepotId,
            "position" => Self::Position,
            selector => return TemplateSelectorSnafu { selector }.fail(),
        };

        Ok(selector)
    }
}

#[derive(Debug, Diagnostic, Snafu)]
#[snafu(display("unsupported context selector: {selector:?}"))]
pub struct TemplateSelectorError {
    pub selector: String,

    pub backtrace: Backtrace,
}

#[derive(Clone, Copy, Debug)]
struct CurrencyTemplateContext<'c> {
    pub currency: &'c Seg,
}

impl Index<&CurrencyTemplateSelector> for CurrencyTemplateContext<'_> {
    type Output = Seg;

    fn index(&self, index: &CurrencyTemplateSelector) -> &Self::Output {
        match index {
            CurrencyTemplateSelector::Currency => self.currency,
        }
    }
}

#[derive(Clone, Copy, Debug)]
struct TemplateContext<'c> {
    pub depot_id: &'c Seg,
    pub position: &'c Seg,
}

impl Index<&TemplateSelector> for TemplateContext<'_> {
    type Output = Seg;

    fn index(&self, index: &TemplateSelector) -> &Self::Output {
        match index {
            TemplateSelector::DepotId => self.depot_id,
            TemplateSelector::Position => self.position,
        }
    }
}

#[derive(Clone, Copy, Debug, Deserialize)]
enum TransactionKind {
    #[serde(rename = "Fondsertrag (Ausschüttung)")]
    Distribution,
    #[serde(rename = "Entgeltbelastung Verkauf")]
    PortfolioFee,
    #[serde(rename = "Vorabpauschale")]
    LumpSumTaxAdvance,
    #[serde(rename = "Kauf")]
    Purchase,
    #[serde(rename = "Wiederanlage Fondsertrag")]
    Reinvest,
    #[serde(rename = "Ansparplan")]
    SavingsPlan,
    #[serde(rename = "Neuabrechnung Ansparplan")]
    SavingsPlanRebooking,
    #[serde(rename = "Stornierung Ansparplan")]
    SavingsPlanReversal,
}

impl TransactionKind {
    fn format_narration(&self, fund: &str) -> String {
        match self {
            Self::Distribution => format!("Distribution of {fund}"),
            Self::PortfolioFee => format!("Sell {fund} to cover portfolio fees"),
            Self::LumpSumTaxAdvance => format!("Lump-sum tax advance for {fund}"),
            Self::Purchase => format!("Buy {fund}"),
            Self::Reinvest => format!("Reinvest distribution of {fund}"),
            Self::SavingsPlan => format!("Savings plan for {fund}"),
            Self::SavingsPlanRebooking => format!("Rebook savings plan for {fund}"),
            Self::SavingsPlanReversal => format!("Reverse savings plan for {fund}"),
        }
    }
}

impl TryFrom<&str> for TransactionKind {
    type Error = Error;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let kind = match value {
            "Ansparplan" => Self::SavingsPlan,
            "Entgeltbelastung Verkauf" => Self::PortfolioFee,
            "Fondsertrag (Ausschüttung)" => Self::Distribution,
            "Kauf" => Self::Purchase,
            "Neuabrechnung Ansparplan" => Self::SavingsPlanRebooking,
            "Stornierung Ansparplan" => Self::SavingsPlanReversal,
            "Vorabpauschale" => Self::LumpSumTaxAdvance,
            "Wiederanlage Fondsertrag" => Self::Reinvest,

            _ => return UnsupportedTransactionKindSnafu { value }.fail(),
        };

        Ok(kind)
    }
}

#[derive(Clone, Copy, Debug)]
struct TransactionReference<'r> {
    order_date: Date,
    transaction_id: &'r str,
}

impl<'de, 'r> serde::Deserialize<'de> for TransactionReference<'r>
where
    'de: 'r,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct Visitor;

        impl<'de> serde::de::Visitor<'de> for Visitor {
            type Value = TransactionReference<'de>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("an ebase transaction reference number")
            }

            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                let (transaction_id, order_date) = v
                    .split_once('/')
                    .ok_or(E::custom("unexpected format for transaction reference"))?;

                let order_date = Date::parse(
                    order_date,
                    time::macros::format_description!("[day][month][year]"),
                )
                .map_err(E::custom)?;

                Ok(TransactionReference {
                    order_date,
                    transaction_id,
                })
            }
        }

        deserializer.deserialize_str(Visitor)
    }
}

impl std::fmt::Display for TransactionReference<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut buffer = [0u8; 8];

        f.write_str(self.transaction_id)?;
        f.write_char('/')?;
        self.order_date
            .format_into(
                &mut buffer.as_mut_slice(),
                format_description!("[day][month][year]"),
            )
            .expect("valid formatting");

        f.write_str(core::str::from_utf8(&buffer).expect("valid UTF-8"))
    }
}