extern crate alloc;

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::Balance;
use beancount_types::Commodity;
use beancount_types::Directive;
use beancount_types::Link;
use beancount_types::MetadataKey;
use beancount_types::Posting;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use hashbrown::HashMap;
use miette::Diagnostic;
use miette::IntoDiagnostic as _;
use miette::Report;
use miette::SourceSpan;
use rust_decimal::Decimal;
use serde::Deserialize;
use snafu::Backtrace;
use snafu::OptionExt;
use snafu::Snafu;
use time::format_description::well_known::Rfc3339;
use time::Date;
use time::Time;
use time_tz::PrimitiveDateTimeExt;

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

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

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

    #[builder(field(type = "Option<String>"), setter(into, strip_option))]
    pub deposit_payee: Option<String>,

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

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

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

    #[snafu(display("unknown currency account for commodity: {commodity:?}"))]
    UnknownCurrencyAccount { commodity: Commodity },

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

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 {
    config: Config,

    #[serde(skip_deserializing)]
    importer: csv::Importer,
}

impl Importer {
    pub const NAME: &'static str = "paypal";

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

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

impl Importer {
    fn extract_record(&self, record: Record) -> Result<(Transaction, Balance), Error> {
        let tz = time_tz::timezones::get_by_name(record.timezone).unwrap();
        let timestamp = record
            .date
            .with_time(record.time)
            .assume_timezone(tz)
            .unwrap();

        let context = {
            let currency: &str = &record.currency;
            let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
            TemplateContext { currency }
        };

        let transaction = {
            let mut transaction = Transaction::on(record.date);

            let (payee, opposite_account) = match record.description {
                TransactionKind::Chargeback => (
                    None,
                    self.config
                        .known_recipients
                        .get(record.sender_email)
                        .unwrap_or(&self.config.default_payment_account)
                        .clone(),
                ),
                TransactionKind::CurrencyConversion => {
                    (None, self.config.currency_account.render(&context))
                }
                TransactionKind::Deposit => (
                    self.config.deposit_payee.as_deref(),
                    self.config.reference_accounts[record.bank_account].clone(),
                ),
                TransactionKind::Payment => (
                    non_empty_field(record.name),
                    self.config
                        .known_recipients
                        .get(record.sender_email)
                        .unwrap_or(&self.config.default_payment_account)
                        .clone(),
                ),
            };

            if let Some(payee) = payee {
                transaction.set_payee(payee);
            }

            if matches!(record.description, TransactionKind::Deposit) {
                transaction.add_link(
                    Link::try_from(format!("^paypal.{}", record.transaction_id)).unwrap(),
                );
            }

            transaction
                .add_meta(common_keys::TIMESTAMP, timestamp.format(&Rfc3339).unwrap())
                .add_meta(common_keys::TRANSACTION_ID, record.transaction_id);

            if let Some(invoice_id) = record.invoice_id {
                transaction.add_meta(MetadataKey::from_str("invoice-id").unwrap(), invoice_id);
            }

            if let Some(related_transaction_id) = record.related_transaction_id {
                transaction.add_meta(
                    MetadataKey::from_str("related-transaction-id").unwrap(),
                    related_transaction_id,
                );
            }

            let amount = Amount::new(record.gross, record.currency);

            transaction
                .build_posting(&self.config.balance_account.render(&context), |posting| {
                    posting.set_amount(amount);
                })
                .add_posting(Posting::on(opposite_account));

            transaction
        };

        let balance = {
            let date = record.date.next_day().unwrap();
            let account = self.config.balance_account.render(&context);
            let amount = Amount::new(record.balance, record.currency);

            let mut balance = Balance::new(date, account, amount);
            balance.add_meta(common_keys::TIMESTAMP, timestamp.format(&Rfc3339).unwrap());
            balance
        };
        Ok((transaction, balance))
    }
}

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

    fn account(&self, _file: &[u8]) -> Result<Account, Self::Error> {
        Ok(self.config.balance_account.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(Self::Error::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; 18] = [
            "Datum",
            "Uhrzeit",
            "Zeitzone",
            "Beschreibung",
            "Währung",
            "Brutto",
            "Entgelt",
            "Netto",
            "Guthaben",
            "Transaktionscode",
            "Absender E-Mail-Adresse",
            "Name",
            "Name der Bank",
            "Bankkonto",
            "Versand- und Bearbeitungsgebühr",
            "Umsatzsteuer",
            "Rechnungsnummer",
            "Zugehöriger Transaktionscode",
        ];

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

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        self.extract_record(record).map(|(transaction, balance)| {
            vec![Directive::from(transaction), Directive::from(balance)]
        })
    }
}

impl ImporterBuilder {
    pub fn clear_known_recipients(&mut self) -> &mut Self {
        self.known_recipients.clear();
        self
    }

    pub fn clear_referenece_accounts(&mut self) -> &mut Self {
        self.reference_accounts.clear();
        self
    }

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

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

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

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

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

            deposit_payee: self.deposit_payee.clone(),

            known_recipients: self.known_recipients.clone(),

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

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

#[derive(Debug, Diagnostic, Snafu)]
#[snafu(display("encountered error(s) while extracting transactions"))]
pub struct MultiError {
    #[related]
    errors: Vec<RecordError>,

    #[source_code]
    contents: String,
}

#[derive(Clone, Copy, Debug, Deserialize)]
pub struct Record<'r> {
    #[serde(rename = "Datum", with = "dmy")]
    date: Date,

    #[serde(rename = "Uhrzeit", with = "hms")]
    time: Time,

    #[serde(rename = "Zeitzone")]
    timezone: &'r str,

    #[serde(rename = "Beschreibung")]
    description: TransactionKind,

    #[serde(rename = "Währung")]
    currency: Commodity,

    #[serde(rename = "Brutto", with = "german_decimal::serde")]
    gross: Decimal,

    #[serde(rename = "Guthaben", with = "german_decimal::serde")]
    balance: Decimal,

    #[serde(rename = "Transaktionscode")]
    transaction_id: &'r str,

    #[serde(rename = "Absender E-Mail-Adresse")]
    sender_email: &'r str,

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

    #[serde(rename = "Bankkonto")]
    bank_account: &'r str,

    #[serde(rename = "Rechnungsnummer")]
    invoice_id: Option<&'r str>,

    #[serde(borrow, rename = "Zugehöriger Transaktionscode")]
    related_transaction_id: Option<&'r str>,
}

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

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

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

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

impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;

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

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

    backtrace: Backtrace,
}

#[derive(Debug, Diagnostic, Snafu)]
#[snafu(display("encountered error while extracting record"))]
struct RecordError {
    #[diagnostic_source]
    source: Error,

    #[label("in this record")]
    span: SourceSpan,
}

type Result<T, E = Error> = core::result::Result<T, E>;

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
enum TransactionKind {
    #[serde(rename = "Rückbuchung")]
    Chargeback,

    #[serde(rename = "Allgemeine Währungsumrechnung")]
    CurrencyConversion,

    #[serde(
        alias = "Allgemeine Abbuchung – Bankkonto",
        rename = "Bankgutschrift auf PayPal-Konto"
    )]
    Deposit,

    #[serde(
        alias = "Allgemeine Zahlung",
        alias = "Handyzahlung",
        alias = "PayPal Express-Zahlung",
        alias = "Rückzahlung",
        alias = "Spendenzahlung",
        alias = "Website-Zahlung",
        rename = "Zahlung im Einzugsverfahren mit Zahlungsrechnung"
    )]
    Payment,
}

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

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let kind = match value {
            "Rückbuchung" => Self::Chargeback,

            "Allgemeine Währungsumrechnung" => Self::CurrencyConversion,

            "Allgemeine Abbuchung – Bankkonto" | "Bankgutschrift auf PayPal-Konto" => {
                Self::Deposit
            }

            "Allgemeine Zahlung"
            | "Handyzahlung"
            | "PayPal Express-Zahlung"
            | "Rückzahlung"
            | "Spendenzahlung"
            | "Website-Zahlung"
            | "Zahlung im Einzugsverfahren mit Zahlungsrechnung" => Self::Payment,

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

        Ok(kind)
    }
}

time::serde::format_description!(dmy, Date, "[day].[month].[year]");
time::serde::format_description!(hms, Time, "[hour]:[minute]:[second]");

fn non_empty_field(field: &str) -> Option<&str> {
    Some(field).filter(|value| !value.is_empty())
}