import datetime
from decimal import Decimal as D

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.template import Context, Template
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _

from oscar.apps.address.abstract_models import (
    AbstractBillingAddress as BaseAbstractBillingAddress,
)
from oscar.apps.address.abstract_models import (
    AbstractShippingAddress as BaseAbstractShippingAddress,
)
from oscar.apps.order import abstract_models as oscar
from oscar.apps.order import exceptions
from oscar.apps.order.signals import order_status_changed
from oscar.core.loading import get_class

from ocyan.main.utils import randomtokengenerator
from ocyan.plugin.oscar_address.address.abstract_models import AbstractUserAddress
from ocyan.plugin.oscar_address.address.mixins import AddressMixin

option_display_value = get_class("basket.option", "option_display_value")
DeliveryTime = get_class("checkout.delivery", "DeliveryTime")


class AbstractLineAttribute(oscar.AbstractLineAttribute):
    def display_value(self):
        return option_display_value(self.value)

    class Meta(oscar.AbstractLineAttribute.Meta):
        abstract = True


class AbstractShippingAddress(BaseAbstractShippingAddress, AddressMixin):
    house_number = models.CharField(_("No."), null=True, blank=True, max_length=255)
    email = models.EmailField(_("Email"), blank=True, null=False)

    base_fields = hash_fields = AbstractUserAddress.hash_fields

    class Meta(BaseAbstractShippingAddress.Meta):
        abstract = True


class AbstractBillingAddress(BaseAbstractBillingAddress, AddressMixin):
    house_number = models.CharField(_("No."), null=True, blank=True, max_length=255)
    email = models.EmailField(_("Email"), blank=True, null=False)

    base_fields = [
        "salutation",
        "line1",
        "house_number",
        "line2",
        "line3",
        "line4",
        "state",
        "postcode",
        "country",
        "email",
    ]
    # it's different!
    hash_fields = AbstractUserAddress.hash_fields

    class Meta(BaseAbstractBillingAddress.Meta):
        abstract = True


class AbstractOrder(oscar.AbstractOrder):
    delivery_time_text = models.TextField(
        _("Delivery time message"), blank=True, null=True
    )
    delivery_time_days = models.PositiveIntegerField(
        _("Number of days delivery time"), blank=True, null=True
    )

    # added because the oscar impl forgets to add surcharges
    @property
    def total_before_discounts_incl_tax(self):
        return (
            self.basket_total_before_discounts_incl_tax
            + self.shipping_before_discounts_incl_tax
            + self.surcharge_incl_tax
        )

    # added because it does not exist
    @property
    def shipping_discount_incl_tax(self):
        return sum(
            [shipping_discount.amount for shipping_discount in self.shipping_discounts],
            D(0),
        )

    # added for consistency
    @property
    def basket_discount_incl_tax(self):
        return super().total_discount_incl_tax

    @property
    def basket_discount_excl_tax(self):
        return super().total_discount_excl_tax

    # added because total_discount_excl_tax forgets to add shipping and is
    # used everywhere assuming it does not add shipping
    @property
    def total_order_discount_incl_tax(self):
        return self.basket_discount_incl_tax + self.shipping_discount_incl_tax

    @property
    def total_order_discount_excl_tax(self):
        raise ValueError("This value can not be determined.")

    # added because this is missing
    @property
    def basket_total_tax(self):
        return self.basket_total_incl_tax - self.basket_total_excl_tax

    @property
    def delivery_time(self):
        return DeliveryTime(self.delivery_time_text, self.delivery_time_days)

    @property
    def delivery_date(self):
        if self.delivery_time_days is not None:
            return self.date_placed + datetime.timedelta(days=self.delivery_time_days)

        return None

    def all_lines(self):
        return self.lines.all()

    @property
    def payment_source_type(self):
        # pylint: disable=no-member
        try:
            single_payment_source = self.sources.get()
            return single_payment_source.source_type
        except self.sources.model.MultipleObjectsReturned:
            return None
        except self.sources.model.DoesNotExist:
            return None

    def verification_hash(self):
        if self.is_anonymous:
            return super().verification_hash()

        return randomtokengenerator.make_token(self.user)

    def check_verification_hash(self, hash_to_check):
        if self.is_anonymous:
            return super().check_verification_hash(hash_to_check)

        return randomtokengenerator.check_token(self.user, hash_to_check)

    def get_try_again_url(self):
        if not self.basket:
            return

        base64_order_id = urlsafe_base64_encode(force_bytes(self.pk))
        token = self.verification_hash()

        return reverse(
            "checkout:try-again-from-link",
            kwargs={"oidb64": base64_order_id, "token": token},
        )

    def get_thank_you_url(self):
        base64_order_id = urlsafe_base64_encode(force_bytes(self.pk))
        token = self.verification_hash()

        return reverse(
            "checkout:thank-you",
            kwargs={"oidb64": base64_order_id, "token": token},
        )

    def set_status(self, new_status):
        """
        Set a new status for this order.
        If the requested status is not valid, then ``InvalidOrderStatus`` is
        raised.
        """
        if new_status == self.status:
            return

        old_status = self.status

        if new_status not in self.available_statuses():
            raise exceptions.InvalidOrderStatus(
                _(
                    "'%(new_status)s' is not a valid status for order %(number)s"
                    " (current status: '%(status)s')"
                )
                % {
                    "new_status": new_status,
                    "number": self.number,
                    "status": self.status,
                }
            )

        self.status = new_status
        if new_status in self.cascade:
            new_line_status = self.cascade[new_status]
            for line in self.lines.all():
                if new_line_status in line.available_statuses():
                    line.status = new_line_status
                    line.save()
        self.save()

        # Send signal for handling status changed
        order_status_changed.send(
            sender=self,
            order=self,
            old_status=old_status,
            new_status=new_status,
        )

        self._create_order_status_change(old_status, new_status)

    @property
    def language_code(self):
        language_code_note = self.notes.filter(note_type="language_code").first()
        if language_code_note is not None:
            return language_code_note.message

        return settings.LANGUAGE_CODE

    class Meta(oscar.AbstractOrder.Meta):
        abstract = True


class AbstractSurcharge(oscar.AbstractSurcharge):
    code = models.CharField(_("Surcharge code"), max_length=128)

    class Meta(oscar.AbstractSurcharge.Meta):
        abstract = True


# Tracking related models
class AbstractCarrier(models.Model):
    name = models.CharField(_("Shipping carrier"), max_length=255, unique=True)
    code = models.CharField(
        _("Carrier code"),
        max_length=255,
        help_text=_(
            "Useful for when working with external market places, as they "
            "mostly require you to send the carrier code"
        ),
        unique=True,
    )
    tracking_url = models.CharField(
        _("Tracking url template"),
        max_length=255,
        help_text=_(
            "The tracking url to be used for this carrier. When no tracking_url is filled out on the tracking we try to generate the tracking url from this template. For example: https://postnl.nl/?number={{tracking_code}}"
        ),
        null=True,
        blank=True,
    )

    def __str__(self):
        return self.name

    class Meta:
        abstract = True
        app_label = "order"
        verbose_name = _("Shipping Carrier")
        verbose_name_plural = _("Shipping Carriers")


class AbstractTracking(models.Model):
    carrier = models.ForeignKey(
        "order.Carrier",
        null=True,
        on_delete=models.SET_NULL,
        related_name="tracking",
        verbose_name=_("Shipping carrier"),
    )
    shipping_event = models.OneToOneField(
        "order.ShippingEvent",
        null=True,
        on_delete=models.SET_NULL,
        related_name="tracking",
        verbose_name=_("Shipping Event"),
    )
    tracking_code = models.CharField(
        _("Tracking code/number"), max_length=255, blank=True, null=True
    )
    tracking_url = models.URLField(
        _("Tracking url"), max_length=255, blank=True, null=True
    )

    # pylint: disable=E1101
    @property
    def get_tracking_url(self):
        if self.tracking_url:
            return self.tracking_url
        elif self.carrier and self.carrier.tracking_url:
            if self.tracking_code:
                tracking_template = Template(self.carrier.tracking_url)
                return tracking_template.render(
                    Context({"tracking_code": self.tracking_code})
                )
            else:
                return self.carrier.tracking_url

    def __str__(self):
        if self.shipping_event and self.tracking_code:
            return str(
                _(
                    "Shipment tracking for order %s with Tracking Code: %s"
                    % (self.shipping_event.order.number, self.tracking_code)
                )
            )
        elif self.shipping_event and self.tracking_url:
            return str(
                _(
                    "Shipment tracking for order %s with Tracking URL: %s"
                    % (self.shipping_event.order.number, self.tracking_url)
                )
            )
        return self.tracking_code or self.tracking_url

    def clean(self):
        super().clean()
        if not any([self.tracking_code, self.tracking_url]):
            raise ValidationError(
                _('Either "tracking_code" or "tracking_url" is required or both.')
            )

    @classmethod
    def create_for_shipping_event(
        cls, shipping_event, carrier, tracking_code, tracking_url, **kwargs
    ):
        """
        This will be called inside the EventHandler whenever a shipping_event is created.
        """
        obj = cls(
            shipping_event=shipping_event,
            carrier=carrier,
            tracking_code=tracking_code,
            tracking_url=tracking_url,
        )
        obj.full_clean()
        obj.save()

    class Meta:
        abstract = True
        app_label = "order"
        verbose_name = _("Shipping Tracking")
        verbose_name_plural = _("Shipping Trackings")
