use core::str::FromStr as _;
use std::collections::HashMap;
use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
use beancount_types::Account;
use beancount_types::Amount;
use beancount_types::Commodity;
use beancount_types::Directive;
use beancount_types::MetadataKey;
use beancount_types::Transaction;
use camino::Utf8Path;
use derive_builder::Builder;
use miette::IntoDiagnostic;
use rust_decimal::Decimal;
use serde::Deserialize;
use serde::Deserializer;
use snafu::OptionExt as _;
use time::format_description::well_known::Rfc3339;
use time::macros::format_description;
use time::PrimitiveDateTime;
use time_tz::PrimitiveDateTimeExt;
use tracing::warn;
#[derive(Builder, Clone, Debug, Deserialize)]
#[builder(
    build_fn(error = "ImporterBuilderError", skip),
    name = "ImporterBuilder"
)]
pub struct Config {
    #[builder(setter(into), try_setter)]
    pub base_account: Account,
    #[builder(field(type = "HashMap<String, LocationInformation>"))]
    pub locations: HashMap<String, LocationInformation>,
    #[builder(field(type = "Option<String>"), setter(into, strip_option))]
    pub payee: Option<String>,
    #[builder(setter(into), try_setter)]
    pub reference_account: Account,
}
#[derive(Debug, Deserialize)]
pub struct Importer {
    config: Config,
}
impl Importer {
    pub const NAME: &str = "ecus/transactions";
    pub fn new(config: Config) -> Self {
        Self { config }
    }
    pub fn builder() -> ImporterBuilder {
        ImporterBuilder::default()
    }
}
impl beancount_importers_framework::ImporterProtocol for Importer {
    type Error = miette::Report;
    fn account(&self, _file: &Utf8Path) -> Result<Account, Self::Error> {
        todo!()
    }
    fn extract(
        &self,
        file: &Utf8Path,
        _existing: &[Directive],
    ) -> Result<Vec<Directive>, Self::Error> {
        let ecus_transactions: Vec<EcusTransaction> = {
            let data = std::fs::read_to_string(file).into_diagnostic()?;
            serde_json::from_str(&data).into_diagnostic()?
        };
        let transactions = ecus_transactions
            .into_iter()
            .filter_map(|ecus_transaction| {
                let EcusTransaction {
                    id,
                    date,
                    location,
                    checkout_counter,
                    typ,
                    mut amount,
                } = ecus_transaction;
                let mut transaction = Transaction::on(date.date());
                let timestamp = date.assume_timezone(time_tz::timezones::db::CET).unwrap();
                if let Some(payee) = &self.config.payee {
                    transaction.set_payee(payee);
                }
                let (narration, target_account) = match typ {
                    TransactionType::Sale => {
                        if let Some(LocationInformation { narration, account }) =
                            self.config.locations.get(&location)
                        {
                            (narration.as_deref(), account)
                        } else {
                            warn!(%location, "ignoring transaction at unknown location");
                            return None;
                        }
                    }
                    TransactionType::Card => (Some("ECUS Charge"), &self.config.reference_account),
                };
                let commodity = Commodity::try_from("EUR").unwrap();
                amount.rescale(2);
                let amount = Amount::new(amount, commodity);
                if let Some(narration) = narration {
                    transaction.set_narration(narration);
                }
                transaction
                    .add_meta(MetadataKey::from_str("transaction-id").unwrap(), id)
                    .add_meta(MetadataKey::from_str("counter").unwrap(), checkout_counter)
                    .add_meta(
                        MetadataKey::from_str("timestamp").unwrap(),
                        timestamp.format(&Rfc3339).unwrap(),
                    )
                    .build_posting(&self.config.base_account, |posting| {
                        posting.set_amount(amount);
                    })
                    .build_posting(target_account, |_| {});
                Some(transaction)
            })
            .map(Directive::from)
            .collect();
        Ok(transactions)
    }
    fn identify(&self, _file: &Utf8Path) -> Result<bool, Self::Error> {
        Ok(false)
    }
    fn name(&self) -> &'static str {
        Self::NAME
    }
    #[doc(hidden)]
    fn typetag_deserialize(&self) {}
}
impl ImporterBuilder {
    pub fn clear_locations(&mut self) -> &mut Self {
        self.locations.clear();
        self
    }
    pub fn add_location(
        &mut self,
        name: impl Into<String>,
        info: LocationInformation,
    ) -> &mut Self {
        self.locations.insert(name.into(), info);
        self
    }
}
impl ImporterBuilder {
    pub fn build(&self) -> Result<Importer, ImporterBuilderError> {
        let config = Config {
            base_account: self.base_account.clone().context(UninitializedFieldSnafu {
                field: "base_account",
                importer: Importer::NAME,
            })?,
            locations: self.locations.clone(),
            payee: self.payee.clone(),
            reference_account: self
                .reference_account
                .clone()
                .context(UninitializedFieldSnafu {
                    field: "reference_account",
                    importer: Importer::NAME,
                })?,
        };
        Ok(Importer::new(config))
    }
}
#[derive(Clone, Debug, Deserialize)]
pub struct LocationInformation {
    pub account: Account,
    pub narration: Option<String>,
}
#[derive(Debug, Deserialize)]
struct EcusTransaction {
    #[serde(rename = "transFullId")]
    id: String,
    #[serde(rename = "datum", deserialize_with = "german_date_time")]
    date: PrimitiveDateTime,
    #[serde(rename = "ortName")]
    location: String,
    #[serde(rename = "kaName")]
    checkout_counter: String,
    #[serde(rename = "typName")]
    typ: TransactionType,
    #[serde(rename = "zahlBetrag")]
    amount: Decimal,
}
#[derive(Debug, Deserialize)]
enum TransactionType {
    #[serde(rename = "Karte")]
    Card,
    #[serde(rename = "Verkauf")]
    Sale,
}
fn german_date_time<'de, D>(deserializer: D) -> Result<PrimitiveDateTime, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_str({
        struct Visitor;
        impl serde::de::Visitor<'_> for Visitor {
            type Value = PrimitiveDateTime;
            fn expecting(&self, _formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                todo!()
            }
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                PrimitiveDateTime::parse(
                    v,
                    format_description!("[day].[month].[year] [hour]:[minute]"),
                )
                .map_err(E::custom)
            }
        }
        Visitor
    })
}