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::CostBasis;
use beancount_types::Directive;
use beancount_types::Link;
use beancount_types::Price;
use beancount_types::PriceSpec;
use beancount_types::Seg;
use beancount_types::Transaction;
use derive_builder::Builder;
use isin::ISIN;
use miette::Diagnostic;
use miette::IntoDiagnostic;
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(Builder, Clone, Debug, Deserialize)]
#[builder(
    build_fn(error = "ImporterBuilderError", skip),
    name = "ImporterBuilder",
    setter(into),
    try_setter
)]
pub struct Config {
    pub account_template: AccountTemplate<TemplateSelector>,

    pub capital_gains_template: AccountTemplate<TemplateSelector>,

    pub payee: String,

    pub reference_account: AccountTemplate<TemplateSelector>,
}

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

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

    #[serde(default = "csv::Importer::semicolon_delimited", skip_deserializing)]
    importer: csv::Importer,
}

impl Importer {
    const NAME: &str = "bw-bank/portfolio-transactions";

    pub fn builder() -> ImporterBuilder {
        ImporterBuilder::default()
    }

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

impl beancount_importers_framework::ImporterProtocol for Importer {
    type Error = miette::Report;

    fn account(&self, _buffer: &[u8]) -> Result<Account, Self::Error> {
        Ok(self.config.account_template.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(Self::Error::from)
    }

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

    fn identify(&self, buffer: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: &[&str] = &[
            "Isin",
            "Wertpapierbezeichnung",
            "Datum",
            "Ordernummer",
            "Geschäftsart",
            "Stück",
            "Einheit",
            "Kurs",
            "Devisenkurs",
            "Kurswert",
        ];

        self.importer
            .identify(buffer, EXPECTED_HEADERS)
            .into_diagnostic()
    }

    fn name(&self) -> &'static str {
        Self::NAME
    }

    fn typetag_deserialize(&self) {}
}

impl csv::RecordImporter for Importer {
    type Error = Error;
    type Record<'de> = Record<'de>;

    fn date(&self, record: Record) -> Date {
        record.date
    }

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        let isin =
            Commodity::try_from(record.isin.to_string()).expect("ISINs are valid commodities");

        let transaction_kind = record.transaction_kind;
        let security_name = record.security_name;
        let order_id = record.order_id;

        let (amount, cost_basis, price, total) = match transaction_kind {
            TransactionKind::Buy => (
                Amount::new(record.shares, isin),
                CostBasis::PerUnit(record.unit_cost),
                None,
                -record.total_cost,
            ),
            TransactionKind::Sell => (
                -Amount::new(record.shares, isin),
                CostBasis::Empty,
                Some(record.unit_cost),
                record.total_cost,
            ),
        };

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

            transaction
                .set_payee(&self.config.payee)
                .set_narration(format!("{transaction_kind:?} {security_name}"))
                .add_link(Link::try_from(format!("^bw-bank.{order_id}")).unwrap())
                .add_meta(common_keys::TRANSACTION_ID, record.order_id)
                .build_posting(self.config.account_template.render(&context), |posting| {
                    posting.set_amount(amount).set_cost(cost_basis);

                    posting.price = price.map(PriceSpec::from);
                })
                .build_posting(self.config.reference_account.render(&context), |posting| {
                    posting.set_amount(total);
                });

            if let TransactionKind::Sell = transaction_kind {
                transaction
                    .build_posting(self.config.capital_gains_template.render(&context), |_| {});
            }
        });

        let price = Price::new(record.date, isin, record.unit_cost);

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

impl ImporterBuilder {
    pub fn build(&self) -> Result<Importer, ImporterBuilderError> {
        let config =
            Config {
                account_template: self.account_template.clone().context(
                    UninitializedFieldSnafu {
                        field: "account_template",
                        importer: Importer::NAME,
                    },
                )?,

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

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

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

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

#[derive(Clone, Debug, Deserialize)]
// TODO derive Copy once ISIN changes to support that
pub struct Record<'r> {
    #[serde(rename = "Datum", with = "dmy")]
    date: Date,

    #[serde(deserialize_with = "deserialize_isin", rename = "Isin")]
    isin: ISIN,

    #[serde(rename = "Ordernummer")]
    order_id: &'r str,

    #[serde(rename = "Wertpapierbezeichnung")]
    security_name: &'r str,

    #[serde(rename = "Stück", with = "german_decimal::serde")]
    shares: Decimal,

    #[serde(deserialize_with = "deserialize_german_amount", rename = "Kurswert")]
    total_cost: Amount,

    #[serde(rename = "Geschäftsart")]
    transaction_kind: TransactionKind,

    #[serde(deserialize_with = "deserialize_german_amount", rename = "Kurs")]
    unit_cost: Amount,
}

#[derive(Clone, Copy, Debug)]
pub enum TemplateSelector {
    Isin,
}

impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;

    fn from_str(selector: &str) -> Result<Self, Self::Err> {
        match selector {
            "isin" => Ok(Self::Isin),
            _ => TemplateSelectorSnafu { selector }.fail(),
        }
    }
}

#[derive(Debug, Diagnostic, Snafu)]
#[snafu(display("unsupported context selector: {selector:?}"))]
pub struct TemplateSelectorError {
    selector: String,

    backtrace: Backtrace,
}

#[derive(Debug)]
struct TemplateContext<'c> {
    isin: &'c Seg,
}

impl Index<&TemplateSelector> for TemplateContext<'_> {
    type Output = Seg;

    fn index(&self, index: &TemplateSelector) -> &Self::Output {
        match index {
            TemplateSelector::Isin => self.isin,
        }
    }
}

#[derive(Clone, Copy, Debug, Deserialize)]
enum TransactionKind {
    #[serde(rename = "Kauf")]
    Buy,
    #[serde(rename = "Verkauf")]
    Sell,
}

impl FromStr for TransactionKind {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "Kauf" => Ok(Self::Buy),
            "Verkauf" => Ok(Self::Sell),
            _ => todo!(),
        }
    }
}

time::serde::format_description!(dmy, Date, "[day].[month].[year]");

fn deserialize_isin<'de, D>(deserializer: D) -> Result<ISIN, D::Error>
where
    D: serde::Deserializer<'de>,
{
    struct Visitor;

    impl<'de> serde::de::Visitor<'de> for Visitor {
        type Value = ISIN;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("an ISIN")
        }

        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            isin::parse(v).map_err(E::custom)
        }
    }

    deserializer.deserialize_str(Visitor)
}

fn deserialize_german_amount<'de, D>(deserializer: D) -> Result<Amount, D::Error>
where
    D: serde::Deserializer<'de>,
{
    struct Visitor;

    impl<'de> serde::de::Visitor<'de> for Visitor {
        type Value = Amount;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            formatter.write_str("an ISIN")
        }

        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            let Some((amount, currency)) = v.split_once(' ') else {
                return Err(E::custom("unexpected format"));
            };

            let amount = german_decimal::parse(amount).map_err(E::custom)?;
            let currency = Commodity::try_from(currency).map_err(E::custom)?;
            Ok(Amount::new(amount, currency))
        }
    }

    deserializer.deserialize_str(Visitor)
}