text: from decimal import Decimal
  from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Union
  
  import opentracing
  import opentracing.tags
  from django.conf import settings
  from django.contrib.sites.models import Site
  from django.core.exceptions import ValidationError
  from django_countries import countries
  from django_countries.fields import Country
  from django_prices_vatlayer.utils import (
      fetch_rate_types,
      fetch_rates,
      get_tax_rate_types,
  )
  from prices import Money, TaxedMoney, TaxedMoneyRange
  
  from ...checkout import calculations
  from ...core.taxes import TaxType
  from ...graphql.core.utils.error_codes import PluginErrorCode
  from ...product.models import ProductType
  from ..base_plugin import BasePlugin, ConfigurationTypeField
  from ..manager import get_plugins_manager
  from . import (
      DEFAULT_TAX_RATE_NAME,
      VatlayerConfiguration,
      apply_tax_to_price,
      get_taxed_shipping_price,
      get_taxes_for_country,
  )
  
  if TYPE_CHECKING:
      # flake8: noqa
      from ...account.models import Address
      from ...channel.models import Channel
      from ...checkout.fetch import CheckoutInfo, CheckoutLineInfo
      from ...checkout.models import Checkout
      from ...discount import DiscountInfo
      from ...order.models import Order, OrderLine
      from ...product.models import (
          Collection,
          Product,
          ProductVariant,
          ProductVariantChannelListing,
      )
      from ..models import PluginConfiguration
  
  
  class VatlayerPlugin(BasePlugin):
      PLUGIN_ID = "mirumee.taxes.vatlayer"
      PLUGIN_NAME = "Vatlayer"
      META_CODE_KEY = "vatlayer.code"
      META_DESCRIPTION_KEY = "vatlayer.description"
  
      DEFAULT_CONFIGURATION = [
          {"name": "Access key", "value": None},
          {"name": "origin_country", "value": None},
          {"name": "countries_to_calculate_taxes_from_origin", "value": None},
          {"name": "excluded_countries", "value": None},
      ]
  
      CONFIG_STRUCTURE = {
          "origin_country": {
              "type": ConfigurationTypeField.STRING,
              "help_test": (
                  "Country code in ISO format, required to calculate taxes for countries "
                  "from `Countries for which taxes will be calculated from origin "
                  "country`."
              ),
              "label": "Origin country",
          },
          "countries_to_calculate_taxes_from_origin": {
              "type": ConfigurationTypeField.STRING,
              "help_text": (
                  "List of destination countries (separated by comma), in ISO format "
                  "which will use origin country to calculate taxes."
              ),
              "label": "Countries for which taxes will be calculated from origin country",
          },
          "excluded_countries": {
              "type": ConfigurationTypeField.STRING,
              "help_text": (
                  "List of countries (separated by comma), in ISO format for which no "
                  "VAT should be added."
              ),
              "label": "Countries for which no VAT will be added.",
          },
          "Access key": {
              "type": ConfigurationTypeField.PASSWORD,
              "help_text": "Required to authenticate to Vatlayer API.",
              "label": "Access key",
          },
      }
  
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          # Convert to dict to easier take config elements
          configuration = {item["name"]: item["value"] for item in self.configuration}
  
          origin_country = configuration["origin_country"] or ""
          origin_country = countries.alpha2(origin_country.strip())
  
          countries_from_origin = configuration[
              "countries_to_calculate_taxes_from_origin"
          ]
          countries_from_origin = countries_from_origin or ""
          countries_from_origin = [
              countries.alpha2(c.strip()) for c in countries_from_origin.split(",")
          ]
          countries_from_origin = list(filter(None, countries_from_origin))
  
          excluded_countries = configuration["excluded_countries"] or ""
          excluded_countries = [
              countries.alpha2(c.strip()) for c in excluded_countries.split(",")
          ]
          excluded_countries = list(filter(None, excluded_countries))
  
          self.config = VatlayerConfiguration(
              access_key=configuration["Access key"],
              origin_country=origin_country,
              excluded_countries=excluded_countries,
              countries_from_origin=countries_from_origin,
          )
          self._cached_taxes = {}
  
      def _skip_plugin(
          self, previous_value: Union[TaxedMoney, TaxedMoneyRange, Decimal]
      ) -> bool:
          if not self.active or not self.config.access_key:
              return True
  
          # The previous plugin already calculated taxes so we can skip our logic
          if isinstance(previous_value, TaxedMoneyRange):
              start = previous_value.start
              stop = previous_value.stop
  
              return start.net != start.gross and stop.net != stop.gross
  
          if isinstance(previous_value, TaxedMoney):
              return previous_value.net != previous_value.gross
          return False
  
      def calculate_checkout_total(
          self,
          checkout_info: "CheckoutInfo",
          lines: List["CheckoutLineInfo"],
          address: Optional["Address"],
          discounts: List["DiscountInfo"],
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          if self._skip_plugin(previous_value):
              return previous_value
  
          manager = get_plugins_manager()
          return (
              calculations.checkout_subtotal(
                  manager=manager,
                  checkout_info=checkout_info,
                  lines=lines,
                  address=address,
                  discounts=discounts,
              )
              + calculations.checkout_shipping_price(
                  manager=manager,
                  checkout_info=checkout_info,
                  lines=lines,
                  address=address,
                  discounts=discounts,
              )
              - checkout_info.checkout.discount
          )
  
      def _get_taxes_for_country(self, country: Country):
          """Try to fetch cached taxes on the plugin level.
  
          If the plugin doesn't have cached taxes for a given country it will fetch it
          from cache or db.
          """
          if not country:
              origin_country_code = self.config.origin_country
              if not origin_country_code:
                  company_address = Site.objects.get_current().settings.company_address
                  origin_country_code = (
                      company_address.country
                      if company_address
                      else settings.DEFAULT_COUNTRY
                  )
  
              country = Country(origin_country_code)
          country_code = country.code
  
          if country_code in self.config.countries_from_origin:
              country_code = self.config.origin_country
  
          if country_code in self.config.excluded_countries:
              return None
  
          if country_code in self._cached_taxes:
              return self._cached_taxes[country_code]
  
          country = Country(country_code)
          taxes = get_taxes_for_country(country)
          self._cached_taxes[country_code] = taxes
          return taxes
  
      def calculate_checkout_shipping(
          self,
          checkout_info: "CheckoutInfo",
          lines: List["CheckoutLineInfo"],
          address: Optional["Address"],
          discounts: List["DiscountInfo"],
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          """Calculate shipping gross for checkout."""
          if self._skip_plugin(previous_value):
              return previous_value
  
          taxes = None
          if address:
              taxes = self._get_taxes_for_country(address.country)
          if not checkout_info.delivery_method_info.delivery_method:
              return previous_value
          shipping_price = getattr(
              checkout_info.delivery_method_info.delivery_method, "price", None
          )
          if shipping_price is None and checkout_info.shipping_method_channel_listings:
              shipping_price = checkout_info.shipping_method_channel_listings.price
  
          return get_taxed_shipping_price(shipping_price, taxes)
  
      def calculate_order_shipping(
          self, order: "Order", previous_value: TaxedMoney
      ) -> TaxedMoney:
          if self._skip_plugin(previous_value):
              return previous_value
  
          address = order.shipping_address or order.billing_address
          taxes = None
          if address:
              taxes = self._get_taxes_for_country(address.country)
          if not order.shipping_method:
              return previous_value
          shipping_price = order.shipping_method.channel_listings.get(
              channel_id=order.channel_id
          ).price
          return get_taxed_shipping_price(shipping_price, taxes)
  
      def calculate_checkout_line_total(
          self,
          checkout_info: "CheckoutInfo",
          lines: List["CheckoutLineInfo"],
          checkout_line_info: "CheckoutLineInfo",
          address: Optional["Address"],
          discounts: Iterable["DiscountInfo"],
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          unit_price = self.__calculate_checkout_line_unit_price(
              address,
              discounts,
              checkout_line_info.variant,
              checkout_line_info.product,
              checkout_line_info.collections,
              checkout_info.channel,
              checkout_line_info.channel_listing,
              previous_value,
          )
          return (
              unit_price * checkout_line_info.line.quantity
              if unit_price is not None
              else previous_value
          )
  
      def calculate_order_line_total(
          self,
          order: "Order",
          order_line: "OrderLine",
          variant: "ProductVariant",
          product: "Product",
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          unit_price = self.__calculate_order_line_unit(
              order,
              order_line,
              variant,
              product,
              previous_value,
          )
          return (
              unit_price * order_line.quantity
              if unit_price is not None
              else previous_value
          )
  
      def calculate_checkout_line_unit_price(
          self,
          checkout_info: "CheckoutInfo",
          lines: Iterable["CheckoutLineInfo"],
          checkout_line_info: "CheckoutLineInfo",
          address: Optional["Address"],
          discounts: Iterable["DiscountInfo"],
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          unit_price = self.__calculate_checkout_line_unit_price(
              address,
              discounts,
              checkout_line_info.variant,
              checkout_line_info.product,
              checkout_line_info.collections,
              checkout_info.channel,
              checkout_line_info.channel_listing,
              previous_value,
          )
          return unit_price if unit_price is not None else previous_value
  
      def __calculate_checkout_line_unit_price(
          self,
          address: Optional["Address"],
          discounts: Iterable["DiscountInfo"],
          variant: "ProductVariant",
          product: "Product",
          collections: List["Collection"],
          channel: "Channel",
          channel_listing: "ProductVariantChannelListing",
          previous_value: TaxedMoney,
      ):
          if self._skip_plugin(previous_value):
              return
  
          price = variant.get_price(
              product, collections, channel, channel_listing, discounts
          )
          country = address.country if address else None
          return self.__apply_taxes_to_product(product, price, country)
  
      def calculate_order_line_unit(
          self,
          order: "Order",
          order_line: "OrderLine",
          variant: "ProductVariant",
          product: "Product",
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          unit_price = self.__calculate_order_line_unit(
              order, order_line, variant, product, previous_value
          )
          return unit_price if unit_price is not None else previous_value
  
      def __calculate_order_line_unit(
          self,
          order: "Order",
          order_line: "OrderLine",
          variant: "ProductVariant",
          product: "Product",
          previous_value: TaxedMoney,
      ):
          if self._skip_plugin(previous_value):
              return
  
          address = order.shipping_address or order.billing_address
          country = address.country if address else None
          if not variant:
              return
          return self.__apply_taxes_to_product(product, order_line.unit_price, country)
  
      def get_checkout_line_tax_rate(
          self,
          checkout_info: "CheckoutInfo",
          lines: Iterable["CheckoutLineInfo"],
          checkout_line_info: "CheckoutLineInfo",
          address: Optional["Address"],
          discounts: Iterable["DiscountInfo"],
          previous_value: Decimal,
      ) -> Decimal:
          return self._get_tax_rate(checkout_line_info.product, address, previous_value)
  
      def get_order_line_tax_rate(
          self,
          order: "Order",
          product: "Product",
          variant: "ProductVariant",
          address: Optional["Address"],
          previous_value: Decimal,
      ) -> Decimal:
          return self._get_tax_rate(product, address, previous_value)
  
      def _get_tax_rate(
          self, product: "Product", address: Optional["Address"], previous_value: Decimal
      ):
          if self._skip_plugin(previous_value):
              return previous_value
          country = address.country if address else None
          taxes, tax_rate = self.__get_tax_data_for_product(product, country)
          if not taxes or not tax_rate:
              return previous_value
          tax = taxes.get(tax_rate) or taxes.get(DEFAULT_TAX_RATE_NAME)
          # tax value is given in percentage so it need be be converted into decimal value
          return Decimal(tax["value"] / 100)
  
      def get_checkout_shipping_tax_rate(
          self,
          checkout_info: "CheckoutInfo",
          lines: Iterable["CheckoutLineInfo"],
          address: Optional["Address"],
          discounts: Iterable["DiscountInfo"],
          previous_value: Decimal,
      ):
          return self._get_shipping_tax_rate(address, previous_value)
  
      def get_order_shipping_tax_rate(self, order: "Order", previous_value: Decimal):
          address = order.shipping_address or order.billing_address
          return self._get_shipping_tax_rate(address, previous_value)
  
      def _get_shipping_tax_rate(
          self, address: Optional["Address"], previous_value: Decimal
      ):
          if self._skip_plugin(previous_value):
              return previous_value
          country = address.country if address else None
          taxes = self._get_taxes_for_country(country)
          if not taxes:
              return previous_value
          tax = taxes.get(DEFAULT_TAX_RATE_NAME)
          # tax value is given in percentage so it need be be converted into decimal value
          return Decimal(tax["value"]) / 100
  
      def get_tax_rate_type_choices(
          self, previous_value: List["TaxType"]
      ) -> List["TaxType"]:
          if not self.active:
              return previous_value
  
          rate_types = get_tax_rate_types() + [DEFAULT_TAX_RATE_NAME]
          choices = [
              TaxType(code=rate_name, description=rate_name) for rate_name in rate_types
          ]
          # sort choices alphabetically by translations
          return sorted(choices, key=lambda x: x.code)
  
      def show_taxes_on_storefront(self, previous_value: bool) -> bool:
          if not self.active:
              return previous_value
          return True
  
      def apply_taxes_to_shipping(
          self, price: Money, shipping_address: "Address", previous_value: TaxedMoney
      ) -> TaxedMoney:
          if self._skip_plugin(previous_value):
              return previous_value
  
          taxes = self._get_taxes_for_country(shipping_address.country)
          return get_taxed_shipping_price(price, taxes)
  
      def apply_taxes_to_product(
          self,
          product: "Product",
          price: Money,
          country: Country,
          previous_value: TaxedMoney,
      ) -> TaxedMoney:
          if self._skip_plugin(previous_value):
              return previous_value
          return self.__apply_taxes_to_product(product, price, country)
  
      def __apply_taxes_to_product(
          self, product: "Product", price: Money, country: Country
      ):
          taxes, tax_rate = self.__get_tax_data_for_product(product, country)
          return apply_tax_to_price(taxes, tax_rate, price)
  
      def __get_tax_data_for_product(self, product: "Product", country: Country):
          taxes = None
          if country and product.charge_taxes:
              taxes = self._get_taxes_for_country(country)
          product_tax_rate = self.__get_tax_code_from_object_meta(product).code
          tax_rate = (
              product_tax_rate
              or self.__get_tax_code_from_object_meta(product.product_type).code
          )
          return taxes, tax_rate
  
      def assign_tax_code_to_object_meta(
          self,
          obj: Union["Product", "ProductType"],
          tax_code: Optional[str],
          previous_value: Any,
      ):
          if not self.active:
              return previous_value
  
          if tax_code is None and obj.pk:
              obj.delete_value_from_metadata(self.META_CODE_KEY)
              obj.delete_value_from_metadata(self.META_DESCRIPTION_KEY)
              return previous_value
  
          tax_item = {self.META_CODE_KEY: tax_code, self.META_DESCRIPTION_KEY: tax_code}
          obj.store_value_in_metadata(items=tax_item)
          return previous_value
  
      def get_tax_code_from_object_meta(
          self, obj: Union["Product", "ProductType"], previous_value: "TaxType"
      ) -> "TaxType":
          if not self.active:
              return previous_value
          return self.__get_tax_code_from_object_meta(obj)
  
      def __get_tax_code_from_object_meta(
          self, obj: Union["Product", "ProductType"]
      ) -> "TaxType":
  
          # Product has None as it determines if we overwrite taxes for the product
          default_tax_code = None
          default_tax_description = None
          if isinstance(obj, ProductType):
              default_tax_code = DEFAULT_TAX_RATE_NAME
              default_tax_description = DEFAULT_TAX_RATE_NAME
  
          tax_code = obj.get_value_from_metadata(self.META_CODE_KEY, default_tax_code)
          tax_description = obj.get_value_from_metadata(
              self.META_DESCRIPTION_KEY, default_tax_description
          )
          return TaxType(code=tax_code, description=tax_description)
  
      def get_tax_rate_percentage_value(
          self, obj: Union["Product", "ProductType"], country: Country, previous_value
      ) -> Decimal:
          """Return tax rate percentage value for given tax rate type in the country."""
          if not self.active:
              return previous_value
          taxes = self._get_taxes_for_country(country)
          if not taxes:
              return Decimal(0)
          rate_name = self.__get_tax_code_from_object_meta(obj).code
          tax = taxes.get(rate_name) or taxes.get(DEFAULT_TAX_RATE_NAME)
          return Decimal(tax["value"])
  
      def fetch_taxes_data(self, previous_value: Any) -> Any:
          """Triggered when ShopFetchTaxRates mutation is called."""
          if not self.active:
              return previous_value
          with opentracing.global_tracer().start_active_span(
              "vatlayer.fetch_taxes_data"
          ) as scope:
              span = scope.span
              span.set_tag(opentracing.tags.COMPONENT, "tax")
              span.set_tag("service.name", "vatlayer")
              fetch_rates(self.config.access_key)
          return True
  
      @classmethod
      def validate_plugin_configuration(cls, plugin_configuration: "PluginConfiguration"):
          """Validate if provided configuration is correct."""
          configuration = plugin_configuration.configuration
          configuration = {item["name"]: item["value"] for item in configuration}
  
          access_key = configuration.get("Access key")
          if plugin_configuration.active and not access_key:
              raise ValidationError(
                  {
                      "Access key": ValidationError(
                          "Cannot be enabled without provided Access key",
                          code=PluginErrorCode.INVALID.value,
                      )
                  }
              )
          if access_key and plugin_configuration.active:
              # let's check if access_key works
              fetched_data = fetch_rate_types(access_key=access_key)
              if not fetched_data["success"]:
                  raise ValidationError(
                      {
                          "Access key": ValidationError(
                              "Cannot enable Vatlayer. Incorrect API key.",
                              code=PluginErrorCode.INVALID.value,
                          )
                      }
                  )
          countries_from_origin = configuration.get(
              "countries_to_calculate_taxes_from_origin"
          )
          origin_country = configuration.get("origin_country")
          if countries_from_origin and not origin_country:
              raise ValidationError(
                  {
                      "origin_country": ValidationError(
                          "Source country required when `Countries for which taxes will "
                          "be calculated from origin country` provided.",
                          code=PluginErrorCode.INVALID.value,
                      )
                  }
              )
  