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

use beancount_importers_framework::error::ImporterBuilderError;
use beancount_importers_framework::error::UninitializedFieldSnafu;
use beancount_types::AccountTemplate;
use beancount_types::Amount;
use beancount_types::Balance;
use beancount_types::Commodity;
use beancount_types::Directive;
use beancount_types::Price;
use beancount_types::Seg;
use derive_builder::Builder;
use miette::Diagnostic;
use miette::IntoDiagnostic as _;
use miette::Report;
use rust_decimal::Decimal;
use serde::Deserialize;
use snafu::Backtrace;
use snafu::OptionExt as _;
use snafu::Snafu;
use time::Date;

#[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>,
}

#[derive(Debug, Diagnostic, Snafu)]
pub enum Error {
    #[snafu(display("error while parsing date"))]
    DateFormat { source: time::Error },

    #[snafu(display("could not parse position {currency:?}"))]
    Currency {
        backtrace: Backtrace,

        currency: String,

        source: <Commodity as TryFrom<&'static str>>::Error,
    },

    #[snafu(display("could not parse position {isin:?}"))]
    Isin {
        backtrace: Backtrace,

        isin: String,

        source: <Commodity as TryFrom<&'static str>>::Error,
    },

    #[snafu(display("could not parse position {position:?}"))]
    Position {
        backtrace: Backtrace,

        source: core::num::ParseIntError,

        position: String,
    },

    #[snafu(display("could not parse share price {share_price:?}"))]
    SharePrice {
        backtrace: Backtrace,

        source: rust_decimal::Error,

        share_price: String,
    },

    #[snafu(display("could not parse shares {shares:?}"))]
    Shares {
        backtrace: Backtrace,

        source: rust_decimal::Error,

        shares: String,
    },
}

impl From<time::error::TryFromParsed> for Error {
    fn from(source: time::error::TryFromParsed) -> Self {
        let source = source.into();
        Self::DateFormat { source }
    }
}

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

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

impl Importer {
    pub const NAME: &str = "ebase/balances";

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

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

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

    fn account(&self, _file: &[u8]) -> Result<beancount_types::Account, Self::Error> {
        Ok(self.config.balance_account.base().to_owned())
    }

    fn date(&self, file: &[u8]) -> Option<Result<Date, Self::Error>> {
        self.importer
            .date(file, self)
            .map(|result| result.map_err(Self::Error::from))
    }

    fn extract(&self, file: &[u8], existing: &[Directive]) -> Result<Vec<Directive>, Self::Error> {
        self.importer
            .extract(file, existing, self)
            .map_err(Report::from)
    }

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

    fn identify(&self, file: &[u8]) -> Result<bool, Self::Error> {
        const EXPECTED_HEADERS: &[&str] = &[
            "Depotnummer",
            "Position",
            "Fondsname / Produkt",
            "ISIN",
            "WKN",
            "Anteile",
            "Anteilswert",
            "Währung (Anteilswert)",
            "Kursdatum",
            "Devisenkurs",
            "+/- Vortag (absolut)",
            "+/- Vortag (relativ)",
            "Gewinn und Verlust seit Monatsanfang",
            "Gewinn und Verlust seit Jahresanfang",
            "Gewinn und Verlust seit Eröffnung",
            "Bestand in Euro",
        ];

        self.importer
            .identify(file, 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.share_price_date
    }

    fn extract(
        &self,
        _existing: &[Directive],
        record: Record,
    ) -> Result<Vec<Directive>, Self::Error> {
        let position = format!("{:02}", record.depot_position);

        let context = TemplateContext {
            depot_id: <&Seg>::try_from(record.depot_id).unwrap(),
            position: <&Seg>::try_from(&*position).unwrap(),
        };

        let isin = record.isin;
        let total_shares = Amount::new(record.shares, isin);

        let balance = Balance::new(
            record.share_price_date,
            self.config.balance_account.render(&context),
            total_shares,
        );

        let share_price = Amount::new(record.share_price, record.share_price_currency);
        let price = Price::new(record.share_price_date, isin, share_price);

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

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

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

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

    #[serde(rename = "Position")]
    depot_position: usize,

    #[serde(rename = "ISIN")]
    isin: Commodity,

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

    #[serde(rename = "Anteilswert", with = "german_decimal::serde")]
    share_price: Decimal,

    #[serde(rename = "Währung (Anteilswert)")]
    share_price_currency: Commodity,

    #[serde(rename = "Kursdatum", with = "dmy")]
    share_price_date: Date,
}

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

impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;

    fn from_str(selector: &str) -> Result<Self, Self::Err> {
        let selector = match selector {
            "depot_id" => Self::DepotId,
            "position" => Self::Position,
            selector => return TemplateSelectorSnafu { selector }.fail(),
        };
        Ok(selector)
    }
}

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

    pub backtrace: Backtrace,
}

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

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

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

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