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 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::Date;
use time::PrimitiveDateTime;
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 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, _buffer: &[u8]) -> Result<Account, Self::Error> {
        Ok(self.config.base_account.clone())
    }

    fn date(&self, buffer: &[u8]) -> Option<Result<Date, Self::Error>> {
        let ecus_transactions: Vec<EcusTransaction> = match serde_json::from_slice(buffer) {
            Ok(data) => data,
            Err(error) => return Some(Err(error).into_diagnostic()),
        };

        ecus_transactions
            .into_iter()
            .map(|EcusTransaction { date, .. }| date.date())
            .max()
            .map(Ok)
    }

    fn extract(
        &self,
        buffer: &[u8],
        _existing: &[Directive],
    ) -> Result<Vec<Directive>, Self::Error> {
        let ecus_transactions: Vec<EcusTransaction> =
            serde_json::from_slice(buffer).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 {
                            tracing::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 filename(&self, _buffer: &[u8]) -> Option<Result<String, Self::Error>> {
        Some(Ok(String::from("ecus-transactions.json")))
    }

    fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
        match serde_json::from_slice::<Vec<EcusTransaction>>(buffer) {
            Ok(_) => Ok(true),
            Err(_) => 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
    })
}