use beancount_parser_lima as parser;
use rust_decimal::Decimal;
use std::{
    borrow::Cow,
    collections::{HashMap, HashSet},
};

use crate::api::types::{ElementIdx, IndexedReport, Plugin, raw};

use super::{Report, raw::*};

impl<'a> From<&'_ parser::Spanned<parser::Directive<'a>>> for Directive<'a> {
    fn from(value: &'_ parser::Spanned<parser::Directive<'a>>) -> Self {
        Directive {
            span: Some(value.into()),
            date: *value.date().item(),
            variant: value.variant().into(),
            tags: from_tags(value.metadata().tags()),
            links: from_links(value.metadata().links()),
            metadata: from_key_values(value.metadata().key_values()),
        }
    }
}

impl<'a> From<&'_ parser::DirectiveVariant<'a>> for DirectiveVariant<'a> {
    fn from(value: &'_ parser::DirectiveVariant<'a>) -> Self {
        use DirectiveVariant::*;
        use parser::DirectiveVariant as parser;

        match value {
            parser::Transaction(transaction) => Transaction(transaction.into()),
            parser::Price(price) => Price(price.into()),
            parser::Balance(balance) => Balance(balance.into()),
            parser::Open(open) => Open(open.into()),
            parser::Close(close) => Close(close.into()),
            parser::Commodity(commodity) => Commodity(commodity.into()),
            parser::Pad(pad) => Pad(pad.into()),
            parser::Document(document) => Document(document.into()),
            parser::Note(note) => Note(note.into()),
            parser::Event(event) => Event(event.into()),
            parser::Query(query) => Query(query.into()),
            parser::Custom(custom) => Custom(custom.into()),
        }
    }
}

impl<'a> From<&'_ parser::Transaction<'a>> for Transaction<'a> {
    fn from(value: &'_ parser::Transaction<'a>) -> Self {
        Transaction {
            flag: from_flag(*value.flag().item()),
            payee: value.payee().map(|x| Cow::Borrowed(*x.item())),
            narration: value.narration().map(|x| Cow::Borrowed(*x.item())),
            postings: value.postings().map(|x| x.into()).collect::<Vec<_>>(),
        }
    }
}

impl<'a> From<&'_ parser::Price<'a>> for PriceDct<'a> {
    fn from(value: &'_ parser::Price<'a>) -> Self {
        PriceDct {
            cur: value.currency().item().into(),
            price: Price {
                per_unit: value.amount().number().value(),
                total: None,
                cur: value.amount().currency().item().into(),
            },
        }
    }
}

impl<'a> From<&'_ parser::Balance<'a>> for Balance<'a> {
    fn from(value: &'_ parser::Balance<'a>) -> Self {
        Balance {
            acc: value.account().item().into(),
            units: value.atol().amount().number().value(),
            cur: value.atol().amount().currency().item().into(),
            tolerance: value.atol().tolerance().map(|x| *x.item()),
        }
    }
}

impl<'a> From<&'_ parser::Open<'a>> for Open<'a> {
    fn from(value: &'_ parser::Open<'a>) -> Self {
        let currencies = (value.currencies().count() > 0).then(|| {
            value
                .currencies()
                .map(|cur| cur.item().into())
                .collect::<HashSet<_>>()
        });
        Open {
            acc: value.account().item().into(),
            currencies,
            booking: value.booking().map(|booking| booking.item().into()),
        }
    }
}

impl<'a> From<&'_ parser::Close<'a>> for Close<'a> {
    fn from(value: &'_ parser::Close<'a>) -> Self {
        Close {
            acc: (value.account().item()).into(),
        }
    }
}

impl<'a> From<&'_ parser::Commodity<'a>> for Commodity<'a> {
    fn from(value: &'_ parser::Commodity<'a>) -> Self {
        Commodity {
            cur: value.currency().item().into(),
        }
    }
}

impl<'a> From<&'_ parser::Pad<'a>> for Pad<'a> {
    fn from(value: &'_ parser::Pad<'a>) -> Self {
        Pad {
            acc: value.account().item().into(),
            source: value.source().item().into(),
        }
    }
}

impl<'a> From<&'_ parser::Document<'a>> for Document<'a> {
    fn from(value: &'_ parser::Document<'a>) -> Self {
        Document {
            acc: value.account().item().into(),
            path: Cow::Borrowed(value.path().item()),
        }
    }
}

impl<'a> From<&'_ parser::Note<'a>> for Note<'a> {
    fn from(value: &'_ parser::Note<'a>) -> Self {
        Note {
            acc: value.account().item().into(),
            comment: Cow::Borrowed(value.comment().item()),
        }
    }
}

impl<'a> From<&'_ parser::Event<'a>> for Event<'a> {
    fn from(value: &'_ parser::Event<'a>) -> Self {
        Event {
            type_: Cow::Borrowed(value.event_type().item()),
            description: Cow::Borrowed(value.description().item()),
        }
    }
}

impl<'a> From<&'_ parser::Query<'a>> for Query<'a> {
    fn from(value: &'_ parser::Query<'a>) -> Self {
        Query {
            name: Cow::Borrowed(value.name().item()),
            content: Cow::Borrowed(value.content().item()),
        }
    }
}

impl<'a> From<&'_ parser::Custom<'a>> for Custom<'a> {
    fn from(value: &'_ parser::Custom<'a>) -> Self {
        Custom {
            type_: Cow::Borrowed(value.type_().item()),
            values: value.values().map(|v| v.item().into()).collect::<Vec<_>>(),
        }
    }
}

pub(crate) fn from_flag(flag: parser::Flag) -> Cow<'static, str> {
    use beancount_parser_lima::Flag;

    match flag {
        Flag::Asterisk => Cow::Borrowed("*"),
        Flag::Exclamation => Cow::Borrowed("!"),
        Flag::Ampersand => Cow::Borrowed("&"),
        Flag::Hash => Cow::Borrowed("#"),
        Flag::Question => Cow::Borrowed("?"),
        Flag::Percent => Cow::Borrowed("%"),
        Flag::Letter(_) => Cow::Owned(flag.to_string()),
    }
}

impl<'a> From<&'_ parser::Spanned<parser::Posting<'a>>> for PostingSpec<'a> {
    fn from(value: &'_ parser::Spanned<parser::Posting<'a>>) -> Self {
        PostingSpec {
            span: Some(value.into()),
            flag: value.flag().map(|x| from_flag(*x.item())),
            acc: value.account().item().into(),
            units: value.amount().map(|x| x.item().value()),
            cur: value.currency().map(|x| x.item().into()),
            cost_spec: value.cost_spec().map(|x| x.item().into()),
            price_spec: value.price_annotation().map(|x| x.item().into()),
            tags: from_tags(value.metadata().tags()),
            links: from_links(value.metadata().links()),
            metadata: from_key_values(value.metadata().key_values()),
        }
    }
}

impl<'a> From<&'_ parser::CostSpec<'a>> for CostSpec<'a> {
    fn from(value: &'_ parser::CostSpec<'a>) -> Self {
        CostSpec {
            per_unit: value.per_unit().map(|x| x.item().value()),
            total: value.total().map(|x| x.item().value()),
            cur: value.currency().map(|x| x.item().into()),
            date: value.date().map(|x| x.item()).copied(),
            label: value.label().map(|x| Cow::Borrowed(*x.item())),
            merge: value.merge(),
        }
    }
}

impl<'a> From<&'_ parser::PriceSpec<'a>> for PriceSpec<'a> {
    fn from(value: &'_ parser::PriceSpec<'a>) -> Self {
        use beancount_parser_lima::PriceSpec as PPS;
        use beancount_parser_lima::ScopedExprValue::*;

        match value {
            PPS::Unspecified => PriceSpec {
                per_unit: None,
                total: None,
                cur: None,
            },
            PPS::BareCurrency(cur) => PriceSpec {
                per_unit: None,
                total: None,
                cur: Some(cur.into()),
            },
            PPS::BareAmount(PerUnit(expr)) => PriceSpec {
                per_unit: Some(expr.value()),
                total: None,
                cur: None,
            },
            PPS::BareAmount(Total(expr)) => PriceSpec {
                per_unit: None,
                total: Some(expr.value()),
                cur: None,
            },
            PPS::CurrencyAmount(PerUnit(expr), cur) => PriceSpec {
                per_unit: Some(expr.value()),
                total: None,
                cur: Some(cur.into()),
            },
            PPS::CurrencyAmount(Total(expr), cur) => PriceSpec {
                per_unit: None,
                total: Some(expr.value()),
                cur: Some(cur.into()),
            },
        }
    }
}

pub(crate) fn from_tags<'a, 'b>(
    tags: impl ExactSizeIterator<Item = &'b parser::Spanned<parser::Tag<'a>>>,
) -> Option<HashSet<&'a str>>
where
    'a: 'b,
{
    let tags = tags
        .map(|tag: &'b parser::Spanned<parser::Tag<'a>>| tag.item().into())
        .collect::<HashSet<_>>();

    (!tags.is_empty()).then_some(tags)
}

pub(crate) fn from_links<'a, 'b>(
    links: impl ExactSizeIterator<Item = &'b parser::Spanned<parser::Link<'a>>>,
) -> Option<HashSet<&'a str>>
where
    'a: 'b,
{
    let links = links
        .map(|link: &'b parser::Spanned<parser::Link<'a>>| link.item().into())
        .collect::<HashSet<_>>();

    (!links.is_empty()).then_some(links)
}

pub(crate) fn from_key_values<'a, 'b>(
    key_values: impl ExactSizeIterator<
        Item = (
            &'b parser::Spanned<parser::Key<'a>>,
            &'b parser::Spanned<parser::MetaValue<'a>>,
        ),
    >,
) -> Option<HashMap<&'a str, MetaValue<'a>>>
where
    'a: 'b,
{
    let key_values = key_values
        .map(
            |(k, v): (
                &'b parser::Spanned<parser::Key<'a>>,
                &'b parser::Spanned<parser::MetaValue<'a>>,
            )| (k.item().into(), v.item().into()),
        )
        .collect::<HashMap<_, _>>();

    (!key_values.is_empty()).then_some(key_values)
}

impl<'a> From<&'_ parser::MetaValue<'a>> for MetaValue<'a> {
    fn from(value: &'_ parser::MetaValue<'a>) -> Self {
        use MetaValue::*;
        use parser::MetaValue as pmv;
        use parser::SimpleValue;

        match value {
            pmv::Simple(SimpleValue::String(x)) => String(Cow::Borrowed(*x)),
            pmv::Simple(SimpleValue::Currency(x)) => Currency(x.into()),
            pmv::Simple(SimpleValue::Account(x)) => Account(x.into()),
            pmv::Simple(SimpleValue::Tag(x)) => Tag(x.into()),
            pmv::Simple(SimpleValue::Link(x)) => Link(x.into()),
            pmv::Simple(SimpleValue::Date(x)) => Date(*x),
            pmv::Simple(SimpleValue::Bool(x)) => Bool(*x),
            pmv::Simple(SimpleValue::Expr(x)) => Number(x.value()),
            pmv::Simple(SimpleValue::Null) => Null,
            pmv::Amount(amount) => Amount(amount.number().value(), amount.currency().item().into()),
        }
    }
}

impl<'a> From<&'_ parser::Options<'a>> for Options<'a> {
    fn from(value: &'_ parser::Options<'a>) -> Options<'a> {
        use parser::AccountType::*;

        let mut inferred_tolerance_default = HashMap::<&str, Decimal>::default();
        let mut inferred_tolerance_default_fallback = None;

        for (cur, tol) in value.inferred_tolerance_defaults() {
            if let Some(cur) = cur {
                inferred_tolerance_default.insert(cur.into(), tol);
            } else {
                inferred_tolerance_default_fallback = Some(tol);
            }
        }

        let documents = value
            .documents()
            .map(|path| path.to_string_lossy().into_owned())
            .collect::<HashSet<_>>();

        let operating_currency = value
            .operating_currency()
            .map(|x| x.into())
            .collect::<HashSet<_>>();

        Options {
            name_assets: value.account_type_name(Assets).into(),
            name_liabilities: value.account_type_name(Liabilities).into(),
            name_equity: value.account_type_name(Equity).into(),
            name_income: value.account_type_name(Income).into(),
            name_expenses: value.account_type_name(Expenses).into(),
            title: value.title().map(|x| *x.item()),
            account_previous_balances: value.account_previous_balances().map(|x| x.item().into()),
            account_previous_earnings: value.account_previous_earnings().map(|x| x.item().into()),
            account_previous_conversions: value
                .account_previous_conversions()
                .map(|x| x.item().into()),
            account_current_earnings: value.account_current_earnings().map(|x| x.item().into()),
            account_current_conversions: value
                .account_current_conversions()
                .map(|x| x.item().into()),
            account_unrealized_gains: value.account_unrealized_gains().map(|x| x.item().into()),
            account_rounding: value.account_rounding().map(|x| x.item().into()),
            conversion_currency: value.conversion_currency().map(|x| x.item().into()),
            inferred_tolerance_default: (!inferred_tolerance_default.is_empty())
                .then_some(inferred_tolerance_default),
            inferred_tolerance_default_fallback,
            inferred_tolerance_multiplier: value.inferred_tolerance_multiplier().map(|x| *x.item()),
            infer_tolerance_from_cost: value.infer_tolerance_from_cost().map(|x| *x.item()),
            documents: (!documents.is_empty()).then_some(documents),
            operating_currency: (!operating_currency.is_empty()).then_some(operating_currency),
            render_commas: value.render_commas().map(|x| *x.item()),
            booking_method: value.booking_method().map(|x| x.item().into()),
            plugin_processing_mode: value.plugin_processing_mode().map(|x| x.item().into()),
        }
    }
}

impl From<parser::Booking> for Booking {
    fn from(value: parser::Booking) -> Self {
        use Booking::*;
        use parser::Booking as parser;

        match value {
            parser::Strict => Strict,
            parser::StrictWithSize => StrictWithSize,
            parser::None => None,
            parser::Average => Average,
            parser::Fifo => Fifo,
            parser::Lifo => Lifo,
            parser::Hifo => Hifo,
        }
    }
}

impl From<&parser::Booking> for Booking {
    fn from(value: &parser::Booking) -> Self {
        Self::from(*value)
    }
}

impl From<parser::PluginProcessingMode> for PluginProcessingMode {
    fn from(value: parser::PluginProcessingMode) -> Self {
        use PluginProcessingMode::*;
        use parser::PluginProcessingMode as parser;

        match value {
            parser::Default => Default,
            parser::Raw => Raw,
        }
    }
}

impl From<&parser::PluginProcessingMode> for PluginProcessingMode {
    fn from(value: &parser::PluginProcessingMode) -> Self {
        Self::from(*value)
    }
}

impl<'a> From<&parser::Plugin<'a>> for Plugin<'a> {
    fn from(value: &parser::Plugin<'a>) -> Self {
        Plugin {
            name: value.module_name().item(),
            config: value.config().as_ref().map(|config| *config.item()),
        }
    }
}

impl<'a> parser::Report for Report<'a> {
    fn message(&self) -> &str {
        self.message.as_ref()
    }
    fn reason(&self) -> &str {
        self.reason.as_ref()
    }
    fn span(&self) -> parser::Span {
        self.span.into()
    }
    fn contexts(&self) -> impl Iterator<Item = (&str, parser::Span)> {
        self.contexts
            .iter()
            .flatten()
            .map(|(label, span)| (label.as_ref(), span.into()))
    }
    fn related(&self) -> impl Iterator<Item = (&str, parser::Span)> {
        self.related
            .iter()
            .flatten()
            .map(|(label, span)| (label.as_ref(), span.into()))
    }
}

fn from_error_or_warning<'a, K>(
    eow: &'a parser::ErrorOrWarning<K>,
    annotation: Option<Cow<'a, str>>,
) -> Report<'a>
where
    K: parser::ErrorOrWarningKind,
{
    use parser::Report;

    let contexts = eow
        .contexts()
        .map(|(ctx, span)| (Cow::Borrowed(ctx), span.into()))
        .collect::<Vec<_>>();
    let related = eow
        .related()
        .map(|(rel, span)| (Cow::Borrowed(rel), span.into()))
        .collect::<Vec<_>>();

    super::Report {
        message: Cow::Borrowed(eow.message()),
        reason: Cow::Borrowed(eow.reason()),
        span: eow.span().into(),
        contexts: (!contexts.is_empty()).then_some(contexts),
        related: (!related.is_empty()).then_some(related),
        annotation,
    }
}

pub(crate) fn create_reports_from_parser_errors<'a, K>(
    errors_or_warnings: &'a [parser::ErrorOrWarning<K>],
) -> Vec<Report<'a>>
where
    K: parser::ErrorOrWarningKind,
{
    errors_or_warnings
        .iter()
        .map(|eow| from_error_or_warning(eow, None))
        .collect::<Vec<_>>()
}

/// Resolve indexed errors using the spans in their indexed element.
/// Missing spans cause panic, so this is only safe to call on parsed directives, not parameter ones.
pub(crate) fn create_reports_from_booking_errors<'a, 'b, 'c>(
    indexed_reports: Vec<IndexedReport>,
    directives: &[raw::Directive<'a>],
) -> Vec<Report<'c>>
where
    'a: 'c,
    'b: 'c,
{
    indexed_reports
        .into_iter()
        .map(|indexed| {
            let (element_type, span) = resolve_indexed_element(indexed.idx, directives);

            Report {
                message: Cow::Owned(format!("invalid {}", element_type)),
                reason: Cow::Owned(indexed.reason),
                span,
                contexts: indexed.idx.posting.map(|_| {
                    vec![resolve_indexed_element(
                        ElementIdx {
                            directive: indexed.idx.directive,
                            posting: None,
                        },
                        directives,
                    )]
                }),
                related: indexed.related.map(|related| {
                    related
                        .into_iter()
                        .map(|related_idx| resolve_indexed_element(related_idx, directives))
                        .collect::<Vec<_>>()
                }),
                annotation: indexed.annotation.map(Cow::Owned),
            }
        })
        .collect::<Vec<_>>()
}

fn resolve_indexed_element(
    idx: ElementIdx,
    directives: &[raw::Directive<'_>],
) -> (Cow<'static, str>, Span) {
    let directive = &directives[idx.directive];

    if let (raw::DirectiveVariant::Transaction(txn), Some(posting_idx)) =
        (&directive.variant, idx.posting)
    {
        (
            Cow::Borrowed("posting"),
            txn.postings
                .get(posting_idx)
                .map(|posting| posting.span.unwrap())
                .unwrap(),
        )
    } else {
        (
            Cow::Borrowed((&directive.variant).into()),
            directive.span.unwrap(),
        )
    }
}

impl<'a> From<parser::SpannedSource<'a>> for SpannedSource<'a> {
    fn from(value: parser::SpannedSource<'a>) -> Self {
        SpannedSource {
            file_name: value.file_name,
            start_line: value.start_line,
            end_line: value.end_line,
            content: value.content,
        }
    }
}

impl<T> From<&parser::Spanned<T>> for Span {
    fn from(value: &parser::Spanned<T>) -> Self {
        let span = value.span();
        Span {
            source: span.source,
            start: span.start,
            end: span.end,
        }
    }
}

impl From<&parser::Span> for Span {
    fn from(value: &parser::Span) -> Self {
        Span {
            source: value.source,
            start: value.start,
            end: value.end,
        }
    }
}

impl From<parser::Span> for Span {
    fn from(value: parser::Span) -> Self {
        Span::from(&value)
    }
}

impl From<&Span> for parser::Span {
    fn from(value: &Span) -> Self {
        parser::Span {
            source: value.source,
            start: value.start,
            end: value.end,
        }
    }
}

impl From<Span> for parser::Span {
    fn from(value: Span) -> Self {
        parser::Span::from(&value)
    }
}
