import json
import re
from datetime import datetime, date, time
from typing import Type, Any

import pydantic
import sqlalchemy
import enum


class ColumnFactory(object):

    func_count = 0
    primary_key = False
    allow_null = False
    allow_blank = False
    index = False
    unique = False
    constraints = []
    column_type = None

    @classmethod
    def get_column(cls, name: str) -> sqlalchemy.Column:
        return sqlalchemy.Column(
            name,
            cls.column_type,
            *cls.constraints,
            primary_key=cls.primary_key,
            nullable=cls.allow_null and not cls.primary_key,
            index=cls.index,
            unique=cls.unique,
        )


def String(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
    allow_blank: bool = False,
    strip_whitespace: bool = False,
    min_length: int = None,
    max_length: int = None,
    curtail_length: int = None,
    regex: str = None,
) -> Type[str]:

    assert max_length is not None, "max_length required field (> 0)."
    assert max_length > 0, "max_length > 0 is required"

    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        allow_blank=allow_blank,
        strip_whitespace=strip_whitespace,
        min_length=min_length,
        max_length=max_length,
        curtail_length=curtail_length,
        regex=regex and re.compile(regex),
        column_type=sqlalchemy.String(length=max_length),
    )

    return type("String", (pydantic.ConstrainedStr, ColumnFactory), namespace)


def Text(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
    allow_blank: bool = False,
    strip_whitespace: bool = False,
) -> Type[str]:

    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        allow_blank=allow_blank,
        strip_whitespace=strip_whitespace,
        column_type=sqlalchemy.Text(),
    )

    return type("Text", (pydantic.ConstrainedStr, ColumnFactory), namespace)


def Integer(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
    minimum: int = None,
    maximum: int = None,
    multiple_of: int = None,
) -> Type[int]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        ge=minimum,
        le=maximum,
        multiple_of=multiple_of,
        column_type=sqlalchemy.Integer(),
    )
    return type("Integer", (pydantic.ConstrainedInt, ColumnFactory), namespace)


def Float(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
    minimum: float = None,
    maximum: float = None,
    multiple_of: int = None,
) -> Type[int]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        ge=minimum,
        le=maximum,
        multiple_of=multiple_of,
        column_type=sqlalchemy.Float(),
    )
    return type("Float", (pydantic.ConstrainedFloat, ColumnFactory), namespace)


def Boolean(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[bool]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.Boolean(),
    )
    return type("Boolean", (int, ColumnFactory), namespace)


def DateTime(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[datetime]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.DateTime(),
    )
    return type("DateTime", (datetime, ColumnFactory), namespace)


def Date(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[date]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.Date(),
    )
    return type("Date", (date, ColumnFactory), namespace)


def Time(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[time]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.Time(),
    )
    return type("Time", (time, ColumnFactory), namespace)


def JSON(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[Any]:
    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.JSON(),
    )

    class Json(object):
        @classmethod
        def __get_validators__(cls) -> "CallableGenerator":
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> Any:
            try:
                if isinstance(v, str):
                    return json.loads(v)
                else:
                    return v
            except ValueError:
                raise errors.JsonError()
            except TypeError:
                raise errors.JsonTypeError()

    return type("JSON", (Json, ColumnFactory), namespace)


def ForeignKey(to, *, allow_null: bool = False) -> Type[object]:
    fk_string = to.Mapping.table_name + "." + to.Mapping.pk_name
    to_field = to.__fields__[to.Mapping.pk_name]
    namespace = dict(
        to=to,
        allow_null=allow_null,
        constraints=[sqlalchemy.schema.ForeignKey(fk_string)],
        column_type=to_field.type_.column_type,
    )

    class ForeignKeyField(object):
        @classmethod
        def __get_validators__(cls) -> "CallableGenerator":
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> Any:
            return v

    return type("ForeignKey", (ForeignKeyField, ColumnFactory), namespace)


def Enum(
    enum_type: Type[enum.Enum],
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[enum.Enum]:

    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.Enum(enum_type),
    )

    class EnumField(object):
        @classmethod
        def __get_validators__(cls) -> "CallableGenerator":
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> Any:
            return v

    return type("Enum", (EnumField, ColumnFactory), namespace)


def StringArray(
    *,
    primary_key: bool = False,
    allow_null: bool = False,
    index: bool = False,
    unique: bool = False,
) -> Type[str]:

    namespace = dict(
        primary_key=primary_key,
        allow_null=allow_null,
        index=index,
        unique=unique,
        column_type=sqlalchemy.ARRAY(sqlalchemy.String),
    )

    class StringArrayField(object):
        @classmethod
        def __get_validators__(cls) -> "CallableGenerator":
            yield cls.validate

        @classmethod
        def validate(cls, v: Any) -> Any:
            return v

    return type("StringArray", (StringArrayField, ColumnFactory), namespace)