extern crate alloc;

use core::fmt;
use core::fmt::Display;
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::common_keys;
use beancount_types::Account;
use beancount_types::AccountTemplate;
use beancount_types::Amount;
use beancount_types::Commodity;
use beancount_types::CostBasis;
use beancount_types::CostSpec;
use beancount_types::Directive;
use beancount_types::MetadataKey;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use hashbrown::HashMap;
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 commodity: Commodity,

    #[builder(setter(into), try_setter)]
    pub express_trade_recipient: AccountTemplate<TemplateSelector>,

    #[builder(setter(into), try_setter)]
    pub express_trade_fees: AccountTemplate<TemplateSelector>,

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

    #[builder(setter(into), try_setter)]
    pub fee_account: AccountTemplate<TemplateSelector>,

    #[builder(setter(into, strip_option))]
    pub fee_payee: Option<String>,

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

    #[builder(setter(into), try_setter)]
    pub savings_bonds_account: AccountTemplate<TemplateSelector>,
    #[builder(setter(into), try_setter)]
    pub savings_bonds_commodity: Commodity,
}

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

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

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

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

    #[must_use]
    pub fn builder() -> ImporterBuilder {
        ImporterBuilder::default()
    }

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

impl Importer {
    fn build_transfer_transaction(
        &self,
        record: Record,
        transaction: &mut Transaction,
        prefix: &str,
    ) {
        let (payee, iban) = record
            .description
            .strip_prefix(prefix)
            .and_then(|rest| {
                let (recipient_name, rest) = rest.split_once(", IBAN ")?;
                let recipient_iban = rest.split_once(", ").map_or(rest, |(iban, _)| iban);

                Some((recipient_name.trim(), recipient_iban.trim()))
            })
            .expect("structured narration");

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

        transaction
            .set_payee(payee)
            .add_meta(
                MetadataKey::try_from("other-iban").expect("valid metadata key"),
                iban,
            )
            .build_posting(opposite_account, |_posting| {});
    }
}

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] = &["Datum", "Beschreibung", "Beschreibung2", "Wert"];

        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 {
        record.date
    }

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        let date = record.date;

        let mut transaction = Transaction::on(date);

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

        let transaction_kind = record.transaction_kind;

        let amount = Amount::new(record.amount, self.config.commodity);

        transaction
            .set_narration(format!("{transaction_kind}"))
            .build_posting(account, |posting| {
                posting.set_amount(amount);
            });

        match transaction_kind {
            TransactionKind::AccountManagementCharge | TransactionKind::ActivityBonus => {
                if let Some(ref payee) = self.config.fee_payee {
                    transaction.set_payee(payee);
                }

                transaction.build_posting(self.config.fee_account.render(&context), |_posting| {});
            }

            TransactionKind::Credit => {
                self.build_transfer_transaction(record, &mut transaction, "Absender  ");
            }
            TransactionKind::Remittance => {
                self.build_transfer_transaction(record, &mut transaction, "Empfaenger  ");
            }

            TransactionKind::PurchaseFee { .. } => {
                if let Some(payee) = &self.config.fee_payee {
                    transaction.set_payee(payee);
                }

                transaction.build_posting(
                    self.config.express_trade_fees.render(&context),
                    |_posting| {},
                );
            }
            TransactionKind::SendFriendsMoney {
                purchase_id,
                recipient,
            } => {
                transaction
                    .set_payee(recipient)
                    .add_meta(common_keys::TRANSACTION_ID, purchase_id)
                    .build_posting(
                        self.config.express_trade_recipient.render(&context),
                        |_posting| {},
                    );
            }

            TransactionKind::SavingsBondCreation { id } => {
                transaction.build_posting(
                    self.config.savings_bonds_account.render(&context),
                    |posting| {
                        let mut cost = CostSpec::from(CostBasis::PerUnit(-amount));
                        cost.set_label(id);
                        posting
                            .set_amount(Amount::new(
                                Decimal::ONE,
                                self.config.savings_bonds_commodity,
                            ))
                            .set_cost(cost);
                    },
                );
            }
            TransactionKind::SavingsBondPayout { id } => {
                transaction.build_posting(
                    self.config.savings_bonds_account.render(&context),
                    |posting| {
                        let amount =
                            Amount::new(-Decimal::ONE, self.config.savings_bonds_commodity);
                        posting.set_amount(amount).set_cost(CostSpec::labelled(id));
                    },
                );
            }
        }

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

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

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

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

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

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

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

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

            known_ibans: self.known_ibans.clone(),

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

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

        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 = "Datum", with = "dmy")]
    date: Date,

    #[serde(rename = "Beschreibung")]
    transaction_kind: TransactionKind<'r>,

    #[serde(rename = "Beschreibung2")]
    description: &'r str,

    #[serde(rename = "Wert", with = "german_decimal::serde")]
    amount: Decimal,
}

#[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,
}

#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(try_from = "&str")]
enum TransactionKind<'i> {
    AccountManagementCharge,
    ActivityBonus,
    Credit,
    Remittance,

    PurchaseFee {
        purchase_id: &'i str,
    },
    SendFriendsMoney {
        purchase_id: &'i str,
        recipient: &'i str,
    },

    SavingsBondCreation {
        id: &'i str,
    },
    SavingsBondPayout {
        id: &'i str,
    },
}

impl Display for TransactionKind<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            TransactionKind::AccountManagementCharge => f.write_str("Account Management Charge"),
            TransactionKind::ActivityBonus => f.write_str("Activity Bonus"),

            TransactionKind::Credit => f.write_str("Incoming transfer"),
            TransactionKind::Remittance => f.write_str("Outgoing transfer"),

            TransactionKind::PurchaseFee { purchase_id } => {
                write!(f, "Express trade fee for {purchase_id}")
            }
            TransactionKind::SendFriendsMoney { purchase_id, .. } => {
                write!(f, "Express trade purchase {purchase_id}")
            }

            TransactionKind::SavingsBondCreation { id } => {
                write!(f, "Savings bond purchase {id}")
            }
            TransactionKind::SavingsBondPayout { id } => write!(f, "Savings bond payout {id}"),
        }
    }
}

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

    fn try_from(s: &'i str) -> Result<Self, Self::Error> {
        match s {
            "Aktivitaetsbonus" => return Ok(Self::ActivityBonus),
            "Kontofuehrung" => return Ok(Self::AccountManagementCharge),
            "Ueberweisung" => return Ok(Self::Remittance),
            _ => {}
        }

        if s.starts_with("Ueberweisung") {
            return Ok(Self::Remittance);
        } else if s.starts_with("Gutschrift") {
            return Ok(Self::Credit);
        } else if let Some(rest) = s.strip_prefix("Sparbrief ") {
            let (id, _) = rest.split_once(", ").expect("structured narration format");
            return Ok(Self::SavingsBondCreation { id });
        } else if let Some(rest) = s.strip_prefix("Auszahlung Sparbrief ") {
            let (id, _) = rest.split_once(", ").expect("structured narration format");
            return Ok(Self::SavingsBondPayout { id });
        } else if let Some(purchase_id) = s.strip_prefix("Kaeufergebuehr ") {
            return Ok(Self::PurchaseFee { purchase_id });
        } else if let Some(rest) = s.strip_prefix(r#""Freunden Geld senden" an Benutzer "#) {
            let (recipient, purchase_id) = rest.split_once("  ").expect("structured narration");
            return Ok(Self::SendFriendsMoney {
                purchase_id,
                recipient,
            });
        }

        todo!("unsupported transaction kind {s:?}")
    }
}

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