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::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 camino::Utf8Path;
use csv::Record;
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::Tap as _;
use tap::TapFallible;
use time::macros::format_description;
use time::parsing::Parsed;
use time::Date;
#[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: &Utf8Path) -> Result<beancount_types::Account, Self::Error> {
        Ok(self.config.account_template.base().to_owned())
    }
    fn date(&self, file: &Utf8Path) -> Option<Result<Date, Self::Error>> {
        self.importer
            .date(file, self)
            .map(|result| result.map_err(Self::Error::from))
    }
    fn extract(
        &self,
        file: &Utf8Path,
        existing: &[Directive],
    ) -> Result<Vec<Directive>, Self::Error> {
        self.importer
            .extract(file, existing, self)
            .map_err(Report::from)
    }
    fn filename(&self, _file: &Utf8Path) -> Option<Result<String, Self::Error>> {
        Some(Ok(String::from("transactions.csv")))
    }
    fn identify(&self, file: &Utf8Path) -> 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;
    fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
        Some(parse_transaction_date(record))
    }
    fn extract(
        &self,
        _existing: &[Directive],
        record: &Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        const DEPOT_ID_COLUMN_INDEX: usize = 0;
        const DEPOT_POSITION_COLUMN_INDEX: usize = 1;
        const FUND_CURRENCY_COLUMN_INDEX: usize = 12;
        const FUND_NAME_COLUMN_INDEX: usize = 6;
        const ISIN_COLUMN_INDEX: usize = 7;
        const REFERENCE_COLUMN_INDEX: usize = 2;
        const SHARES_COLUMN_INDEX: usize = 10;
        const PRICE_DATE_COLUMN_INDEX: usize = 13;
        const PAYMENT_AMOUNT_COLUMN_INDEX: usize = 8;
        const PAYMENT_CURRENY_COLUMN_INDEX: usize = 9;
        const TRANSACTION_KIND_COLUMN_INDEX: usize = 4;
        const SHARE_PRICE_COLUMN_INDEX: usize = 11;
        const EXCHANGE_RATE_COLUMN_INDEX: usize = 14;
        // TODO improve error handling
        let kind = TransactionKind::try_from(&record[TRANSACTION_KIND_COLUMN_INDEX])?;
        let transaction_id = &record[REFERENCE_COLUMN_INDEX];
        let (reference, ..) = transaction_id.split_once('/').unwrap();
        let fund_commodity = Commodity::try_from(&record[ISIN_COLUMN_INDEX]).unwrap();
        let depot_id = &record[DEPOT_ID_COLUMN_INDEX];
        let position = &record[DEPOT_POSITION_COLUMN_INDEX];
        let context = TemplateContext {
            depot_id: <&Seg>::try_from(depot_id).unwrap(),
            position: <&Seg>::try_from(position).unwrap(),
        };
        let fund_currency = Commodity::try_from(&record[FUND_CURRENCY_COLUMN_INDEX]).unwrap();
        let share_price = Amount::new(
            german_decimal::parse(&record[SHARE_PRICE_COLUMN_INDEX]).unwrap(),
            fund_currency,
        );
        let shares_amount = Amount::new(
            german_decimal::parse(&record[SHARES_COLUMN_INDEX])
                .map_err(|_| -> Error { todo!() })?,
            fund_commodity,
        );
        let payment_currency = Commodity::try_from(&record[PAYMENT_CURRENY_COLUMN_INDEX])
            .map_err(|_| -> Error { todo!() })?;
        let payment_amount = {
            let amount = parse_decimal_with_precision(&record[PAYMENT_AMOUNT_COLUMN_INDEX], 2)
                .map_err(|_| -> Error { todo!() })?;
            Amount::new(amount, payment_currency)
        };
        let investment_amount_payment = {
            let investment_amount = &record[15];
            let investment_amount = parse_decimal_with_precision(investment_amount, 2)
                .map_err(|_| -> Error { todo!() })?;
            Amount::new(investment_amount, payment_currency)
        };
        let investment_amount_fund = {
            let exchange_rate = Some(&record[EXCHANGE_RATE_COLUMN_INDEX])
                .filter(|value| !value.is_empty())
                .map(german_decimal::parse)
                .transpose()
                .map_err(|_| -> Error { todo!() })?;
            exchange_rate.map(|rate| {
                let mut amount = investment_amount_payment.amount * rate;
                amount.rescale(2);
                Amount::new(amount, fund_currency)
            })
        };
        let fees = german_decimal::parse(&record[26])
            .unwrap()
            .tap_mut(|amount| {
                amount.rescale(2);
            });
        let mut transaction = Transaction::on(parse_transaction_date(record)?);
        transaction
            .set_payee(&self.config.payee)
            .set_narration(kind.format_narration(&record[FUND_NAME_COLUMN_INDEX]))
            .add_meta(common_keys::TRANSACTION_ID, transaction_id);
        match kind {
            TransactionKind::Purchase | TransactionKind::SavingsPlan => {
                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 !fees.is_zero() {
                    let fees = Amount::new(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
                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(Amount::new(fees, payment_currency));
                        },
                    )
                    .build_posting(
                        self.config.capital_gains_account.render(&context),
                        |_posting| {},
                    );
            }
            TransactionKind::Reinvest => {
                transaction.build_posting(
                    self.config.account_template.render(&context),
                    |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    },
                );
                if !fees.is_zero() {
                    let fees = Amount::new(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.distributions_template.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 = german_decimal::parse(&record[14]).unwrap();
                            posting.set_price(Amount::new(exchange_rate, fund_currency));
                        }
                    },
                );
            }
            TransactionKind::SavingsPlanRebooking => {
                transaction.build_posting(
                    self.config.account_template.render(&context),
                    |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    },
                );
                if !fees.is_zero() {
                    let fees = Amount::new(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 = german_decimal::parse(&record[14]).unwrap();
                            posting.set_price(Amount::new(exchange_rate, fund_currency));
                        }
                    },
                );
            }
            TransactionKind::SavingsPlanReversal => {
                transaction.build_posting(
                    self.config.account_template.render(&context),
                    |posting| {
                        posting
                            .set_amount(shares_amount)
                            .set_cost(CostBasis::PerUnit(share_price));
                    },
                );
                if !fees.is_zero() {
                    let fees = Amount::new(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);
                            },
                        );
                }
            }
        };
        let price = {
            let date = parse_date(&record[PRICE_DATE_COLUMN_INDEX])?;
            Price::new(date, fund_commodity, share_price)
        };
        Ok(vec![Directive::from(transaction), Directive::from(price)])
    }
}
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)]
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(Debug)]
enum TransactionKind {
    PortfolioFee,
    Purchase,
    Reinvest,
    SavingsPlan,
    SavingsPlanRebooking,
    SavingsPlanReversal,
}
impl TransactionKind {
    fn format_narration(&self, fund: &str) -> String {
        match self {
            TransactionKind::PortfolioFee => format!("Sell {fund} to cover portfolio fees"),
            TransactionKind::Purchase => format!("Buy {fund}"),
            TransactionKind::Reinvest => format!("Reinvest distribution of {fund}"),
            TransactionKind::SavingsPlan => format!("Savings plan for {fund}"),
            TransactionKind::SavingsPlanRebooking => format!("Rebook savings plan for {fund}"),
            TransactionKind::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,
            "Kauf" => Self::Purchase,
            "Neuabrechnung Ansparplan" => Self::SavingsPlanRebooking,
            "Stornierung Ansparplan" => Self::SavingsPlanReversal,
            "Wiederanlage Fondsertrag" => Self::Reinvest,
            _ => return UnsupportedTransactionKindSnafu { value }.fail(),
        };
        Ok(kind)
    }
}
pub fn parse_transaction_date(record: &Record) -> Result<Date, Error> {
    const TRANSACTION_DATE_COLUMN_INDEX: usize = 3;
    parse_date(&record[TRANSACTION_DATE_COLUMN_INDEX])
}
pub fn parse_date(date: &str) -> Result<Date, Error> {
    const BASE_YEAR: i32 = 2000;
    const DATE_FORMAT: &[time::format_description::FormatItem] =
        format_description!("[day].[month].[year repr:last_two]");
    let mut parsed = Parsed::new();
    parsed.parse_items(date.as_bytes(), DATE_FORMAT).unwrap();
    let year_last_two = i32::from(parsed.year_last_two().unwrap());
    parsed.set_year(BASE_YEAR + year_last_two);
    parsed.try_into().map_err(Error::from)
}
fn parse_decimal_with_precision(
    decimal: &str,
    precision: u32,
) -> Result<Decimal, rust_decimal::Error> {
    german_decimal::parse(decimal).tap_ok_mut(|amount| {
        amount.rescale(precision);
    })
}