from __future__ import unicode_literals

import sys
from ipaddress import ip_address, ip_network
from operator import attrgetter

from django.contrib.auth import get_user_model
from six import text_type as str
from six.moves.urllib_parse import urljoin

import django
from django import forms
from django.conf import settings

from payfast import api
from payfast import conf
from payfast.models import PayFastOrder

# Django 1.10 introduces django.urls
if django.VERSION < (1, 10):
    from django.core.urlresolvers import reverse
else:
    from django.urls import reverse


def full_url(link):
    """
    Return an absolute version of a possibly-relative URL.

    This uses the PAYFAST_URL_BASE setting.
    """
    url_base = (conf.URL_BASE() if callable(conf.URL_BASE) else
                conf.URL_BASE)
    return urljoin(url_base, link)


def notify_url():
    return full_url(reverse('payfast_notify'))


class HiddenForm(forms.Form):
    """ A form with all fields hidden """
    def __init__(self, *args, **kwargs):
        super(HiddenForm, self).__init__(*args, **kwargs)
        for field in self.fields:
            self.fields[field].widget = forms.HiddenInput()


class PayFastForm(HiddenForm):
    """ PayFast helper form.
    It is not for validating data.
    It can be used to output html.

    Pass all the fields to form 'initial' argument. Form also has an optional
    'user' parameter: it is the User instance the order is purchased by. If
    'user' is specified, 'name_first', 'name_last' and 'email_address' fields
    will be filled automatically if they are not passed with 'initial'.

    If `m_payment_id` is specified, it will uniquely identify the PayFastOrder.
    Otherwise, a new PayFastOrder will be created for each form instantiation.
    """

    target = conf.PROCESS_URL

    # Receiver Details
    merchant_id = forms.CharField()
    merchant_key = forms.CharField()

    return_url = forms.URLField()
    cancel_url = forms.URLField()
    notify_url = forms.URLField()

    # Payer Details
    name_first = forms.CharField()
    name_last = forms.CharField()
    email_address = forms.CharField()
    # TODO: cell_number

    # Transaction Details
    m_payment_id = forms.CharField()
    amount = forms.CharField()
    item_name = forms.CharField()
    item_description = forms.CharField()

    custom_str1 = forms.CharField()
    custom_str2 = forms.CharField()
    custom_str3 = forms.CharField()
    custom_str4 = forms.CharField()
    custom_str5 = forms.CharField()

    custom_int1 = forms.IntegerField()
    custom_int2 = forms.IntegerField()
    custom_int3 = forms.IntegerField()
    custom_int4 = forms.IntegerField()
    custom_int5 = forms.IntegerField()

    # Transaction Options
    email_confirmation = forms.IntegerField()
    confirmation_address = forms.CharField()

    # Security
    signature = forms.CharField()

    def __init__(self, *args, **kwargs):
        get_first_name = getattr(settings, 'PAYFAST_GET_USER_FIRST_NAME', attrgetter('first_name'))
        get_last_name = getattr(settings, 'PAYFAST_GET_USER_LAST_NAME', attrgetter('last_name'))

        user = kwargs.pop('user', None)
        if user:

            if get_first_name is not None:
                kwargs['initial'].setdefault('name_first', get_first_name(user))
            if get_last_name is not None:
                kwargs['initial'].setdefault('name_last', get_last_name(user))

            # Django 1.11 adds AbstractBaseUser.get_email_field_name()
            email_address = (user.email if django.VERSION < (1, 11) else
                             getattr(user, get_user_model().get_email_field_name()))
            kwargs['initial'].setdefault('email_address', email_address)

        kwargs['initial'].setdefault('notify_url', notify_url())
        kwargs['initial'].setdefault('merchant_id', conf.MERCHANT_ID)
        kwargs['initial'].setdefault('merchant_key', conf.MERCHANT_KEY)

        super(PayFastForm, self).__init__(*args, **kwargs)

        if 'm_payment_id' in self.initial:
            # If the caller supplies m_payment_id, find the existing order, or create it.
            (self.order, created) = PayFastOrder.objects.get_or_create(
                m_payment_id=self.initial['m_payment_id'],
                defaults=dict(
                    user=user,
                    amount_gross=self.initial['amount'],
                ),
            )
            if not created:
                # If the order is existing, check the user and amount fields,
                # and update if necessary.
                #
                # XXX: Also consistency-check that the order is not paid yet?
                #
                if not (self.order.user == user and
                        self.order.amount_gross == self.initial['amount']):
                    self.order.user = user
                    self.order.amount_gross = self.initial['amount']
                    self.order.save()
        else:
            # Old path: Create a new PayFastOrder each time form is instantiated.
            self.order = PayFastOrder.objects.create(
                user=user,
                amount_gross=self.initial['amount'],
            )

            # Initialise m_payment_id from the pk.
            self.order.m_payment_id = str(self.order.pk)
            self.order.save()

            self.initial['m_payment_id'] = self.order.m_payment_id

        # Coerce values to strings, for signing.
        data = {k: str(v) for (k, v) in self.initial.items()}
        self._signature = self.fields['signature'].initial = api.checkout_signature(data)


def is_payfast_ip_address(ip_address_str):
    """
    Return True if ip_address_str matches one of PayFast's server IP addresses.

    Setting: `PAYFAST_IP_ADDRESSES`

    :type ip_address_str: str
    :rtype: bool
    """
    # TODO: Django system check for validity?
    payfast_ip_addresses = getattr(settings, 'PAYFAST_IP_ADDRESSES',
                                   conf.DEFAULT_PAYFAST_IP_ADDRESSES)

    if sys.version_info < (3,):
        # Python 2 usability: Coerce str to unicode, to avoid very common TypeErrors.
        # (On Python 3, this should generally not happen:
        #  let unexpected bytes values fail as expected.)
        ip_address_str = unicode(ip_address_str)  # noqa: F821
        payfast_ip_addresses = [unicode(address) for address in payfast_ip_addresses]  # noqa: F821

    return any(ip_address(ip_address_str) in ip_network(payfast_address)
               for payfast_address in payfast_ip_addresses)


class NotifyForm(forms.ModelForm):

    def __init__(self, request, *args, **kwargs):
        self.request = request
        super(NotifyForm, self).__init__(*args, **kwargs)
        # the form must be used with order instance provided
        assert self.instance.pk

    def clean(self):
        self.ip = self.request.META.get(conf.IP_HEADER, None)
        if not is_payfast_ip_address(self.ip):
            raise forms.ValidationError('untrusted ip: %s' % self.ip)

        # Verify signature
        sig = api.itn_signature(self.data)
        if sig != self.cleaned_data['signature']:
            raise forms.ValidationError('Signature is invalid: %s != %s' % (
                sig, self.cleaned_data['signature'],))

        if conf.USE_POSTBACK:
            is_valid = api.data_is_valid(self.request.POST, conf.SERVER)
            if is_valid is None:
                raise forms.ValidationError('Postback fails')
            if not is_valid:
                raise forms.ValidationError('Postback validation fails')

        return self.cleaned_data

    def clean_merchant_id(self):
        merchant_id = self.cleaned_data['merchant_id']
        if merchant_id != conf.MERCHANT_ID:
            raise forms.ValidationError('Invalid merchant id (%s).' % merchant_id)
        return merchant_id

    def clean_amount_gross(self):
        received = self.cleaned_data['amount_gross']
        if conf.REQUIRE_AMOUNT_MATCH:
            requested = self.instance.amount_gross
            if requested != received:
                raise forms.ValidationError('Amount is not the same: %s != %s' % (
                                            requested, received,))
        return received

    def save(self, *args, **kwargs):
        self.instance.request_ip = self.ip

        # Decode body, for saving as debug_info
        body_bytes = self.request.read()  # type: bytes
        body_encoding = (settings.DEFAULT_CHARSET if self.request.encoding is None else
                         self.request.encoding)
        body_str = body_bytes.decode(body_encoding)  # type: str

        self.instance.debug_info = body_str[:255]

        self.instance.trusted = True
        return super(NotifyForm, self).save(*args, **kwargs)

    def plain_errors(self):
        ''' plain error list (without the html) '''
        return '|'.join(["%s: %s" % (k, (v[0])) for k, v in self.errors.items()])

    class Meta:
        model = PayFastOrder
        exclude = ['created_at', 'updated_at', 'request_ip', 'debug_info',
                   'trusted', 'user']
