import decimal
from collections import namedtuple

from django import forms
from django.conf import settings
from django.core.cache import cache
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.utils.translation import gettext as _

from oscar.core.decorators import deprecated
from oscar.core.loading import get_class, get_model

from ocyan.core.utils import (
    round_to_two_dec as deprecated_round_to_two_dec,
)
from ocyan.core.utils import (
    to_money as deprecated_to_money,
)
from ocyan.plugin.oscar.taxes.constants import COUNTRY_TAX_RATES
from ocyan.plugin.oscar.taxes.utils import (
    exclusive_tax_component as deprecated_exclusive_tax_component,
)
from ocyan.plugin.oscar.taxes.utils import (
    get_country_specific_tax_code,
)
from ocyan.plugin.oscar.taxes.utils import (
    inclusive_tax_component as deprecated_inclusive_tax_component,
)
from ocyan.plugin.oscar.taxes.utils import (
    tax_from_price_incl_tax as deprecated_tax_from_price_incl_tax,
)

from .constants import (
    EU_APPLICABLE_COUNTRIES,
    EU_LOCAL_VAT_TRESHOLD,
    LOCAL_VAT_APPLICABLE_KEY,
)

decode_option_value = get_class("basket.option", "option_display_value")
tax_rate_as_fraction = get_class("taxes.taxrate", "tax_rate_as_fraction")
get_local_vat_order_queryset = get_class(
    "partner.queryset", "get_local_vat_order_queryset"
)

round_to_two_dec = deprecated(deprecated_round_to_two_dec)
to_money = deprecated(deprecated_to_money)
exclusive_tax_component = deprecated(deprecated_exclusive_tax_component)
inclusive_tax_component = deprecated(deprecated_inclusive_tax_component)
tax_from_price_incl_tax = deprecated(deprecated_tax_from_price_incl_tax)

LocalEUVatSettings = get_model("oscar_partner", "LocalEUVatSettings")

TaxInfo = namedtuple("TaxInfo", ["percentage", "tax_code"])


def get_vat_country_from_request(request):
    if request is not None:
        return request.META.get(
            "shipping_country", settings.OSCAR_VAT_COUNTRY_OF_ORIGIN_CODE
        )

    return settings.OSCAR_VAT_COUNTRY_OF_ORIGIN_CODE


def is_local_vat_applicable(country):
    if not settings.OCYAN_PARTNER_EU_VAT_RULES_ACTIVATED:
        return False

    if country in EU_APPLICABLE_COUNTRIES:
        local_vat_applicable = cache.get(LOCAL_VAT_APPLICABLE_KEY)
        if local_vat_applicable is None:
            eu_settings = LocalEUVatSettings.load()
            if eu_settings.vat_threshold_exceeded:
                cache.set(LOCAL_VAT_APPLICABLE_KEY, True, 60 * 60)
                local_vat_applicable = True
            else:
                orders = get_local_vat_order_queryset()

                total_sum = orders.aggregate(
                    total_sum=Coalesce(Sum("total_incl_tax"), decimal.Decimal("0.00"))
                )["total_sum"]

                if total_sum > decimal.Decimal(EU_LOCAL_VAT_TRESHOLD):
                    cache.set(LOCAL_VAT_APPLICABLE_KEY, True, 60 * 60)
                    local_vat_applicable = True

        return local_vat_applicable

    return False


@deprecated
def get_vat_rate_for_country(
    vat_rate, country, is_shipping_charge, basket=None, user=None
):
    return get_vat_info_for_country(
        vat_rate, country, is_shipping_charge, basket=basket, user=user
    )


# pylint: disable=unused-argument
def get_vat_info_for_country(
    vat_rate, country, is_shipping_charge, basket=None, user=None
):
    if is_local_vat_applicable(country):
        if is_shipping_charge and "shipping" in COUNTRY_TAX_RATES[country]:
            vat_rate = "shipping"

        return TaxInfo(
            tax_rate_as_fraction(vat_rate, country=country),
            get_country_specific_tax_code(vat_rate, country),
        )
    else:
        return TaxInfo(tax_rate_as_fraction(vat_rate), vat_rate)


def get_name_and_value(option_value):
    """
    Depending if the option comes from the posted data
    of from the database, it could be a dict or an object.
    This function extracts the name and the value regardless of which it is.
    The value will be treated as if it was a json value.

    >>> from argparse import Namespace
    >>> option = Namespace(code="ruuu")
    >>> attr_opt = Namespace(option=option, value="1")
    >>> dict_opt = {"option": option, "value": "[1, 2, 3]"}
    >>> get_name_and_value(attr_opt)
    ('ruuu', '1')
    >>> get_name_and_value(dict_opt)
    ('ruuu', '[1, 2, 3]')
    """
    try:
        option = option_value["option"]
        value = option_value["value"]
    except TypeError:
        option = option_value.option
        value = option_value.value

    return option.code, decode_option_value(value)


class ParsedOptions(dict):
    """
    Access product options like they where structured in a normal way as
    attributes::

        a = ParsedOptions(POST['options])
        a["shirt_text"]

    or::

        a = ParsedOptions(line.options)
        a["shirt_text"]

    The obvious usecase is to parse the options passed to the strategy in::

        def fetch_for_product(self, product, options=None, stockrecord=None):
            pass
    """

    def __init__(self, option_list):
        if option_list is not None:
            super().__init__([get_name_and_value(option) for option in option_list])
        else:
            super().__init__()

    def validate(self, *names):
        errors = []
        for name in names:
            if name not in self:
                errors.append(name)
        if errors:
            raise forms.ValidationError(_("Option %(errors)s missing") % locals())
