# -*- coding: utf-8 -*-
from datetime import datetime
import csv
import re
from functools import wraps
from future.utils import viewitems
from textwrap import dedent

from datacloud.dev_utils.id_value.id_value_lib import count_md5
from datacloud.dev_utils.validators.csv_validator import CSVValidator, RecordError
from datacloud.input_pipeline.input_checker.constants import MULTIPLE_VALUES_DELIMITER, id_fields


class XProdCsvValidator(CSVValidator):
    BAD_HEADER_CODE = 'EX_BAD_HEADER'
    BAD_HEADER_MESSAGE = 'Bad CSV Headers'
    REQUIRED_FIELDS_MESSAGE = 'There has to be required field in headers'

    def __init__(self, full_path_to_file, delimiter, allowed_headers,
                 required_fields, hard_headers):
        headers = []
        with open(full_path_to_file, 'rb') as csv_file:
            reader = csv.reader(csv_file, delimiter=delimiter)
            headers = reader.next()
        if hard_headers:
            good_headers = tuple(header for header in headers if header in allowed_headers)
        else:
            good_headers = tuple(headers)
        CSVValidator.__init__(self, good_headers)
        self._fields_set = set(good_headers)
        self._required_fields = required_fields
        self._unique_accum = {
            'external_id': {},
            ('external_id', 'retro_date'): {}
        }
        for header in set(good_headers) & id_fields:
            self._unique_accum[header] = {}

        self._target_accum = {
            ('external_id', 'retro_date'): {}
        }

    @property
    def fields_set(self):
        return self._fields_set

    def headers_problems(self, data):
        headers = tuple(data.next())
        if headers != self._field_names:
            p = {
                'code': self.BAD_HEADER_CODE,
                'message': self.BAD_HEADER_MESSAGE,
                'headers': headers,
                'expected headers': self._field_names,
            }
            return p
        elif not set(headers) & self._required_fields:
            p = {
                'code': self.BAD_HEADER_CODE,
                'message': self.REQUIRED_FIELDS_MESSAGE,
                'headers': headers,
            }
            return p
        else:
            return None

    def ivalidate_xprod(self, data, expect_header_row_for_parent=False, *args, **kwargs):
        hproblems = self.headers_problems(data)
        if hproblems:
            yield hproblems
        else:
            for problem in CSVValidator.ivalidate(self, data, expect_header_row_for_parent, *args, **kwargs):
                yield problem

    def validate_xprod(self, data, expect_header_row_for_parent=False, *args, **kwargs):
        hproblems = self.headers_problems(data)
        if hproblems:
            return [hproblems]
        else:
            return CSVValidator.validate(self, data, expect_header_row_for_parent, *args, **kwargs)

    def add_value_check_if_exists(self, field_name, *args):
        if field_name in self._fields_set:
            self.add_value_check(field_name, *args)

    def add_unique_check_if_exists(self, field_name, *args):
        if field_name in self._fields_set:
            self.add_unique_check(field_name, *args)

    def accum_unique(self, field, value):
        if value in self._unique_accum[field]:
            self._unique_accum[field][value] += 1
            return False
        self._unique_accum[field][value] = 1
        return True

    def accumm_target(self, field, value, target_name, target):
        if target_name in self._target_accum[field].get(value, {}):
            return self._target_accum[field][value][target_name] == target

        value_dict = self._target_accum[field].setdefault(value, {})
        value_dict[target_name] = target
        return True

    @property
    def unique_accum(self):
        return self._unique_accum

    def write_unique_accum(self, file):
        w = file.write
        w(dedent("""
            Unique check summary
            =======
        """))
        for header, header_dict in viewitems(self._unique_accum):
            sum = 0
            duplicates_counter = 0
            for _, num_of_val in viewitems(header_dict):
                if num_of_val > 1:
                    sum += num_of_val - 1
                    duplicates_counter += 1
            if sum > 0:
                w(dedent("""
                    %s has %s duplicate%s | on average %.2f items per duplicate \
                """) % (header, sum, 's' if sum > 1 else '', float(sum) / duplicates_counter + 1)
                )  # noqa
        w('\n\n')


# Usefull decorstors
def multiple_values_splitter(delimiter):
    def split_decorator(func):
        @wraps(func)
        def func_wrapper(v):
            if type(v) is str:
                splited_v = v.split(delimiter)
                for val in splited_v:
                    func(val)
            else:
                func(v)
        return func_wrapper
    return split_decorator


def strip_val(func):
    @wraps(func)
    def func_wrapper(v):
        return func(str(v).strip())
    return func_wrapper


# Checkers
def check_phone(phone_len):
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        if v and (len(v) != phone_len or not v.isdigit()):
            raise ValueError(v)
        pass
    return checker


def check_email():
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        if v and not re.match(r'[^@]+@[^@]+\.[^@]+', v):
            raise ValueError(v)
        pass
    return checker


def check_md5():
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        if v and not re.match(r'([a-fA-F\d]{32})', v):
            raise ValueError(v)
        pass
    return checker


# Doesn't work with multiple!
def check_val_is_md5(val_field, md5_field):
    def checker(r):
        if r.get(val_field) and r.get(md5_field):
            if count_md5(r[val_field]) != r[md5_field]:
                raise RecordError('EX_VAL_NOT_MD5', '{} != md5 of it!'.format(val_field))
    return checker


def check_client_id_site():
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        if v and not re.match(r'[^@]+@[^@]+\.[^@]+', v):
            raise ValueError(v)
        pass
    return checker


def check_one_of_required_fields(required):
    def checker(r):
        for field in required:
            if field in r and r[field].strip().translate(None, MULTIPLE_VALUES_DELIMITER):
                return
        raise RecordError('EX_FIELDS_REQUIRED', 'there has to be one of required fields')
    return checker


def check_uuid_app_id():
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        splited_v = v.split('@')
        if len(splited_v) != 2:
            raise ValueError(v)
        if not re.match(r'([a-fA-F\d]{32})', splited_v[0]):
            raise ValueError(v)
        if not splited_v[1]:
            raise ValueError(v)
    return checker


def check_date(formats, allow_empty=False):
    @strip_val
    def checker(v):
        if not v and allow_empty:
            return
        for fmt in formats:
            try:
                if datetime.strptime(v, fmt) < datetime.now():
                    return
            except ValueError:
                pass
        raise ValueError(v)
    return checker


def unique_validate(field, validator):
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        if v and not validator.accum_unique(field, v):
            pass
            # raise ValueError(v)
    return checker


def unique_tuple_validate(fields, target_fields, validator):
    def checker(r):
        value = tuple(r[field] for field in fields)
        validator.accum_unique(fields, value)
        for tfield in target_fields:
            if not validator.accumm_target(fields, value, tfield, r[tfield]):
                raise RecordError(
                    'EX_UNIQUE_TUPLE_{}'.format(tfield),
                    'Fields tuple duplicate with different target'
                )
    return checker


def check_yuid():
    @strip_val
    @multiple_values_splitter(MULTIPLE_VALUES_DELIMITER)
    def checker(v):
        if not v:
            return
        int(v)
    return checker
