# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 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 datetime
import hashlib
import os
import re
import string
from itertools import chain

from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.core.validators import RegexValidator
from django.db.models import query
from django.urls import reverse, reverse_lazy
from django.utils import formats, html
from django.utils.functional import keep_lazy
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from gadjo.templatetags.gadjo import xstatic
from rest_framework import serializers
from rest_framework.fields import empty

from . import app_settings
from .decorators import to_iter
from .forms import fields, widgets
from .plugins import collect_from_plugins


@keep_lazy(str)
def capfirst(value):
    return value and value[0].upper() + value[1:]


DEFAULT_TITLE_CHOICES = (
    (pgettext_lazy('title', 'Mrs'), pgettext_lazy('title', 'Mrs')),
    (pgettext_lazy('title', 'Mr'), pgettext_lazy('title', 'Mr')),
)


class DateWidget(widgets.DateWidget):
    help_text = _('Format: yyyy-mm-dd')


class DateField(forms.DateField):
    widget = DateWidget


class DateRestField(serializers.DateField):
    default_error_messages = {
        'blank': _('This field may not be blank.'),
    }

    def __init__(self, **kwargs):
        self.allow_blank = kwargs.pop('allow_blank', False)
        self.trim_whitespace = kwargs.pop('trim_whitespace', True)
        super().__init__(**kwargs)

    def run_validation(self, data=empty):
        # Test for the empty string here so that it does not get validated,
        # and so that subclasses do not need to handle it explicitly
        # inside the `to_internal_value()` method.
        if data == '' or (self.trim_whitespace and str(data).strip() == ''):
            if not self.allow_blank:
                self.fail('blank')
            return ''
        return super().run_validation(data)


class BirthdateWidget(DateWidget):
    def __init__(self, *args, **kwargs):
        attrs = kwargs.setdefault('attrs', {})
        options = kwargs.setdefault('options', {})
        options['endDate'] = '-1d'
        options['startDate'] = '1900-01-01'
        attrs['min'] = '1900-01-01'
        attrs['max'] = (datetime.date.today() - datetime.timedelta(days=1)).isoformat()
        super().__init__(*args, **kwargs)


def validate_birthdate(value):
    if value and not (datetime.date(1900, 1, 1) <= value < datetime.date.today()):
        raise ValidationError(_('birthdate must be in the past and greater or equal than 1900-01-01.'))


class BirthdateField(forms.DateField):
    widget = BirthdateWidget
    default_validators = [
        validate_birthdate,
    ]


class BirthdateRestField(DateRestField):
    default_validators = [
        validate_birthdate,
    ]


class AddressAutocompleteInput(forms.Select):
    template_name = 'authentic2/widgets/address_autocomplete.html'

    class Media:
        js = [
            xstatic('jquery.js', 'jquery.min.js'),
            settings.SELECT2_JS,
            'authentic2/manager/js/select2_locale.js',
            reverse_lazy('a2-manager-javascript-catalog'),
            'authentic2/js/address_autocomplete.js',
        ]
        css = {
            'screen': [settings.SELECT2_CSS],
        }

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.attrs['data-select2-url'] = reverse('a2-api-address-autocomplete')
        self.attrs['class'] = 'address-autocomplete'


class AddressAutocompleteField(forms.CharField):
    widget = AddressAutocompleteInput


@to_iter
def get_title_choices():
    return app_settings.A2_ATTRIBUTE_KIND_TITLE_CHOICES or DEFAULT_TITLE_CHOICES


validate_phone_number = RegexValidator(
    r'^\+?\d{,20}$', message=_('Phone number can start with a + and must contain only digits.')
)


def clean_number(number):
    cleaned_number = re.sub(r'[-.\s/]', '', number)
    validate_phone_number(cleaned_number)
    return cleaned_number


class PhoneNumberField(forms.CharField):
    widget = widgets.PhoneNumberInput

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 30
        kwargs.setdefault('help_text', _('ex.: 0699999999, +33 6 99 99 99 99'))
        super().__init__(*args, **kwargs)

    def clean(self, value):
        if value not in self.empty_values:
            value = clean_number(value)
        return value


class PhoneNumberDRFField(serializers.CharField):
    default_validators = [validate_phone_number]

    def to_internal_value(self, data):
        return clean_number(super().to_internal_value(data))


validate_fr_postcode = RegexValidator(r'^\d{5}$', message=_('The value must be a valid french postcode'))


class FrPostcodeField(forms.CharField):
    def __init__(self, *args, **kwargs):
        kwargs.setdefault('help_text', _('ex.: 13260'))
        super().__init__(*args, **kwargs)

    def clean(self, value):
        value = super().clean(value)
        if value not in self.empty_values:
            value = value.strip()
            validate_fr_postcode(value)
        return value

    def widget_attrs(self, widget):
        return {'inputmode': 'numeric'}


class FrPostcodeDRFField(serializers.CharField):
    default_validators = [validate_fr_postcode]


class ProfileImageFile:
    def __init__(self, name):
        self.name = name

    @property
    def url(self):
        return default_storage.url(self.name)


def profile_image_serialize(uploadedfile):
    if not uploadedfile:
        return ''
    if hasattr(uploadedfile, 'url'):
        return uploadedfile.name
    h_computation = hashlib.md5()
    for chunk in uploadedfile.chunks():
        h_computation.update(chunk)
    hexdigest = h_computation.hexdigest()
    stored_file = default_storage.save(os.path.join('profile-image', hexdigest + '.jpeg'), uploadedfile)
    return stored_file


def profile_image_deserialize(name):
    if name:
        return ProfileImageFile(name)
    return None


def profile_image_html_value(attribute, value):
    if value:
        fragment = '<a href="%s"><img class="%s" src="%s"/></a>' % (value.url, attribute.name, value.url)
        return html.mark_safe(fragment)
    return ''


def profile_attributes_ng_serialize(ctx, value):
    if value and getattr(value, 'url', None):
        request = ctx.get('request')
        if request:
            return request.build_absolute_uri(value.url)
        else:
            return value.url
    return None


def date_free_text_search(term):
    for date_format in formats.get_format('DATE_INPUT_FORMATS'):
        try:
            date = datetime.datetime.strptime(term, date_format).date()
            break
        except (ValueError, TypeError):
            pass
    else:
        return None
    return query.Q(attribute_values__content__exact=date.isoformat())


DEFAULT_ALLOW_BLANK = True
DEFAULT_MAX_LENGTH = 256

DEFAULT_ATTRIBUTE_KINDS = [
    {
        'label': _('string'),
        'name': 'string',
        'field_class': forms.CharField,
        'kwargs': {
            'max_length': DEFAULT_MAX_LENGTH,
        },
    },
    {
        'label': _('title'),
        'name': 'title',
        'field_class': forms.ChoiceField,
        'kwargs': {
            'choices': get_title_choices(),
            'widget': forms.RadioSelect,
        },
    },
    {
        'label': _('boolean'),
        'name': 'boolean',
        'field_class': forms.BooleanField,
        'serialize': lambda x: str(int(bool(x))),
        'deserialize': lambda x: bool(int(x)),
        'rest_framework_field_class': serializers.NullBooleanField,
    },
    {
        'label': _('date'),
        'name': 'date',
        'field_class': DateField,
        'serialize': lambda x: x and x.isoformat(),
        'deserialize': lambda x: x and datetime.datetime.strptime(x, '%Y-%m-%d').date(),
        'rest_framework_field_class': DateRestField,
    },
    {
        'label': _('birthdate'),
        'name': 'birthdate',
        'field_class': BirthdateField,
        'serialize': lambda x: x and x.isoformat(),
        'deserialize': lambda x: x and datetime.datetime.strptime(x, '%Y-%m-%d').date(),
        'rest_framework_field_class': BirthdateRestField,
        'free_text_search': date_free_text_search,
    },
    {
        'label': _('address (autocomplete)'),
        'name': 'address_auto',
        'field_class': AddressAutocompleteField,
    },
    {
        'label': _('french postcode'),
        'name': 'fr_postcode',
        'field_class': FrPostcodeField,
        'rest_framework_field_class': FrPostcodeDRFField,
    },
    {
        'label': _('phone number'),
        'name': 'phone_number',
        'field_class': PhoneNumberField,
        'rest_framework_field_class': PhoneNumberDRFField,
    },
    {
        'label': _('profile image'),
        'name': 'profile_image',
        'field_class': fields.ProfileImageField,
        'serialize': profile_image_serialize,
        'deserialize': profile_image_deserialize,
        'rest_framework_field_class': serializers.FileField,
        'rest_framework_field_kwargs': {
            'read_only': True,
            'use_url': True,
        },
        'html_value': profile_image_html_value,
        'attributes_ng_serialize': profile_attributes_ng_serialize,
        'csv_importable': False,
    },
]


def get_attribute_kinds():
    attribute_kinds = {}
    for attribute_kind in chain(DEFAULT_ATTRIBUTE_KINDS, app_settings.A2_ATTRIBUTE_KINDS):
        attribute_kinds[attribute_kind['name']] = attribute_kind
    for attribute_kind in chain(*collect_from_plugins('attribute_kinds')):
        attribute_kinds[attribute_kind['name']] = attribute_kind
    return attribute_kinds


@to_iter
def get_choices():
    '''Produce a choice list to use in form fields'''
    for d in get_attribute_kinds().values():
        yield (d['name'], capfirst(d['label']))


def only_digits(value):
    return ''.join(x for x in value if x in string.digits)


def validate_lun(value):
    l = [(int(x) * (1 + i % 2)) for i, x in enumerate(reversed(value))]  # noqa: E741
    return sum(x - 9 if x > 10 else x for x in l) % 10 == 0


def validate_siret(value):
    RegexValidator(r'^( *[0-9] *){14}$', _('SIRET number must contain 14 digits'), 'coin')(value)
    value = only_digits(value)
    if not validate_lun(value) or not validate_lun(value[:9]):
        raise ValidationError(_('SIRET validation code does not match'))


class SIRETField(forms.CharField):
    default_validators = [validate_siret]

    def to_python(self, value):
        value = super().to_python(value)
        value = only_digits(value)
        return value

    def widget_attrs(self, widget):
        return {'inputmode': 'numeric'}


def contribute_to_form(attribute_descriptions, form):
    for attribute_description in attribute_descriptions:
        attribute_description.contribute_to_form(form)


def get_form_field(kind, **kwargs):
    defn = get_attribute_kinds()[kind]
    if 'kwargs' in defn:
        kwargs.update(defn['kwargs'])
    return defn['field_class'](**kwargs)


def identity(x):
    return x


def get_kind(kind):
    d = get_attribute_kinds()[kind]
    d.setdefault('default', None)
    d.setdefault('serialize', identity)
    d.setdefault('deserialize', identity)
    rest_field_kwargs = d.setdefault('rest_framework_field_kwargs', {})
    if 'rest_framework_field_class' not in d:
        d['rest_framework_field_class'] = serializers.CharField
        rest_field_kwargs.setdefault('allow_blank', DEFAULT_ALLOW_BLANK)
        rest_field_kwargs.setdefault('max_length', DEFAULT_MAX_LENGTH)
    return d
