from functools import partial
from typing import Any, Callable, Dict, List

from .array_validators import (
    validate_items,
    validate_maximum_items,
    validate_minimum_items,
    validate_tuple,
    validate_unique_items,
)
from .combining_validators import (
    validate_all_of,
    validate_any_of,
    validate_not,
    validate_one_of,
)
from .number_validators import (
    validate_exclusive_maximum,
    validate_exclusive_minimum,
    validate_maximum,
    validate_minimum,
    validate_multiple_of,
)
from .object_validators import (
    validate_object_dependencies,
    validate_object_maximum_properties,
    validate_object_minimum_properties,
    validate_object_properties,
    validate_object_property_names,
    validate_object_required_properties,
)
from .string_validators import (
    validate_maximum_length,
    validate_minimum_length,
    validate_string_format,
    validate_string_pattern,
)
from .type_validators import (
    validate_array,
    validate_boolean,
    validate_enum,
    validate_integer,
    validate_number,
    validate_object,
    validate_string,
)


def build_validator_from_schema(schema: Dict[str, Any]) -> Callable:
    if "anyOf" in schema:
        validators = [build_validator_from_schema(item) for item in schema["anyOf"]]
        return partial(validate_any_of, validators=validators)

    if "oneOf" in schema:
        validators = [build_validator_from_schema(item) for item in schema["oneOf"]]
        return partial(validate_one_of, validators=validators)

    if "allOf" in schema:
        return _build_all_of_validator(schema["allOf"])

    if "not" in schema:
        return partial(validate_not, validator=build_validator_from_schema(schema["not"]))

    if "type" in schema:
        return _build_validator_for_type(schema["type"], schema)

    if "enum" in schema:
        return _build_enum_validator(schema)

    raise ValueError("Could not build validator for passed schema")


def _build_all_of_validator(items: List) -> Callable:
    """
    String formatters are casting string values to specific type which corresponds
    to given format. There are scenarios with allOf validator where one property might
    be partially validated and can be cast to certain type which will automatically
    cause to fail the next validator as the value is not longer a string.
    Combining object validators into one fixes this issue.
    """
    combined_object_validator: Dict[str, Any] = {}
    schemas = []
    for item in items:
        if "type" not in item or item["type"] != "object":
            schemas.append(item)
            continue

        combined_object_validator["type"] = "object"

        if "properties" in item:
            if "properties" not in combined_object_validator:
                combined_object_validator["properties"] = {}
            combined_object_validator["properties"].update(item["properties"])
        if "maxProperties" in item:
            combined_object_validator["maxProperties"] = item["maxProperties"]
        if "minProperties" in item:
            combined_object_validator["minProperties"] = item["minProperties"]
        if "required" in item:
            if "required" not in combined_object_validator:
                combined_object_validator["required"] = []
            combined_object_validator["required"] += item["required"]
        if "patternProperties" in item:
            if "patternProperties" not in combined_object_validator:
                combined_object_validator["patternProperties"] = {}
            combined_object_validator["patternProperties"].update(item["patternProperties"])
        if "additionalProperties" in item:
            combined_object_validator["additionalProperties"] = item["additionalProperties"]
        if "propertyNames" in item:
            combined_object_validator["propertyNames"] = item["propertyNames"]

    if combined_object_validator:
        schemas.append(combined_object_validator)

    validators = [build_validator_from_schema(item) for item in schemas]

    return partial(validate_all_of, validators=validators)


def _build_validator_for_type(schema_type: str, definition: Dict[str, Any]) -> Callable:
    if schema_type == "boolean":
        return _build_boolean_validator()

    if schema_type == "integer":
        return _build_number_validator(validate_integer, definition)

    if schema_type == "number":
        return _build_number_validator(validate_number, definition)

    if schema_type == "string":
        return _build_string_validator(definition)

    if schema_type == "array":
        return _build_array_validator(definition)

    if schema_type == "object":
        return _build_object_validator(definition)

    # default to string validation
    return _build_string_validator(definition)


def _build_string_validator(definition: Dict[str, Any]) -> Callable:
    validators = [validate_string]
    if "enum" in definition:
        validators.append(_build_enum_validator(definition))

    if "format" in definition:
        validators.append(partial(validate_string_format, format_name=definition["format"]))

    if "pattern" in definition:
        validators.append(partial(validate_string_pattern, pattern=definition["pattern"]))

    if "minLength" in definition:
        validators.append(partial(validate_minimum_length, expected_minimum=definition["minLength"]))

    if "maxLength" in definition:
        validators.append(partial(validate_maximum_length, expected_maximum=definition["maxLength"]))

    return partial(validate_all_of, validators=validators)


def _build_enum_validator(definition: Dict[str, Any]) -> Callable:
    return partial(validate_enum, values=definition["enum"])


def _build_boolean_validator() -> Callable:
    return validate_boolean


def _build_number_validator(base_validator: Callable, definition: Dict[str, Any]) -> Callable:
    validators = [base_validator]
    _add_range_validators(validators, definition)
    if "multipleOf" in definition:
        validators.append(partial(validate_multiple_of, multiple_of=definition["multipleOf"]))

    if "enum" in definition:
        validators.append(_build_enum_validator(definition))

    return partial(validate_all_of, validators=validators)


def _build_array_validator(definition: Dict[str, Any]) -> Callable:
    validators = [validate_array]
    if "minItems" in definition:
        validators.append(partial(validate_minimum_items, expected_minimum=definition["minItems"]))
    if "maxItems" in definition:
        validators.append(partial(validate_maximum_items, expected_maximum=definition["maxItems"]))

    if "uniqueItems" in definition and definition["uniqueItems"]:
        validators.append(validate_unique_items)

    if "items" in definition:
        if isinstance(definition["items"], list):
            _build_tuple_validator(definition, validators)
        elif isinstance(definition["items"], dict):
            validators.append(
                partial(
                    validate_items,
                    item_validator=build_validator_from_schema(definition["items"]),
                )
            )

    return partial(validate_all_of, validators=validators)


def _build_tuple_validator(definition: Dict[str, Any], validators: List[Callable]) -> None:
    items_validators = [build_validator_from_schema(item_schema) for item_schema in definition["items"]]
    if "additionalItems" in definition:
        if isinstance(definition["additionalItems"], bool):
            validators.append(
                partial(
                    validate_tuple,
                    item_validator=items_validators,
                    additional_items=definition["additionalItems"],
                )
            )
        else:
            validators.append(
                partial(
                    validate_tuple,
                    item_validator=items_validators,
                    additional_items=build_validator_from_schema(definition["additionalItems"]),
                )
            )
    else:
        validators.append(partial(validate_tuple, item_validator=items_validators, additional_items=True))


def _build_object_validator(definition: Dict[str, Any]) -> Callable:
    validators = [validate_object]

    if "propertyNames" in definition:
        definition["propertyNames"]["type"] = "string"
        validators.append(
            partial(
                validate_object_property_names,
                property_names=build_validator_from_schema(definition["propertyNames"]),
            )
        )

    if "minProperties" in definition:
        validators.append(
            partial(
                validate_object_minimum_properties,
                expected_minimum=definition["minProperties"],
            )
        )

    if "maxProperties" in definition:
        validators.append(
            partial(
                validate_object_maximum_properties,
                expected_maximum=definition["maxProperties"],
            )
        )

    if "required" in definition and definition["required"]:
        validators.append(
            partial(
                validate_object_required_properties,
                required_properties=definition["required"],
            )
        )

    if "dependencies" in definition:
        validators.append(partial(validate_object_dependencies, dependencies=definition["dependencies"]))

    property_validator_settings: Dict[str, Any] = {}

    if "properties" in definition:
        properties_validators = {
            property_name: build_validator_from_schema(property_schema)
            for property_name, property_schema in definition["properties"].items()
        }

        property_validator_settings["properties"] = properties_validators

    if "additionalProperties" in definition:
        if isinstance(definition["additionalProperties"], bool):
            property_validator_settings["additional_properties"] = definition["additionalProperties"]
        else:
            property_validator_settings["additional_properties"] = build_validator_from_schema(
                definition["additionalProperties"]
            )

    if "patternProperties" in definition:
        property_validator_settings["pattern_properties"] = {
            key: build_validator_from_schema(value) for key, value in definition["patternProperties"].items()
        }

    if property_validator_settings:
        validators.append(partial(validate_object_properties, **property_validator_settings))

    return partial(validate_all_of, validators=validators)


def _add_range_validators(validators: List[Callable], definition: Dict[str, Any]) -> None:
    if "minimum" in definition:
        validators.append(partial(validate_minimum, expected_minimum=definition["minimum"]))
    if "inclusiveMinimum" in definition:
        validators.append(partial(validate_minimum, expected_minimum=definition["inclusiveMinimum"]))

    if "maximum" in definition:
        validators.append(partial(validate_maximum, expected_maximum=definition["maximum"]))
    if "inclusiveMaximum" in definition:
        validators.append(partial(validate_maximum, expected_maximum=definition["inclusiveMaximum"]))

    if "exclusiveMinimum" in definition:
        validators.append(
            partial(
                validate_exclusive_minimum,
                expected_minimum=definition["exclusiveMinimum"],
            )
        )
    if "exclusiveMaximum" in definition:
        validators.append(
            partial(
                validate_exclusive_maximum,
                expected_maximum=definition["exclusiveMaximum"],
            )
        )
