# -*- coding: utf-8 -*-
import functools

from jsonschema import validators
from jsonschema.exceptions import ValidationError
from jsonschema.validators import Draft4Validator
from swagger_spec_validator.ref_validators import in_scope

from bravado_core.model import MODEL_MARKER
from bravado_core.schema import is_param_spec
from bravado_core.schema import is_prop_nullable
from bravado_core.schema import is_required
from bravado_core.util import memoize_by_id


"""Draft4Validator is not completely compatible with Swagger 2.0 schema
objects like parameter, etc. Swagger20Validator is an extension of
Draft4Validator which customizes/wraps some of the operations of the default
validator.
"""

# Extract "original" validators as defined in jsonschema library. Saving those to constants to reduce indirections during validation.  # noqa
_DRAFT4_ENUM_VALIDATOR = Draft4Validator.VALIDATORS['enum']
_DRAFT4_FORMAT_VALIDATOR = Draft4Validator.VALIDATORS['format']
_DRAFT4_REQUIRED_VALIDATOR = Draft4Validator.VALIDATORS['required']
_DRAFT4_TYPE_VALIDATOR = Draft4Validator.VALIDATORS['type']


def format_validator(swagger_spec, validator, format, instance, schema):
    """Skip the `format` validator when a Swagger parameter value is None.
    Otherwise it will fail with a "Failed validating u'format' in schema" failure instead
    of letting the downstream `required_validator` do its job.
    Also skip when a Swagger property value is None and the schema contains
    the extension field `x-nullable` set to True.
    In all other cases, delegate to the existing Draft4 `format` validator.

    """
    if instance is None and (
        is_param_spec(swagger_spec, schema) or
        is_prop_nullable(swagger_spec, schema)
    ):
        return

    for error in _DRAFT4_FORMAT_VALIDATOR(validator, format, instance, schema):
        yield error


def type_validator(swagger_spec, validator, types, instance, schema):
    """Skip the `type` validator when a Swagger parameter value is None.
    Otherwise it will fail with a "None is not a valid type" failure instead
    of letting the downstream `required_validator` do its job.
    Also skip when a Swagger property value is None and the schema contains
    the extension field `x-nullable` set to True.
    In all other cases, delegate to the existing Draft4 `type` validator.

    :param swagger_spec: needed for access to deref()
    :type swagger_spec: :class:`bravado_core.spec.Spec`
    :param validator: Validator class used to validate the object
    :type validator: :class:`Swagger20Validator` or
        :class:`jsonschema.validators.Draft4Validator`
    :param types: validate types
    :type types: string or list
    :param instance: object instance value
    :param schema: swagger spec for the object
    :type schema: dict
    """
    if instance is None and (
        is_param_spec(swagger_spec, schema) or
        is_prop_nullable(swagger_spec, schema)
    ):
        return

    for error in _DRAFT4_TYPE_VALIDATOR(validator, types, instance, schema):
        yield error


def required_validator(swagger_spec, validator, required, instance, schema):
    """Swagger 2.0 expects `required` to be a bool in the Parameter object,
    but a list of properties everywhere else.

    :param swagger_spec: needed for access to deref()
    :type swagger_spec: :class:`bravado_core.spec.Spec`
    :param validator: Validator class used to validate the object
    :type validator: :class:`Swagger20Validator` or
        :class:`jsonschema.validators.Draft4Validator`
    :param required: value of `required` field
    :type required: boolean or list or None
    :param instance: object instance value
    :param schema: swagger spec for the object
    :type schema: dict
    """
    if is_param_spec(swagger_spec, schema):
        if required and instance is None:
            yield ValidationError('{0} is a required parameter.'.format(schema['name']))
    else:
        for error in _DRAFT4_REQUIRED_VALIDATOR(validator, required, instance, schema):
            yield error


def enum_validator(swagger_spec, validator, enums, instance, schema):
    """Swagger 2.0 allows enums to be validated against objects of type
    arrays, like query parameter (collectionFormat: multi)

    :param swagger_spec: needed for access to deref()
    :type swagger_spec: :class:`bravado_core.spec.Spec`
    :param validator: Validator class used to validate the object
    :type validator: :class: `Swagger20Validator` or
        :class: `jsonschema.validators.Draft4Validator`
    :param enums: allowed enum values
    :type enums: list
    :param instance: enum instance value
    :param schema: swagger spec for the object
    :type schema: dict
    """

    if instance is None and is_prop_nullable(swagger_spec, schema):
        return

    if schema.get('type') == 'array':
        for element in instance:
            for error in _DRAFT4_ENUM_VALIDATOR(validator, enums, element, schema):
                yield error
        return

    # Handle optional enum params with no value
    if is_param_spec(swagger_spec, schema):
        if instance is None and not is_required(swagger_spec, schema):
            return

    for error in _DRAFT4_ENUM_VALIDATOR(validator, enums, instance, schema):
        yield error


def discriminator_validator(swagger_spec, validator, discriminator_attribute, instance, schema):
    """
    Validates instance against the schema defined by the discriminator attribute.

    [Swagger 2.0 Schema Object](http://swagger.io/specification/#schemaObject) allows discriminator field to be defined.
    discriminator field defines the attribute that will be used to discriminate the object type.

    NOTE: discriminator_validator assumes that discriminator_attribute is not None or empty

    :param swagger_spec: needed for access to deref()
    :type swagger_spec: :class:`bravado_core.spec.Spec`
    :param validator: Validator class used to validate the object
    :type validator: :class: `Swagger20Validator` or
        :class: `jsonschema.validators.Draft4Validator`
    :param discriminator_attribute: name of the discriminator attribute
    :type discriminator_attribute: str
    :param instance: object instance value
    :type instance: dict
    :param schema: swagger spec for the object
    :type schema: dict
    """

    try:
        discriminator_value = instance[discriminator_attribute]
    except KeyError:
        raise ValidationError("'{}' is a required property".format(discriminator_attribute))

    if discriminator_value not in swagger_spec.definitions:
        raise ValidationError(
            message='\'{}\' is not a recognized schema'.format(discriminator_value),
        )

    if discriminator_value == schema[MODEL_MARKER]:
        return

    discriminated_schema = swagger_spec.definitions[discriminator_value]._model_spec
    if 'allOf' not in discriminated_schema:
        raise ValidationError(
            message='discriminated schema \'{}\' must inherit from \'{}\''.format(
                discriminator_value, schema[MODEL_MARKER],
            ),
        )

    schemas_to_remove = [s for s in discriminated_schema['allOf'] if swagger_spec.deref(s) == schema]
    if not schemas_to_remove:
        # Not checking against len(schemas_to_remove) > 1 because it should be prevented by swagger spec validation
        raise ValidationError(
            message='discriminated schema \'{}\' must inherit from \'{}\''.format(
                discriminator_value, schema[MODEL_MARKER],
            ),
        )

    # Remove the current schema from the allOf list in order to avoid unbounded recursion
    # (the current object is already validated against schema)
    # WARNING: This is especially important if internally_dereference_refs is set to true
    #   as we're modifying new_schema and new_schema is a dict (so mutable) we need to copy
    #   it in order to have a brand new dictionary that we can modify
    new_schema = discriminated_schema.copy()
    new_schema['allOf'] = [
        all_of_schema if all_of_schema not in schemas_to_remove else {}
        for all_of_schema in new_schema['allOf']
    ]

    from bravado_core.validate import validate_object  # Local import due to circular dependency
    validate_object(swagger_spec=swagger_spec, object_spec=new_schema, value=instance)


def ref_validator(validator, ref, instance, schema):
    """When validating a request or response that contain $refs, jsonschema's
     RefResolver only contains scope (RefResolver._scopes_stack) for the
     request_spec or response_spec that it is fed. This scope doesn't contain
     the full scope built when ingesting the spec from its root
     (#/ in swagger.json). So, we need to modify the behavior of ref
     validation to use the `x-scope` annotations that were created during spec
     ingestion (see model_discovery in bravado_core/model.py).

    :param validator: Validator class used to validate the object
    :type validator: :class: `Swagger20Validator` or
        :class: `jsonschema.validators.Draft4Validator`
    :param ref: the target of the ref. eg. #/foo/bar/Baz
    :type ref: string
    :param instance: the object being validated
    :param schema: swagger spec that contains the ref.
        eg {'$ref': '#/foo/bar/Baz'}
    :type schema: dict
    """
    # This is a copy of jsonschema._validators.ref(..) with the
    # in_scope(..) context manager applied before any refs are resolved.
    resolve = getattr(validator.resolver, "resolve")
    if resolve is None:
        with in_scope(validator.resolver, schema):
            with validator.resolver.resolving(ref) as resolved:
                for error in validator.descend(instance, resolved):
                    yield error
    else:
        with in_scope(validator.resolver, schema):
            scope, resolved = validator.resolver.resolve(ref)
        validator.resolver.push_scope(scope)

        try:
            for error in validator.descend(instance, resolved):
                yield error
        finally:
            validator.resolver.pop_scope()


@memoize_by_id
def get_validator_type(swagger_spec):
    """Create a custom jsonschema validator for Swagger 2.0 specs.

    :rtype: Its complicated. See jsonschema.validators.create()
    """
    return validators.extend(
        Draft4Validator,
        {
            '$ref': ref_validator,
            'required': functools.partial(required_validator, swagger_spec),
            'enum': functools.partial(enum_validator, swagger_spec),
            'type': functools.partial(type_validator, swagger_spec),
            'format': functools.partial(format_validator, swagger_spec),
            'discriminator': functools.partial(discriminator_validator, swagger_spec),
        },
    )
