import re
from onecodex.exceptions import ValidationError


def validate_appendables(appendables, api):
    appendables['valid_tags'] = []
    appendables['valid_metadata'] = {'custom': {}}
    validate_tags(appendables, api)
    validate_metadata(appendables, api)
    return appendables


def validate_tags(appendables, api):
    if 'tags' not in appendables:
        return

    tag_array = appendables['tags']
    for tag in tag_array:
        name_property = api.Tags._resource._schema['properties']['name']
        if len(tag) > name_property.get('maxLength', 1000):
            raise ValidationError('{} is too long'.format(tag))

        appendables['valid_tags'].append({'name': tag})


def validate_metadata(appendables, api):
    if 'metadata' not in appendables:
        return

    schema_props = metadata_properties(api)
    for key, value in appendables['metadata'].items():
        if is_blacklisted(key):
            raise ValidationError('{} cannot be manually updated'.format(key))

        if key in schema_props.keys():
            settable_value = validate_metadata_against_schema(schema_props, key, value)
            appendables['valid_metadata'][key] = settable_value
        else:
            coerced_value = coerce_custom_value(value)
            appendables['valid_metadata']['custom'][key] = coerced_value


def validate_metadata_against_schema(schema_props, key, value):
    schema_rules = schema_props[key]
    if 'enum' in schema_rules:
        return validate_enum(value, schema_rules)
    elif 'number' in schema_rules['type']:
        return validate_number(value, schema_rules)
    elif 'boolean' in schema_rules['type']:
        return validate_boolean(value)
    elif 'format' in schema_rules and 'date-time' in schema_rules['format']:
        return validate_datetime(value)
    else:
        return value


def validate_enum(value, schema_rules):
    if value not in schema_rules['enum']:
        # This is gross, but is necessary for string comparison between Python2 ([u'Illumina HiSeq']) and Python3 (['Illumina Hiseq']). On the plus side, it makes the error message more Human Readable.
        error_array = []
        for rule in schema_rules['enum']:
            if rule:
                error_array.append(str(rule))
            else:
                error_array.append(rule)
        raise ValidationError('{} is not a valid value for this key. Value must be one of the following options: {}'.format(value, error_array))
    return value


def validate_number(value, schema_rules):
    num_value = value
    try:
        num_value = float(value)
    except ValueError:
        raise ValidationError('{} must be a number'.format(value))
    if 'minimum' in schema_rules and num_value <= schema_rules['minimum']:
        raise ValidationError('{} must be larger than the minimum value: {}'.format(value, schema_rules['minimum']))
    if 'maximum' in schema_rules and num_value >= schema_rules['maximum']:
        raise ValidationError('{} must be smaller than the maximum value: {}'.format(value, schema_rules['maximum']))
    return num_value


def validate_boolean(value):
    if value.lower() in truthy_values():
        return True
    elif value.lower() in falsy_values():
        return False
    else:
        raise ValidationError('{} must be either "true" or "false"'.format(value))


def validate_datetime(value):
    if not is_iso_8601_compliant(value):
        raise ValidationError('"{}" must be formatted in iso8601 compliant date format. Example: "2018-05-15T16:21:36+00:00"'.format(value))

    return value


def is_blacklisted(key):
    return key in ['$uri', 'custom']


def truthy_values():
    return ['true', '1', 't', 'y', 'yes']


def falsy_values():
    return ['false', '0', 'f', 'n', 'no']


def coerce_custom_value(value):
    try:
        return float(value)
    except ValueError:
        pass

    if value.lower() in truthy_values():
        return True

    if value.lower() in falsy_values():
        return False

    # String case, includes dates in JSON
    return value


def is_iso_8601_compliant(value):
    iso8601 = re.compile(r'^(?P<full>((?P<year>\d{4})([/-]?(?P<mon>(0[1-9])|(1[012]))([/-]?(?P<mday>(0[1-9])|([12]\d)|(3[01])))?)?(?:T(?P<hour>([01][0-9])|(?:2[0123]))(\:?(?P<min>[0-5][0-9])(\:?(?P<sec>[0-5][0-9]([\,\.]\d{1,10})?))?)?(?:Z|([\-+](?:([01][0-9])|(?:2[0123]))(\:?(?:[0-5][0-9]))?))?)?))$')
    return iso8601.match(value)


def metadata_properties(api):
    return api.Metadata._resource._schema['properties']

import re
from onecodex.exceptions import ValidationError


def validate_appendables(appendables, api):
    appendables['valid_tags'] = []
    appendables['valid_metadata'] = {'custom': {}}
    validate_tags(appendables, api)
    validate_metadata(appendables, api)
    return appendables


def validate_tags(appendables, api):
    if 'tags' not in appendables:
        return

    tag_array = appendables['tags']
    for tag in tag_array:
        name_property = api.Tags._resource._schema['properties']['name']
        if len(tag) > name_property.get('maxLength', 1000):
            raise ValidationError('{} is too long'.format(tag))

        appendables['valid_tags'].append({'name': tag})


def validate_metadata(appendables, api):
    if 'metadata' not in appendables:
        return

    schema_props = metadata_properties(api)
    for key, value in appendables['metadata'].items():
        if is_blacklisted(key):
            raise ValidationError('{} cannot be manually updated'.format(key))

        if key in schema_props.keys():
            settable_value = validate_metadata_against_schema(schema_props, key, value)
            appendables['valid_metadata'][key] = settable_value
        else:
            coerced_value = coerce_custom_value(value)
            appendables['valid_metadata']['custom'][key] = coerced_value


def validate_metadata_against_schema(schema_props, key, value):
    schema_rules = schema_props[key]
    if 'enum' in schema_rules:
        return validate_enum(value, schema_rules)
    elif 'number' in schema_rules['type']:
        return validate_number(value, schema_rules)
    elif 'boolean' in schema_rules['type']:
        return validate_boolean(value)
    elif 'format' in schema_rules and 'date-time' in schema_rules['format']:
        return validate_datetime(value)
    else:
        return value


def validate_enum(value, schema_rules):
    if value not in schema_rules['enum']:
        # This is gross, but is necessary for string comparison between Python2 ([u'Illumina HiSeq']) and Python3 (['Illumina Hiseq']). On the plus side, it makes the error message more Human Readable.
        error_array = []
        for rule in schema_rules['enum']:
            if rule:
                error_array.append(str(rule))
            else:
                error_array.append(rule)
        raise ValidationError('{} is not a valid value for this key. Value must be one of the following options: {}'.format(value, error_array))
    return value


def validate_number(value, schema_rules):
    num_value = value
    try:
        num_value = float(value)
    except ValueError:
        raise ValidationError('{} must be a number'.format(value))
    if 'minimum' in schema_rules and num_value <= schema_rules['minimum']:
        raise ValidationError('{} must be larger than the minimum value: {}'.format(value, schema_rules['minimum']))
    if 'maximum' in schema_rules and num_value >= schema_rules['maximum']:
        raise ValidationError('{} must be smaller than the maximum value: {}'.format(value, schema_rules['maximum']))
    return num_value


def validate_boolean(value):
    if value.lower() in truthy_values():
        return True
    elif value.lower() in falsy_values():
        return False
    else:
        raise ValidationError('{} must be either "true" or "false"'.format(value))


def validate_datetime(value):
    if not is_iso_8601_compliant(value):
        raise ValidationError('"{}" must be formatted in iso8601 compliant date format. Example: "2018-05-15T16:21:36+00:00"'.format(value))

    return value


def is_blacklisted(key):
    return key in ['$uri', 'custom']


def truthy_values():
    return ['true', '1', 't', 'y', 'yes']


def falsy_values():
    return ['false', '0', 'f', 'n', 'no']


def coerce_custom_value(value):
    try:
        return float(value)
    except ValueError:
        pass

    if value.lower() in truthy_values():
        return True

    if value.lower() in falsy_values():
        return False

    # String case, includes dates in JSON
    return value


def is_iso_8601_compliant(value):
    iso8601 = re.compile(r'^(?P<full>((?P<year>\d{4})([/-]?(?P<mon>(0[1-9])|(1[012]))([/-]?(?P<mday>(0[1-9])|([12]\d)|(3[01])))?)?(?:T(?P<hour>([01][0-9])|(?:2[0123]))(\:?(?P<min>[0-5][0-9])(\:?(?P<sec>[0-5][0-9]([\,\.]\d{1,10})?))?)?(?:Z|([\-+](?:([01][0-9])|(?:2[0123]))(\:?(?:[0-5][0-9]))?))?)?))$')
    return iso8601.match(value)


def metadata_properties(api):
    return api.Metadata._resource._schema['properties']

# This is gross, but is necessary for string comparison between Python2 ([u'Illumina HiSeq']) and Python3 (['Illumina Hiseq']). On the plus side, it makes the error message more Human Readable. # String case, includes dates in JSON