use core::ops::Index;
use core::str::FromStr;

use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
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::MetadataKey;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
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 tap::Tap as _;
use time::Date;

#[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 base_account: AccountTemplate<TemplateSelector>,

    pub expense_account: AccountTemplate<TemplateSelector>,
}

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

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

impl Importer {
    pub const NAME: &str = "amazon/digital-items";

    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.base_account.base().to_owned())
    }

    fn date(&self, buffer: &[u8]) -> Option<Result<time::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("digital-items.csv")))
    }

    fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: &[&str] = &[
            "ASIN",
            "Title",
            "OrderId",
            "DigitalOrderItemId",
            "DeclaredCountryCode",
            "BaseCurrencyCode",
            "FulfilledDate",
            "IsFulfilled",
            "Marketplace",
            "OrderDate",
            "OriginalQuantity",
            "OurPrice",
            "OurPriceCurrencyCode",
            "OurPriceTax",
            "OurPriceTaxCurrencyCode",
            "SellerOfRecord",
            "Publisher",
            "ThirdPartyDisplayPrice",
            "ThirdPartyDisplayCurrencyCode",
            "ListPriceAmount",
            "ListPriceCurrencyCode",
            "ListPriceTaxAmount",
            "ListPriceTaxCurrencyCode",
            "GiftItem",
            "OrderingCustomerNickname",
            "GiftCustomerNickname",
            "GiftMessage",
            "GiftEmail",
            "RecipientEmail",
            "GiftRedemption",
            "ItemMergedFromAnotherOrder",
            "QuantityOrdered",
            "ItemFulfilled",
            "ShipFrom",
            "ShipTo",
            "IsOrderEligibleForPrimeBenefit",
            "OfferingSKU",
            "FulfillmentMobileNumber",
            "RechargeAmount",
            "RechargeAmountCurrencyCode",
            "SubscriptionOrderInfoList",
            "PreviouslyPaidDigitalOrderItemId",
            "PreviouslyPaidOrderId",
            "InstallmentOurPrice",
            "InstallmentOurPricePlusTax",
            "DigitalOrderItemAttributes",
            "InstallmentOurPriceCurrencyCode",
            "InstallmentOurPricePlusTaxCurrencyCode",
        ];

        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 {
        Date::max(
            record.order_date,
            record.fulfilled_date.unwrap_or(Date::MIN),
        )
    }

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

        let asin = record.asin;
        let product_name = record.title;
        let order_id = record.order_id;

        let amount = Amount::new(
            record.our_price_tax.tap_mut(|amount| amount.rescale(2)),
            record.our_price_tax_currency_code,
        );

        let transaction = Transaction::on(date).tap_mut(|transaction| {
            let context = {
                let currency: &str = &record.our_price_tax_currency_code;
                // TODO make this conversion in beancount_types
                let currency = <&Seg>::try_from(currency).expect("commodities are valid segments");
                TemplateContext { currency }
            };

            transaction
                .set_payee(record.seller_of_record)
                .set_narration(product_name)
                .add_link(Link::try_from(format!("^amazon.{order_id}")).expect("valid link"))
                .add_meta(MetadataKey::try_from("order-id").unwrap(), order_id)
                .add_meta(MetadataKey::try_from("asin").unwrap(), asin)
                .build_posting(self.config.base_account.render(&context), |posting| {
                    posting.set_amount(-amount);
                })
                // TODO handle auto matching
                .build_posting(self.config.expense_account.render(&context), |_posting| {});
        });

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

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

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

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

#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Record<'r> {
    #[serde(rename = "ASIN")]
    asin: &'r str,

    fulfilled_date: Option<Date>,

    order_date: Date,

    order_id: &'r str,

    our_price_tax: Decimal,

    our_price_tax_currency_code: Commodity,

    seller_of_record: &'r str,

    #[serde(rename = "Title")]
    title: &'r str,
}

#[derive(Clone, Copy, Debug)]
pub struct TemplateContext<'c> {
    pub(crate) 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 {
    pub(crate) selector: String,

    pub(crate) backtrace: Backtrace,
}