from marshmallow import Schema, ValidationError, fields, validate, validates, validates_schema
import datetime
import math
import re


TASK_PREFIX_RE = re.compile('^([^/]+/)+$')
TASK_RE = re.compile('^[^/]+(/[^/]+)*$')

REQUIRED_ROLE_RE = re.compile(r'^[\w.-]+$')
REQUIRED_ROLE_RE_DESC = 'letters, digits, dots, dashes, underscores'

TARIFF_RUB_PER_HOUR = 180.0
TARIFF_RUB_PER_SEC = TARIFF_RUB_PER_HOUR / 3600.0


def get_duplicates(values):
    unique_values = set(values)

    if len(unique_values) == len(values):
        return None

    result = list(values)
    for value in unique_values:
        result.remove(value)
    return set(result)


class DateField(fields.Date):
    '''
    Date field that supports `date` and `datetime` objects as input value.

    `fields.Date` works with strings only, whereas yaml `load()` functions
    converts them internally to `date` and `datetime`.
    '''

    def _deserialize(self, value, attr, data, **kwargs):
        if isinstance(value, datetime.date):
            return value
        if isinstance(value, datetime.datetime):
            if value.hour or value.minute or value.second or value.microsecond or value.tzinfo:
                raise ValidationError('Wrong date: {}'.format(value))
            return value.date()
        return super()._deserialize(value, attr, data, **kwargs)


class TariffSchema(Schema):
    tariff_name = fields.String(required=True)
    start_date = DateField(required=False)
    end_date = DateField(required=False)
    seconds = fields.Integer(required=True)
    rub = fields.Float(required=True)
    tasks = fields.List(
        fields.String(),
        required=True,
        validate=validate.Length(min=1)
    )
    required_role = fields.String(
        required=False,
        validate=validate.Regexp(
            REQUIRED_ROLE_RE,
            error='required_role "{input}" should consist of ' + REQUIRED_ROLE_RE_DESC + ".",
        ),
    )

    @validates_schema
    def validate_cost(self, data, **kwargs):
        if math.isclose(data['rub'], data['seconds'] * TARIFF_RUB_PER_SEC):
            return

        raise ValidationError(
            'Cost in rubles must be equal to seconds * TARIFF_RUB_PER_SEC({}).'
            .format(round(TARIFF_RUB_PER_SEC, 2))
        )

    @validates_schema
    def validate_dates(self, data, **kwargs):
        if 'start_date' in data and 'end_date' in data and data['start_date'] > data['end_date']:
            raise ValidationError(
                'Tariff start date ({}) cannot be larger than end date ({}).'
                .format(data['start_date'], data['end_date'])
            )

    @validates("tasks")
    def should_not_contain_duplicate_tasks(self, tasks_to_validate):
        duplicates = get_duplicates(tasks_to_validate)
        if duplicates:
            raise ValidationError('Duplicate tasks: {}.'.format(duplicates))

    @validates("tasks")
    def should_have_correct_names(self, tasks_to_validate):
        invalid_tasks = {task for task in tasks_to_validate if not TASK_RE.match(task)}
        if len(invalid_tasks) > 0:
            raise ValidationError('Invalid tasks: "{}". Example: "valid/task".'.format(invalid_tasks))


class TaskPrefixField(fields.String):
    def __init__(self, required=False):
        super().__init__(
            validate=validate.Regexp(
                TASK_PREFIX_RE,
                error='"{input}" is not valid task prefix. Example: "valid/task/prefix/".'
            ),
            required=required
        )


class TariffsSchema(Schema):
    task_group = fields.String(required=True)
    task_subgroup = fields.String(required=False)
    task_prefix = TaskPrefixField(required=False)
    task_prefixes = fields.List(
        TaskPrefixField(required=True),
        validate=validate.Length(min=1)
    )

    tariffs = fields.List(
        fields.Nested(TariffSchema()),
        required=True,
        validate=validate.Length(min=1)
    )

    @validates_schema
    def should_contain_prefix_or_prefixes(self, data, **kwargs):
        if 'task_prefix' in data and 'task_prefixes' in data:
            raise ValidationError('`task_prefix` and `task_prefixes` cannot be specified simultaneously.')
        if 'task_prefix' not in data and 'task_prefixes' not in data:
            raise ValidationError('`task_prefix` or `task_prefixes` must be specified.')

    @validates('task_prefixes')
    def should_contain_unique_prefixes(self, task_prefixes):
        duplicates = get_duplicates(task_prefixes)
        if duplicates:
            raise ValidationError('Duplicate task prefixes: {}.'.format(duplicates))

    @validates('tariffs')
    def should_not_contain_duplicate_tariffs(self, tariffs_to_validate):
        tariff_names = [tariff['tariff_name'] for tariff in tariffs_to_validate]
        duplicates = get_duplicates(tariff_names)
        if duplicates:
            raise ValidationError('Duplicate tariff names: {}.'.format(duplicates))

    @validates_schema
    def should_not_specify_required_role_outside_of_assessment(self, data, **kwargs):
        task_prefixes = data.get('task_prefixes', [])
        if 'task_prefix' in data:
            task_prefixes.append(data['task_prefix'])

        is_assessment_only = all(task_prefix.startswith('assessment/') for task_prefix in task_prefixes)
        mentions_roles = any('required_role' in tariff for tariff in data.get('tariffs', []))

        if not is_assessment_only and mentions_roles:
            raise ValidationError('required_role is for assessment/ tariffs only.')
