extern crate alloc;

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

use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
use beancount_types::Account;
use beancount_types::AccountTemplate;
use beancount_types::Amount;
use beancount_types::Balance;
use beancount_types::Commodity;
use beancount_types::Directive;
use beancount_types::MetadataKey;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use hashbrown::HashMap;
use iso_currency::Currency;
use miette::Diagnostic;
use miette::IntoDiagnostic as _;
use rust_decimal::Decimal;
use serde::Deserialize;
use snafu::Backtrace;
use snafu::OptionExt as _;
use snafu::Snafu;
use time::Date;

// TODO documentation

// TODO balance assertions

// TODO metadata + linking

// TODO adjust accounts

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

    #[builder(setter(into), try_setter)]
    pub fallback_account: Account,

    #[builder(field(type = "HashMap<String, Account>"))]
    pub known_ibans: HashMap<String, Account>,
}

#[derive(Debug, Diagnostic, Snafu)]
pub enum Error {}

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

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

impl fmt::Debug for Importer {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("Importer")
            .field("config", &self.config)
            .field("importer", &self.importer)
            .finish_non_exhaustive()
    }
}

impl Importer {
    const NAME: &str = "vr-bank/transactions";

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

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

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

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

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

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

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

    fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: &[&str] = &[
            "Bezeichnung Auftragskonto",
            "IBAN Auftragskonto",
            "BIC Auftragskonto",
            "Bankname Auftragskonto",
            "Buchungstag",
            "Valutadatum",
            "Name Zahlungsbeteiligter",
            "IBAN Zahlungsbeteiligter",
            "BIC (SWIFT-Code) Zahlungsbeteiligter",
            "Buchungstext",
            "Verwendungszweck",
            "Betrag",
            "Waehrung",
            "Saldo nach Buchung",
            "Bemerkung",
            "Kategorie",
            "Steuerrelevant",
            "Glaeubiger ID",
            "Mandatsreferenz",
        ];

        self.importer
            .identify(buffer, EXPECTED_HEADERS)
            .into_diagnostic()
    }

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

    fn typetag_deserialize(&self) {}
}

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

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

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        let commodity =
            Commodity::try_from(record.currency.code()).expect("currencies are valid commodities");

        // TODO handle transaction
        let transaction = {
            let mut transaction = Transaction::on(record.value_date);

            transaction
                .set_payee(record.transaction_partner_name)
                .set_narration(record.purpose);

            let context = TemplateContext {};
            let account = self.config.account.render(&context);

            let opposite_account = self
                .config
                .known_ibans
                .get(record.transaction_partner_iban)
                .unwrap_or(&self.config.fallback_account);

            transaction
                .add_meta(
                    MetadataKey::try_from("other-iban").expect("valid key"),
                    record.transaction_partner_iban,
                )
                .build_posting(account, |posting| {
                    posting.set_amount(Amount::new(record.amount, commodity));
                })
                .build_posting(opposite_account, |_posting| {});

            transaction
        };

        // TODO create an interface for matchers that can modify transactions

        let balance = Balance::new(
            record.value_date.next_day().unwrap(),
            self.config.account.base(),
            Amount::new(record.balance, commodity),
        );

        Ok(vec![Directive::from(transaction), Directive::from(balance)])
    }
}

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

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

            known_ibans: self.known_ibans.clone(),
        };

        Ok(Importer::new(config))
    }

    pub fn clear_known_ibans(&mut self) -> &mut Self {
        self.known_ibans.clear();
        self
    }

    pub fn try_add_known_iban<A>(
        &mut self,
        iban: impl Into<String>,
        account: A,
    ) -> Result<&mut Self, A::Error>
    where
        A: TryInto<Account>,
    {
        self.known_ibans.insert(iban.into(), account.try_into()?);
        Ok(self)
    }
}

#[derive(Clone, Copy, Debug, Deserialize)]
pub struct Record<'r> {
    #[serde(rename = "Betrag", with = "german_decimal::serde")]
    amount: Decimal,

    #[serde(rename = "Saldo nach Buchung", with = "german_decimal::serde")]
    balance: Decimal,

    #[serde(rename = "Buchungstag", with = "dmy")]
    booking_date: Date,

    #[serde(rename = "Waehrung")]
    currency: Currency,

    #[serde(rename = "Verwendungszweck")]
    purpose: &'r str,

    #[serde(rename = "IBAN Zahlungsbeteiligter")]
    transaction_partner_iban: &'r str,

    #[serde(rename = "Name Zahlungsbeteiligter")]
    transaction_partner_name: &'r str,

    #[serde(rename = "Valutadatum", with = "dmy")]
    value_date: Date,
}

#[derive(Debug)]
struct TemplateContext {}

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

    fn index(&self, selector: &TemplateSelector) -> &Self::Output {
        match *selector {}
    }
}

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

impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;

    fn from_str(selector: &str) -> Result<Self, Self::Err> {
        TemplateSelectorSnafu { selector }.fail()
    }
}

#[derive(Debug, Diagnostic, Snafu)]
pub struct TemplateSelectorError {
    selector: String,

    backtrace: Backtrace,
}

time::serde::format_description!(dmy, Date, "[day].[month].[year]");