from datetime import datetime
from decimal import Decimal
from gettext import gettext as _

import mptt.models
import pytz
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import signals, Sum, Q
from django.dispatch import receiver
from django.urls import reverse

from tracker.validators import positive, nonzero

__all__ = [
    'Bid',
    'DonationBid',
    'BidSuggestion',
]


class BidManager(models.Manager):
    def get_by_natural_key(self, event, name, speedrun=None, parent=None):
        from .event import Event, SpeedRun

        return self.get(
            event=Event.objects.get_by_natural_key(*event),
            name=name,
            speedrun=SpeedRun.objects.get_by_natural_key(*speedrun)
            if speedrun
            else None,
            parent=self.get_by_natural_key(*parent) if parent else None,
        )


class Bid(mptt.models.MPTTModel):
    objects = BidManager()
    event = models.ForeignKey(
        'Event',
        on_delete=models.PROTECT,
        verbose_name='Event',
        null=True,
        blank=True,
        related_name='bids',
        help_text='Required for top level bids if Run is not set',
    )
    speedrun = models.ForeignKey(
        'SpeedRun',
        on_delete=models.PROTECT,
        verbose_name='Run',
        null=True,
        blank=True,
        related_name='bids',
    )
    parent = mptt.models.TreeForeignKey(
        'self',
        on_delete=models.PROTECT,
        verbose_name='Parent',
        editable=False,
        null=True,
        blank=True,
        related_name='options',
    )
    name = models.CharField(max_length=64)
    state = models.CharField(
        max_length=32,
        db_index=True,
        default='OPENED',
        choices=(
            ('PENDING', 'Pending'),
            ('DENIED', 'Denied'),
            ('HIDDEN', 'Hidden'),
            ('OPENED', 'Opened'),
            ('CLOSED', 'Closed'),
        ),
    )
    description = models.TextField(max_length=1024, blank=True)
    shortdescription = models.TextField(
        max_length=256,
        blank=True,
        verbose_name='Short Description',
        help_text='Alternative description text to display in tight spaces',
    )
    goal = models.DecimalField(
        decimal_places=2, max_digits=20, null=True, blank=True, default=None
    )
    istarget = models.BooleanField(
        default=False,
        verbose_name='Target',
        help_text="Set this if this bid is a 'target' for donations (bottom level choice or challenge)",
    )
    allowuseroptions = models.BooleanField(
        default=False,
        verbose_name='Allow User Options',
        help_text='If set, this will allow donors to specify their own options on the donate page (pending moderator approval)',
    )
    option_max_length = models.PositiveSmallIntegerField(
        'Max length of user suggestions',
        blank=True,
        null=True,
        default=None,
        validators=[MinValueValidator(1), MaxValueValidator(64)],
        help_text='If allowuseroptions is set, this sets the maximum length of user-submitted bid suggestions',
    )
    revealedtime = models.DateTimeField(
        verbose_name='Revealed Time', null=True, blank=True
    )
    biddependency = models.ForeignKey(
        'self',
        on_delete=models.PROTECT,
        verbose_name='Dependency',
        null=True,
        blank=True,
        related_name='dependent_bids',
    )
    total = models.DecimalField(
        decimal_places=2, max_digits=20, editable=False, default=Decimal('0.00')
    )
    count = models.IntegerField(editable=False)

    class Meta:
        app_label = 'tracker'
        unique_together = (('event', 'name', 'speedrun', 'parent',),)
        ordering = ['event__datetime', 'speedrun__starttime', 'parent__name', 'name']
        permissions = (
            ('top_level_bid', 'Can create new top level bids'),
            ('delete_all_bids', 'Can delete bids with donations attached'),
            ('view_hidden', 'Can view hidden bids'),
        )

    class MPTTMeta:
        order_insertion_by = ['name']

    def get_absolute_url(self):
        return reverse('tracker:bid', args=(self.id,))

    def natural_key(self):
        if self.parent:
            return (
                self.event.natural_key(),
                self.name,
                self.speedrun.natural_key() if self.speedrun else None,
                self.parent.natural_key(),
            )
        elif self.speedrun:
            return (self.event.natural_key(), self.name, self.speedrun.natural_key())
        else:
            return (self.event.natural_key(), self.name)

    def clean(self):
        # Manually de-normalize speedrun/event/state to help with searching
        # TODO: refactor this logic, it should be correct, but is probably not minimal

        if self.option_max_length:
            if not self.allowuseroptions:
                raise ValidationError(
                    {
                        'option_max_length': ValidationError(
                            _('Cannot set option_max_length without allowuseroptions'),
                            code='invalid',
                        ),
                    }
                )
            if self.id:
                for child in self.get_children():
                    if len(child.name) > self.option_max_length:
                        raise ValidationError(
                            _(
                                'Cannot set option_max_length to %(length)d, child name `%(name)s` is too long'
                            ),
                            code='invalid',
                            params={
                                'length': self.option_max_length,
                                'name': child.name,
                            },
                        )
                        # TODO: why is this printing 'please enter a whole number'?
                        # raise ValidationError({
                        #     'option_max_length': ValidationError(
                        #         _('Cannot set option_max_length to %(length), child name %(name) is too long'),
                        #         code='invalid',
                        #         params={
                        #             'length': self.option_max_length,
                        #             'name': child.name,
                        #         }
                        #     ),
                        # })

        # TODO: move this to save/a post_save signal?
        if self.speedrun:
            self.event = self.speedrun.event
        # TODO: move some of this to save/a post_save signal?
        if self.parent:
            curr = self.parent
            while curr.parent is not None:
                curr = curr.parent
            root = curr
            self.speedrun = root.speedrun
            self.event = root.event
            if self.state not in ['PENDING', 'DENIED', 'HIDDEN']:
                self.state = root.state
            max_len = self.parent.option_max_length
            if max_len and len(self.name) > max_len:
                raise ValidationError(
                    {
                        'name': ValidationError(
                            _('Name is longer than %(limit)s characters'),
                            params={'limit': max_len},
                            code='invalid',
                        ),
                    }
                )
        # TODO: move this to save/a post_save signal?
        if self.biddependency:
            if self.parent or self.speedrun:
                if self.event != self.biddependency.event:
                    raise ValidationError('Dependent bids must be on the same event')
            self.event = self.biddependency.event
            if not self.speedrun:
                self.speedrun = self.biddependency.speedrun
        if not self.parent:
            if not self.get_event():
                raise ValidationError('Top level bids must have their event set')
        # TODO: move this to save/a post_save signal?
        if self.id:
            for option in self.get_descendants():
                option.speedrun = self.speedrun
                option.event = self.event
                if option.state not in ['PENDING', 'DENIED', 'HIDDEN']:
                    option.state = self.state
                option.save()
        if not self.goal:
            self.goal = None
        elif self.goal <= Decimal('0.0'):
            raise ValidationError('Goal should be a positive value')
        if self.istarget and self.options.count() != 0:
            raise ValidationError('Targets cannot have children')
        if self.parent and self.parent.istarget:
            raise ValidationError('Cannot set that parent, parent is a target')
        if self.istarget and self.allowuseroptions:
            raise ValidationError(
                'A bid target cannot allow user options, since it cannot have children.'
            )
        same_name = Bid.objects.filter(
            speedrun=self.speedrun,
            event=self.event,
            parent=self.parent,
            name__iexact=self.name,
        ).exclude(pk=self.pk)
        if same_name.exists():
            raise ValidationError(
                'Cannot have a bid under the same event/run/parent with the same name'
            )
        # TODO: move this to save/a post_save signal?
        if self.state in ['OPENED', 'CLOSED'] and not self.revealedtime:
            self.revealedtime = datetime.utcnow().replace(tzinfo=pytz.utc)
        self.update_total()

    @property
    def has_options(self):
        return self.allowuseroptions or self.public_options.exists()

    @property
    def public_options(self):
        return self.options.filter(Q(state='OPENED') | Q(state='CLOSED')).order_by(
            '-total'
        )

    def update_total(self):
        if self.istarget:
            self.total = self.bids.filter(
                donation__transactionstate='COMPLETED'
            ).aggregate(Sum('amount'))['amount__sum'] or Decimal('0.00')
            self.count = self.bids.filter(
                donation__transactionstate='COMPLETED'
            ).count()
            # auto close this if it's a challenge with no children and the goal's been met
            if (
                self.goal
                and self.state == 'OPENED'
                and self.total >= self.goal
                and self.istarget
            ):
                self.state = 'CLOSED'
        else:
            options = self.options.exclude(
                state__in=('HIDDEN', 'DENIED', 'PENDING')
            ).aggregate(Sum('total'), Sum('count'))
            self.total = options['total__sum'] or Decimal('0.00')
            self.count = options['count__sum'] or 0

    def get_event(self):
        if self.speedrun:
            return self.speedrun.event
        else:
            return self.event

    def full_label(self, addMoney=True):
        result = [self.fullname()]
        if self.speedrun:
            result = [self.speedrun.name_with_category(), ' : '] + result
        if addMoney:
            result += [' $', '%0.2f' % self.total]
            if self.goal:
                result += [' / ', '%0.2f' % self.goal]
        return ''.join(result)

    def __str__(self):
        if self.parent:
            return f'{self.parent} (Parent) -- {self.name}'
        elif self.speedrun:
            return f'{self.speedrun.name_with_category()} (Run) -- {self.name}'
        else:
            return f'{self.event} (Event) -- {self.name}'

    def fullname(self):
        parent = self.parent.fullname() + ' -- ' if self.parent else ''
        return parent + self.name


@receiver(signals.pre_save, sender=Bid)
def BidTotalUpdate(sender, instance, raw, **kwargs):
    if raw:
        return
    instance.update_total()


@receiver(signals.post_save, sender=Bid)
def BidParentUpdate(sender, instance, created, raw, **kwargs):
    if created or raw:
        return
    if instance.parent:
        instance.parent.save()


class DonationBid(models.Model):
    bid = models.ForeignKey('Bid', on_delete=models.PROTECT, related_name='bids')
    donation = models.ForeignKey(
        'Donation', on_delete=models.PROTECT, related_name='bids'
    )
    amount = models.DecimalField(
        default=0, decimal_places=2, max_digits=20, validators=[positive, nonzero]
    )

    class Meta:
        app_label = 'tracker'
        verbose_name = 'Donation Bid'
        ordering = ['-donation__timereceived']
        unique_together = (('bid', 'donation'),)

    def clean(self):
        if not self.bid.istarget:
            raise ValidationError('Target bid must be a leaf node')
        self.donation.clean(self)
        from .. import viewutil

        bidsTree = (
            viewutil.get_tree_queryset_all(Bid, [self.bid])
            .select_related('parent')
            .prefetch_related('options')
        )
        for bid in bidsTree:
            if bid.state == 'OPENED' and bid.goal is not None and bid.goal <= bid.total:
                bid.state = 'CLOSED'
                if hasattr(bid, 'dependent_bids_set'):
                    for dependentBid in bid.dependent_bids_set():
                        if dependentBid.state == 'HIDDEN':
                            dependentBid.state = 'OPENED'
                            dependentBid.save()

    @property
    def speedrun(self):
        return self.bid.speedrun

    @property
    def speedrun_id(self):
        return self.bid.speedrun_id

    @property
    def event(self):
        return self.bid.event

    @property
    def event_id(self):
        return self.bid.event_id

    @property
    def donor_cache(self):
        return self.donation.donor_cache

    @property
    def fullname(self):
        return self.bid.fullname()

    def __str__(self):
        return str(self.bid) + ' -- ' + str(self.donation)


@receiver(signals.post_save, sender=DonationBid)
def DonationBidParentUpdate(sender, instance, created, raw, **kwargs):
    if raw:
        return
    if instance.donation.transactionstate == 'COMPLETED':
        instance.bid.save()


# FIXME: this appears to be unused, see #154548040
class BidSuggestion(models.Model):
    bid = models.ForeignKey(
        'Bid', related_name='suggestions', null=False, on_delete=models.PROTECT
    )
    name = models.CharField(max_length=64, blank=False, null=False, verbose_name='Name')

    class Meta:
        app_label = 'tracker'
        ordering = ['name']

    def __init__(self):
        raise Exception('Nothing should be using this any more')

    def clean(self):
        sameBid = BidSuggestion.objects.filter(
            Q(name__iexact=self.name)
            & (
                Q(bid__event=self.bid.get_event())
                | Q(bid__speedrun__event=self.bid.get_event())
            )
        )
        if sameBid.exists():
            if sameBid.count() > 1 or sameBid[0].id != self.id:
                raise ValidationError(
                    'Cannot have a bid suggestion with the same name within the same event.'
                )

        # If set, limit the length of suggestions based on the parent bid's
        # setting

    def __str__(self):
        return self.name + ' -- ' + str(self.bid)