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::Directive;
use beancount_types::Link;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use iso_currency::Country;
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::format_description::well_known::Iso8601;
use time::format_description::well_known::Rfc3339;
use time::Date;
use time::OffsetDateTime;

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

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

    pub claim_account: AccountTemplate<TemplateSelector>,

    pub payee: String,

    pub payment_account: AccountTemplate<TemplateSelector>,
}

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

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

impl Importer {
    pub const NAME: &str = "apple/store-balance";

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

    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.balance_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(miette::Report::from)
    }

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

    fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: &[&str] = &[
            "Transaction date",
            "Country",
            "Order number",
            "Amount",
            "Transaction type",
        ];

        self.importer
            .identify(buffer, 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.transaction_date.date()
    }

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        let commodity = match record.country {
            Country::DE => Commodity::try_from("EUR").unwrap(),
            _ => todo!(),
        };

        let transaction_id = record.order_number;

        let kind = TransactionKind::try_from(record.transaction_type)?;
        let amount = Amount::new(record.amount, commodity);

        let context = {
            let currency: &str = &amount.commodity;
            // TODO make this conversion in beancount_types
            let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
            TemplateContext { currency }
        };

        let transaction = match kind {
            TransactionKind::Debit => {
                let mut transaction = Transaction::on(record.transaction_date.date());
                transaction
                    .set_payee(&self.config.payee)
                    .set_narration(format!("Payment {transaction_id}"))
                    .add_link(
                        Link::try_from({
                            let order_id =
                                transaction_id.rsplit_once('-').ok_or_else(|| todo!())?.0;
                            format!("^apple.{order_id}")
                        })
                        .map_err(|_| todo!())?,
                    );

                transaction
                    .add_meta(
                        common_keys::TIMESTAMP,
                        record
                            .transaction_date
                            .format(&Rfc3339)
                            .map_err(|_| todo!())?,
                    )
                    .add_meta(common_keys::TRANSACTION_ID, transaction_id)
                    .build_posting(self.config.balance_account.render(&context), |posting| {
                        posting.set_amount(-amount);
                    })
                    .build_posting(self.config.payment_account.render(&context), |_| {});

                transaction
            }

            TransactionKind::Redeem => {
                let mut transaction = Transaction::on(record.transaction_date.date());

                transaction
                    .set_payee(&self.config.payee)
                    .set_narration(format!("Gift card redemption {transaction_id}"))
                    .add_meta(
                        common_keys::TIMESTAMP,
                        record
                            .transaction_date
                            .format(&Rfc3339)
                            .map_err(|_| todo!())?,
                    )
                    .add_meta(common_keys::TRANSACTION_ID, transaction_id)
                    .build_posting(self.config.balance_account.render(&context), |posting| {
                        posting.set_amount(amount);
                    })
                    .build_posting(self.config.claim_account.render(&context), |_| {});

                transaction
            }
        };

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

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,
                })?,

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

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

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

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

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

    #[serde(rename = "Country")]
    country: Country,

    #[serde(rename = "Order number")]
    order_number: &'r str,

    #[serde(
        rename = "Transaction date",
        deserialize_with = "deserialize_local_offset_date_time"
    )]
    transaction_date: OffsetDateTime,

    #[serde(rename = "Transaction type")]
    transaction_type: &'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(Clone, Copy, Debug)]
enum TransactionKind {
    Debit,
    Redeem,
}

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

    fn try_from(kind: &'d str) -> Result<Self, Self::Error> {
        match kind {
            "DEBIT" => Ok(Self::Debit),
            "REDEEM" => Ok(Self::Redeem),
            _ => todo!("unsupported description {kind:?}"),
        }
    }
}

fn deserialize_local_offset_date_time<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
    D: serde::Deserializer<'de>,
{
    use serde::de::Error as _;
    use time_tz::system;
    use time_tz::PrimitiveDateTimeExt as _;
    use time_tz::TimeZone as _;

    let timestamp = iso_primitivedatetime::deserialize(deserializer)?;

    let tz = system::get_timezone().map_err(D::Error::custom)?;
    timestamp.assume_timezone(tz).take_first().ok_or_else(|| {
        D::Error::custom(format!(
            "invalid datetime {timestamp} in timezone {}",
            tz.name()
        ))
    })
}

const PRIMITIVE_DATE_TIME_FORMAT: Iso8601<0> =
    time::format_description::well_known::Iso8601::PARSING;
time::serde::format_description!(
    iso_primitivedatetime,
    PrimitiveDateTime,
    PRIMITIVE_DATE_TIME_FORMAT
);