# Petale - Simple App as Key/Value Storage Interface
# Copyright (C) 2017  Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import hashlib
import logging

from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.files.storage import default_storage
from django.core.mail import send_mail
from django.core.validators import RegexValidator
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _

from . import utils
from .exceptions import GlobalSpaceExhausted, PetalSizeExhausted

id_validator = RegexValidator('^[A-Za-z0-9-_]+$')

logger = logging.getLogger('petale')


class Partner(models.Model):
    name = models.CharField(verbose_name=_('Partner'), max_length=64, unique=True, validators=[id_validator])
    admin_emails = models.CharField(
        verbose_name=_('Admin emails'),
        max_length=256,
        blank=True,
        help_text=_('List of admin emails separated by comma'),
    )
    hard_global_max_size = models.IntegerField(
        verbose_name=_('Hard max size'), default=0, help_text=_('as kilobytes')
    )
    soft_global_max_size = models.IntegerField(
        verbose_name=_('Soft max size'), default=0, help_text=_('as kilobytes')
    )
    hard_per_key_max_size = models.IntegerField(
        verbose_name=_('Hard max size per key'), default=0, help_text=_('as kilobytes')
    )
    soft_per_key_max_size = models.IntegerField(
        verbose_name=_('Soft max size per key'), default=0, help_text=_('as kilobytes')
    )
    size = models.BigIntegerField(verbose_name=_('Size'), default=0, help_text=_('as bytes'))

    def __str__(self):
        return str(self.name)

    def check_limits(self, size_delta, **kwargs):
        new_size = self.size + size_delta

        if new_size > self.hard_global_max_size * 1024:
            self.notify_admins(
                subject=_('Partner %s space is exhausted') % self.name,
                # pylint: disable=no-member
                body=_('Current size: {current_size}, Max size: {max_size}').format(
                    current_size=new_size, max_size=self.hard_global_max_size * 1024
                ),
                period=600,
                **kwargs,
            )
            raise GlobalSpaceExhausted

        if new_size > self.soft_global_max_size * 1024:
            self.notify_admins(
                subject=_('Partner %s space almost exhausted') % self.name,
                # pylint: disable=no-member
                body=_('Current size: {current_size}, Max size: {max_size}').format(
                    current_size=new_size, max_size=self.hard_global_max_size * 1024
                ),
                **kwargs,
            )

    def notify_admins(self, subject, body, *, period=3600 * 24, **kwargs):
        # notify one time each day
        key = hashlib.md5((subject + self.admin_emails).encode()).hexdigest()
        if not cache.add(f'notify-{key}', '1', timeout=3600 * 24):
            return
        logger.warning('notified %s of "%s"', self.admin_emails, subject)
        if kwargs:
            body += '\n'
            for key, value in kwargs.items():
                body += '\n %s = %r' % (key, value)
        send_mail(subject, body, None, self.admin_emails.split(','))

    class Meta:
        verbose_name = _('Partner')
        verbose_name_plural = _('Partners')
        ordering = ['name']


def cut_path(partner, uuid):
    return default_storage.path(
        'data/{}/{}/{}/'.format(partner.name, hashlib.md5(uuid.encode('ascii')).hexdigest()[:3], uuid)
    )


class CUT(models.Model):
    uuid = models.CharField(max_length=255, validators=[id_validator], unique=True)

    def __str__(self):
        return str(self.uuid)

    class Meta:
        verbose_name = _('CUT')
        verbose_name_plural = _('CUTs')


def petal_directory(instance, filename):
    assert instance.name
    assert instance.partner
    assert instance.partner.name
    assert instance.cut
    assert instance.cut.uuid

    return 'data/{}/{}/{}/{}'.format(
        instance.partner.name,
        hashlib.md5(instance.cut.uuid.encode('ascii')).hexdigest()[:3],
        instance.cut.uuid,
        instance.name,
    )


class Petal(models.Model):
    created_at = models.DateTimeField(_('Created'), auto_now_add=True)
    updated_at = models.DateTimeField(_('Updated'), auto_now=True)
    name = models.CharField(_('Name'), max_length=128, validators=[id_validator])
    etag = models.CharField(_('ETag'), max_length=256)
    data = models.FileField(_('Data Content'), max_length=512, upload_to=petal_directory)
    content_type = models.CharField(_('Content type'), max_length=128)
    size = models.IntegerField(_('Size'), default=0, help_text=_('as bytes'))
    cut = models.ForeignKey(CUT, on_delete=models.CASCADE)
    partner = models.ForeignKey(Partner, on_delete=models.CASCADE)

    def __str__(self):
        return '%s/%s/%s' % (self.partner.name, self.cut.uuid, self.name)

    def clean(self):
        if self.data:
            self.size = self.data.size
            self.etag = utils.etag(self.data)

    def check_limits(self, content_length):
        '''Delegate global limits check to partner, and check per key size limits'''
        size_delta = content_length - self.size

        self.partner.check_limits(size_delta, partner=self.partner.name, cut=self.cut.uuid, key=self.name)

        if content_length > self.partner.hard_per_key_max_size * 1024:
            self.partner.notify_admins(
                # pylint: disable=no-member
                _('Key {key} space of partner {partner} is exhausted').format(
                    key=self.name, partner=self.partner.name
                ),
                # pylint: disable=no-member
                _('Current size: {current_size}, Max size: {max_size}').format(
                    current_size=content_length, max_size=self.partner.hard_per_key_max_size * 1024
                ),
                period=600,
                partner=self.partner.name,
                cut=self.cut.uuid,
                key=self.name,
            )
            raise PetalSizeExhausted

        if (
            self.size <= self.partner.soft_per_key_max_size * 1024
            and content_length > self.partner.soft_per_key_max_size * 1024
        ):
            self.partner.notify_admins(
                # pylint: disable=no-member
                _('Key {key} space of partner {partner} almost exhausted').format(
                    key=self.name, partner=self.partner.name
                ),
                # pylint: disable=no-member
                _('Current size: {current_size}, Max size: {max_size}').format(
                    current_size=content_length, max_size=self.partner.hard_per_key_max_size * 1024
                ),
                partner=self.partner.name,
                cut=self.cut.uuid,
                key=self.name,
            )

    @transaction.atomic(savepoint=False)
    def delete(self, *args, **kwargs):
        result = super().delete(*args, **kwargs)
        Partner.objects.filter(pk=self.partner_id).update(size=models.F('size') - models.Value(self.size))
        self.data.delete(save=False)
        return result

    class Meta:
        unique_together = ('name', 'partner', 'cut')
        verbose_name = _('Petal')
        verbose_name_plural = _('Petals')


class AccessControlList(models.Model):
    order = models.IntegerField(_('Order'))
    partner = models.ForeignKey(Partner, verbose_name=_('Partner'), on_delete=models.CASCADE)
    user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE)
    methods = models.CharField(
        _('Allowed methods'), max_length=128, default='GET,PUT,DELETE', help_text=('GET, PUT, DELETE')
    )
    key = models.CharField(_('Allowed keys'), max_length=128, default='*')

    def __str__(self):
        return '%s %s %s %s' % (self.partner.name, self.user.username, self.methods, self.key)

    class Meta:
        verbose_name = _('Access control list')
        verbose_name_plural = _('Access control lists')
        ordering = ['partner__name', 'user__username', 'order', 'key', 'methods']
