use core::fmt;
use core::fmt::Display;
use core::fmt::Formatter;
use core::ops::Index;
use core::str::FromStr;
use std::collections::HashMap;

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::CostBasis;
use beancount_types::Directive;
use beancount_types::PriceSpec;
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 time::Date;

// TODO documentation

// TODO balance assertions

// TODO metadata + linking

// TODO adjust accounts

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

    #[builder(setter(into), try_setter)]
    pub capital_gains_account: AccountTemplate<TemplateSelector>,

    #[builder(field(type = "HashMap<String, Commodity>"), setter(into))]
    #[serde(default)]
    pub currencies: HashMap<String, Commodity>,

    #[builder(setter(into), try_setter)]
    pub distributions_account: AccountTemplate<TemplateSelector>,

    #[builder(setter(into), try_setter)]
    pub fee_account: AccountTemplate<TemplateSelector>,

    #[builder(setter(into))]
    pub payee: String,

    #[builder(field(type = "HashMap<String, Commodity>"), setter(into))]
    #[serde(default)]
    pub positions: HashMap<String, Commodity>,

    #[builder(setter(into), try_setter)]
    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 = "uniondepot/transactions";

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

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

impl Importer {
    fn lookup_currency(&self, name: &str) -> Result<Commodity, Error> {
        self.config
            .currencies
            .get(name)
            .copied()
            .ok_or_else(|| todo!())
    }

    fn lookup_commodity(&self, position: &str) -> Result<Commodity, Error> {
        self.config
            .positions
            .get(position)
            .copied()
            .ok_or_else(|| todo!())
    }
}

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<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] = &[
            "DATUM",
            "UNIONDEPOTNUMMER",
            "FONDS/PRODUKT",
            "TRANSAKTIONSART",
            "STATUS",
            "FONDSPREIS",
            "ANTEILE",
            "VOLUMEN",
            "EINHEIT",
        ];

        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 context = TemplateContext::try_from(record.depot_id)?;
        let account = self.config.base_account.render(&context);

        let stock = self.lookup_commodity(record.depot_id.position).unwrap();

        let date = record.date;

        let currency: Commodity = self.lookup_currency(record.currency)?;
        let units = record.shares;
        let unit_cost = record.unit_cost;

        let total = Amount::new(record.total, currency);

        let transaction_kind = record.transaction_kind;

        let opposite_account = match transaction_kind {
            TransactionKind::DepositFee | TransactionKind::ForeignFees => {
                self.config.fee_account.render(&context)
            }
            TransactionKind::Distribution => self.config.distributions_account.render(&context),
            TransactionKind::Purchase | TransactionKind::Reversal | TransactionKind::Sale => {
                self.config.reference_account.render(&context)
            }
        };
        let (cost_basis, price) = match transaction_kind {
            TransactionKind::Distribution
            | TransactionKind::Purchase
            | TransactionKind::Reversal => (
                CostBasis::PerUnit(Amount {
                    amount: unit_cost,
                    commodity: currency,
                }),
                None,
            ),
            TransactionKind::DepositFee | TransactionKind::ForeignFees | TransactionKind::Sale => (
                CostBasis::Empty,
                Some(Amount {
                    amount: unit_cost,
                    commodity: currency,
                }),
            ),
        };

        let mut transaction = Transaction::on(date);

        transaction
            .set_payee(&self.config.payee)
            .set_narration(transaction_kind.to_string())
            .build_posting(account, |posting| {
                posting
                    .set_amount(Amount::new(units, stock))
                    .set_cost(cost_basis);

                posting.price = price.map(PriceSpec::from);
            })
            .build_posting(opposite_account, |posting| {
                posting.set_amount(-total);
            });

        if matches!(
            transaction_kind,
            TransactionKind::DepositFee | TransactionKind::ForeignFees | TransactionKind::Sale
        ) {
            // Third leg for capital gains
            transaction.build_posting(
                self.config.capital_gains_account.render(&context),
                |_posting| {},
            );
        }

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

impl ImporterBuilder {
    pub fn clear_currencies(&mut self) -> &mut Self {
        self.currencies.clear();
        self
    }

    pub fn clear_positions(&mut self) -> &mut Self {
        self.positions.clear();
        self
    }

    pub fn try_add_currency<C>(
        &mut self,
        name: impl Into<String>,
        commodity: C,
    ) -> Result<&mut Self, C::Error>
    where
        C: TryInto<Commodity>,
    {
        self.currencies.insert(name.into(), commodity.try_into()?);
        Ok(self)
    }

    pub fn try_add_position<C>(
        &mut self,
        position: impl Into<String>,
        commodity: C,
    ) -> Result<&mut Self, C::Error>
    where
        C: TryInto<Commodity>,
    {
        self.positions
            .insert(position.into(), commodity.try_into()?);
        Ok(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,
            })?,

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

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

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

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

            positions: self.positions.clone(),

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

            currencies: self.currencies.clone(),
        };

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

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

    #[serde(rename = "UNIONDEPOTNUMMER")]
    depot_id: DepotId<'r>,

    #[serde(rename = "TRANSAKTIONSART")]
    transaction_kind: TransactionKind,

    #[serde(rename = "FONDSPREIS", with = "german_decimal::serde")]
    unit_cost: Decimal,

    #[serde(rename = "ANTEILE", with = "german_decimal::serde")]
    shares: Decimal,

    #[serde(rename = "VOLUMEN", with = "german_decimal::serde")]
    total: Decimal,

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

#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(try_from = "&str")]
struct DepotId<'r> {
    depot: &'r str,
    position: &'r str,
}

impl<'i> TryFrom<&'i str> for DepotId<'i> {
    type Error = Error;

    fn try_from(value: &'i str) -> Result<Self, Self::Error> {
        let (depot, position) = value.split_once('/').ok_or_else(|| todo!())?;

        Ok(Self { depot, position })
    }
}

#[derive(Debug)]
struct TemplateContext<'i> {
    depot: &'i Seg,
    position: &'i Seg,
}

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

    fn index(&self, selector: &TemplateSelector) -> &Self::Output {
        match selector {
            TemplateSelector::Depot => self.depot,
            TemplateSelector::Position => self.position,
        }
    }
}

impl<'r> TryFrom<DepotId<'r>> for TemplateContext<'r> {
    type Error = Error;

    fn try_from(value: DepotId<'r>) -> Result<Self, Self::Error> {
        let DepotId { depot, position } = value;

        let depot = <&Seg>::try_from(depot).map_err(|_| todo!())?;
        let position = <&Seg>::try_from(position).map_err(|_| todo!())?;

        Ok(Self { depot, position })
    }
}

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

impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;

    fn from_str(selector: &str) -> Result<Self, Self::Err> {
        match selector {
            "depot" => Ok(Self::Depot),
            "position" => Ok(Self::Position),

            selector => TemplateSelectorSnafu { selector }.fail(),
        }
    }
}

#[derive(Debug, Diagnostic, Snafu)]
pub struct TemplateSelectorError {
    selector: String,

    backtrace: Backtrace,
}

#[derive(Clone, Copy, Debug, Deserialize)]
enum TransactionKind {
    #[serde(rename = "Verkauf wg. Depotgebühr UID mit Postbox")]
    DepositFee,

    #[serde(rename = "Ertragsausschüttung")]
    Distribution,

    #[serde(rename = "Fremde Gebühren")]
    ForeignFees,

    #[serde(rename = "Kauf")]
    Purchase,

    #[serde(rename = "Storno wegen Rücklastschrift")]
    Reversal,

    #[serde(rename = "Verkauf")]
    Sale,
}

impl Display for TransactionKind {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            TransactionKind::DepositFee => "Sale To Cover Deposit Fees",
            TransactionKind::Distribution => "Reinvestment of Distribution",
            TransactionKind::ForeignFees => "Sale To Cover Foreign Fees",
            TransactionKind::Purchase => "Purchase",
            TransactionKind::Reversal => "Reversal of Purchase",
            TransactionKind::Sale => "Sale",
        })
    }
}

impl FromStr for TransactionKind {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let value = match s {
            "Ertragsausschüttung" => Self::Distribution,
            "Fremde Gebühren" => Self::ForeignFees,
            "Kauf" => Self::Purchase,
            "Storno wegen Rücklastschrift" => Self::Reversal,
            "Verkauf" => Self::Sale,
            "Verkauf wg. Depotgebühr UID mit Postbox" => Self::DepositFee,
            other => todo!("unsupported transaction kind {other:?}"),
        };
        Ok(value)
    }
}