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::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::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 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/order-history";

    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("orders.csv")))
    }

    fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: &[&str] = &[
            "Website",
            "Order ID",
            "Order Date",
            "Purchase Order Number",
            "Currency",
            "Unit Price",
            "Unit Price Tax",
            "Shipping Charge",
            "Total Discounts",
            "Total Owed",
            "Shipment Item Subtotal",
            "Shipment Item Subtotal Tax",
            "ASIN",
            "Product Condition",
            "Quantity",
            "Payment Instrument Type",
            "Order Status",
            "Shipment Status",
            "Ship Date",
            "Shipping Option",
            "Shipping Address",
            "Billing Address",
            "Carrier Name & Tracking Number",
            "Product Name",
            "Gift Message",
            "Gift Sender Name",
            "Gift Recipient Contact Details",
        ];

        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.order_timestamp.date()
    }

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

        let amount = {
            Amount::new(
                record.total_owed.tap_mut(|amount| amount.rescale(2)),
                record.currency,
            )
        };

        let asin = record.asin;
        let product_name = record.product_name;

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

            transaction
                .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)
                .add_meta(
                    common_keys::TIMESTAMP,
                    record.order_timestamp.format(&Rfc3339).unwrap(),
                )
                .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)]
pub struct Record<'r> {
    #[serde(rename = "Order ID")]
    order_id: &'r str,

    #[serde(rename = "Order Date", with = "time::serde::iso8601")]
    order_timestamp: OffsetDateTime,

    #[serde(rename = "Currency")]
    currency: Commodity,

    #[serde(rename = "Total Owed")]
    total_owed: Decimal,

    #[serde(rename = "ASIN")]
    asin: &'r str,

    #[serde(rename = "Product Name")]
    product_name: &'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,
}