from __future__ import annotations

from datetime import datetime
from re import Pattern
from typing import TYPE_CHECKING, Any

from starlite.exceptions import MissingDependencyException
from starlite.openapi.spec.enums import OpenAPIFormat, OpenAPIType
from starlite.openapi.spec.schema import Schema
from starlite.utils import is_class_and_subclass

if TYPE_CHECKING:
    from starlite.plugins import OpenAPISchemaPluginProtocol

if TYPE_CHECKING:
    from starlite._signature.models import SignatureField

    try:
        from pydantic import (
            ConstrainedBytes,
            ConstrainedDate,
            ConstrainedDecimal,
            ConstrainedFloat,
            ConstrainedFrozenSet,
            ConstrainedInt,
            ConstrainedList,
            ConstrainedSet,
            ConstrainedStr,
        )
    except ImportError:
        ConstrainedBytes = Any  # type: ignore
        ConstrainedDate = Any  # type: ignore
        ConstrainedDecimal = Any  # type: ignore
        ConstrainedFloat = Any  # type: ignore
        ConstrainedFrozenSet = Any  # type: ignore
        ConstrainedInt = Any  # type: ignore
        ConstrainedList = Any  # type: ignore
        ConstrainedSet = Any  # type: ignore
        ConstrainedStr = Any  # type: ignore


__all__ = (
    "create_collection_constrained_field_schema",
    "create_constrained_field_schema",
    "create_date_constrained_field_schema",
    "create_numerical_constrained_field_schema",
    "create_string_constrained_field_schema",
    "is_pydantic_constrained_field",
)


def is_pydantic_constrained_field(annotation: Any) -> bool:
    """Check if the given annotation is a constrained pydantic type.

    Args:
        annotation: A type annotation

    Returns:
        True if pydantic is installed and the type is a constrained type, otherwise False.
    """
    try:
        import pydantic

        return any(
            is_class_and_subclass(annotation, constrained_type)
            for constrained_type in (
                pydantic.ConstrainedBytes,
                pydantic.ConstrainedDate,
                pydantic.ConstrainedDecimal,
                pydantic.ConstrainedFloat,
                pydantic.ConstrainedFrozenSet,
                pydantic.ConstrainedInt,
                pydantic.ConstrainedList,
                pydantic.ConstrainedSet,
                pydantic.ConstrainedStr,
            )
        )
    except ImportError:
        return False


def create_numerical_constrained_field_schema(
    field_type: type["ConstrainedFloat"] | type["ConstrainedInt"] | type["ConstrainedDecimal"],
) -> Schema:
    """Create Schema from Constrained Int/Float/Decimal field."""
    schema = Schema(type=OpenAPIType.INTEGER if issubclass(field_type, int) else OpenAPIType.NUMBER)
    if field_type.le is not None:
        schema.maximum = float(field_type.le)
    if field_type.lt is not None:
        schema.exclusive_maximum = float(field_type.lt)
    if field_type.ge is not None:
        schema.minimum = float(field_type.ge)
    if field_type.gt is not None:
        schema.exclusive_minimum = float(field_type.gt)
    if field_type.multiple_of is not None:
        schema.multiple_of = float(field_type.multiple_of)
    return schema


def create_date_constrained_field_schema(field_type: type["ConstrainedDate"]) -> Schema:
    """Create Schema from Constrained Date Field."""
    schema = Schema(type=OpenAPIType.STRING, format=OpenAPIFormat.DATE)
    if field_type.le is not None:
        schema.maximum = float(datetime.combine(field_type.le, datetime.min.time()).timestamp())
    if field_type.lt is not None:
        schema.exclusive_maximum = float(datetime.combine(field_type.lt, datetime.min.time()).timestamp())
    if field_type.ge is not None:
        schema.minimum = float(datetime.combine(field_type.ge, datetime.min.time()).timestamp())
    if field_type.gt is not None:
        schema.exclusive_minimum = float(datetime.combine(field_type.gt, datetime.min.time()).timestamp())
    return schema


def create_string_constrained_field_schema(field_type: type["ConstrainedStr"] | type["ConstrainedBytes"]) -> Schema:
    """Create Schema from Constrained Str/Bytes field."""
    schema = Schema(type=OpenAPIType.STRING)
    if field_type.min_length:
        schema.min_length = field_type.min_length
    if field_type.max_length:
        schema.max_length = field_type.max_length
    if hasattr(field_type, "regex") and isinstance(field_type.regex, Pattern):
        schema.pattern = field_type.regex.pattern
    if field_type.to_lower:
        schema.description = "must be in lower case"
    return schema


def create_collection_constrained_field_schema(
    field_type: type["ConstrainedList"] | type["ConstrainedSet"] | type["ConstrainedFrozenSet"],
    children: tuple["SignatureField", ...] | None,
    plugins: list["OpenAPISchemaPluginProtocol"],
    schemas: dict[str, Schema],
) -> Schema:
    """Create Schema from Constrained List/Set field.

    Args:
        field_type: A constrained field type.
        children: Any child fields.
        plugins: A list of plugins.
        schemas: A mapping of namespaces to schemas - this mapping is used in the OA components section.

    Returns:
        A schema instance.
    """
    import pydantic

    from starlite._openapi.schema_generation import create_schema

    schema = Schema(type=OpenAPIType.ARRAY)
    if field_type.min_items:
        schema.min_items = field_type.min_items
    if field_type.max_items:
        schema.max_items = field_type.max_items
    if issubclass(field_type, (pydantic.ConstrainedSet, pydantic.ConstrainedFrozenSet)):
        schema.unique_items = True
    if children:
        items = [
            create_schema(field=sub_field, generate_examples=False, plugins=plugins, schemas=schemas)
            for sub_field in children
        ]
        if len(items) > 1:
            schema.items = Schema(one_of=items)
        else:
            schema.items = items[0]
    else:
        from starlite._signature.models import SignatureField

        schema.items = create_schema(
            field=SignatureField.create(field_type=field_type.item_type, name=f"{field_type.__name__}Field"),
            generate_examples=False,
            plugins=plugins,
            schemas=schemas,
        )
    return schema


def create_constrained_field_schema(
    field_type: type["ConstrainedBytes"]
    | type["ConstrainedDate"]
    | type["ConstrainedDecimal"]
    | type["ConstrainedFloat"]
    | type["ConstrainedFrozenSet"]
    | type["ConstrainedInt"]
    | type["ConstrainedList"]
    | type["ConstrainedSet"]
    | type["ConstrainedStr"],
    children: tuple["SignatureField", ...] | None,
    plugins: list["OpenAPISchemaPluginProtocol"],
    schemas: dict[str, Schema],
) -> "Schema":
    """Create Schema for Pydantic Constrained fields (created using constr(), conint() and so forth, or by subclassing
    Constrained*)

    Args:
        field_type: A constrained field type.
        children: Any children.
        plugins: A list of plugins.
        schemas: A mapping of namespaces to schemas - this mapping is used in the OA components section.

    Returns:
        A schema instance.

    """
    try:
        import pydantic

        if issubclass(field_type, (pydantic.ConstrainedFloat, pydantic.ConstrainedInt, pydantic.ConstrainedDecimal)):
            return create_numerical_constrained_field_schema(field_type=field_type)
        if issubclass(field_type, (pydantic.ConstrainedStr, pydantic.ConstrainedBytes)):
            return create_string_constrained_field_schema(field_type=field_type)
        if issubclass(field_type, pydantic.ConstrainedDate):
            return create_date_constrained_field_schema(field_type=field_type)
        return create_collection_constrained_field_schema(
            field_type=field_type, children=tuple(children) if children else None, plugins=plugins, schemas=schemas
        )
    except ImportError as e:
        raise MissingDependencyException("pydantic dependencies are not installed") from e
