# coding=utf-8

from __future__ import unicode_literals

import re

from faker.providers.date_time import Provider as DateTimeProvider
from .. import BaseProvider


class CreditCard(object):
    valid_characters_pattern = re.compile('^[0-9 ]*$')

    def __init__(self, name, prefixes, length=16, security_code='CVC', security_code_length=3):
        self.name = name
        self.prefixes = prefixes
        self.length = length
        self.security_code = security_code
        self.security_code_length = security_code_length


class Provider(BaseProvider):

    visa_prefix_list = [['4', '5', '3', '9'],
                        ['4', '5', '5', '6'],
                        ['4', '9', '1', '6'],
                        ['4', '5', '3', '2'],
                        ['4', '9', '2', '9'],
                        ['4', '0', '2', '4', '0', '0', '7', '1'],
                        ['4', '4', '8', '6'],
                        ['4', '7', '1', '6'],
                        ['4']]

    mastercard_prefix_list = [['5', '1'],
                              ['5', '2'],
                              ['5', '3'],
                              ['5', '4'],
                              ['5', '5']]

    amex_prefix_list = [['3', '4'],
                        ['3', '7']]

    discover_prefix_list = [['6', '0', '1', '1']]

    diners_prefix_list = [['3', '0', '0'],
                          ['3', '0', '1'],
                          ['3', '0', '2'],
                          ['3', '0', '3'],
                          ['3', '6'],
                          ['3', '8']]

    enroute_prefix_list = [['2', '0', '1', '4'],
                           ['2', '1', '4', '9']]

    jcb16_prefix_list = [['3', '0', '8', '8'],
                         ['3', '0', '9', '6'],
                         ['3', '1', '1', '2'],
                         ['3', '1', '5', '8'],
                         ['3', '3', '3', '7'],
                         ['3', '5', '2', '8']]

    jcb15_prefix_list = [['2', '1', '0', '0'],
                         ['1', '8', '0', '0']]

    voyager_prefix_list = [['8', '6', '9', '9']]

    credit_card_types = {
        'mastercard':   CreditCard('Mastercard',        mastercard_prefix_list, 16, security_code='CVV'),
        'visa16':       CreditCard('VISA 16 digit',     visa_prefix_list),
        'visa13':       CreditCard('VISA 13 digit',     visa_prefix_list, 13),
        'amex':         CreditCard('American Express',  amex_prefix_list, 15, security_code='CID', security_code_length=4),
        'discover':     CreditCard('Discover',          discover_prefix_list),
        'diners':       CreditCard('Diners Club / Carte Blanche', diners_prefix_list, 14),
        'enroute':      CreditCard('enRoute',           enroute_prefix_list, 15),
        'jcb15':        CreditCard('JCB 15 digit',      jcb15_prefix_list, 15),
        'jcb16':        CreditCard('JCB 16 digit',      jcb16_prefix_list),
        'voyager':      CreditCard('Voyager',           voyager_prefix_list, 15),
    }
    credit_card_types['visa'] = credit_card_types['visa16']
    credit_card_types['jcb'] = credit_card_types['jcb16']

    @classmethod
    def credit_card_provider(cls, card_type=None):
        if card_type is None:
            card_type = cls.random_element(cls.credit_card_types.keys())
        return cls._credit_card_type(card_type).name

    @classmethod
    def credit_card_number(cls, card_type=None, validate=False, max_check=10):
        card = cls._credit_card_type(card_type)
        number = ''
        for i in range(0, max_check):
            number = cls._generate_number(cls.random_element(card.prefixes), card.length)
            if not validate or cls._validate_credit_card_number(card, number):
                break
        return number

    @classmethod
    def credit_card_expire(cls, start='now', end='+10y', date_format='%m/%y'):
        expire_date = DateTimeProvider.date_time_between(start, end)
        return expire_date.strftime(date_format)

    def credit_card_full(self, card_type=None, validate=False, max_check=10):
        card = self._credit_card_type(card_type)
        template = """
{provider}
{owner}
{number}  {expire_date}
{security}: {security_nb}""".format(
            provider=card.name,
            owner=self.generator.parse("{{first_name}} {{last_name}}"),
            number=self.credit_card_number(card, validate, max_check),
            expire_date=self.credit_card_expire(),
            security=card.security_code,
            security_nb=self.credit_card_security_code(card)
        )
        return self.generator.parse(template)

    @classmethod
    def credit_card_security_code(cls, card_type=None):
        return ''.join(str(cls.random_digit()) for _ in range(cls._credit_card_type(card_type).security_code_length))

    @classmethod
    def _credit_card_type(cls, card_type=None):
        """returns a random credit card type instance"""
        if card_type is None:
            card_type = cls.random_element(cls.credit_card_types.keys())
        elif isinstance(card_type, CreditCard):
            return card_type
        return cls.credit_card_types[card_type]

    @classmethod
    def _generate_number(cls, prefix, length):
        """
        'prefix' is the start of the CC number as a string, any number of digits.
        'length' is the length of the CC number to generate. Typically 13 or 16
        """
        # add list() to copy prefixes
        number = list(prefix)
        # generate digits
        while len(number) < (length - 1):
            number.append(str(cls.random_digit()))

        # Calculate sum
        tot = 0
        pos = 0

        reversed_number = []
        reversed_number.extend(number)
        reversed_number.reverse()

        while pos < length - 1:
            odd = int(reversed_number[pos]) * 2
            if odd > 9:
                odd -= 9
            tot += odd
            if pos != (length - 2):
                tot += int(reversed_number[pos+1])
            pos += 2
        # Calculate check digit
        check_digit = ((tot / 10 + 1) * 10 - tot) % 10
        number.append(str(check_digit))
        return ''.join(number)

    @classmethod
    def _validate_credit_card_number(cls, card, number):
        if card.valid_characters_pattern.match(number) is not None:
            return cls._validate_luhn_checksum(number)
        return False

    @staticmethod
    def _validate_luhn_checksum(number_as_string):
        """ checks to make sure that the card passes a luhn mod-10 checksum """

        number = 0
        num_digits = len(number_as_string)
        odd_even = num_digits & 1

        for i in range(0, num_digits):
            digit = int(number_as_string[i])

            if not ((i & 1) ^ odd_even):
                digit *= 2
            if digit > 9:
                digit -= 9
            number += digit
        return (number % 10) == 0
