import json
from enum import unique, Enum
from typing import List, Any

from ..utils.singleton import Singleton


@unique
class PartitionBy(Enum):
    @staticmethod
    def year(column: str = '{column}'):
        return f'toYear({column})'

    @staticmethod
    def year_and_month(column: str = '{column}'):
        return f'toYYYYMM({column})'

    @staticmethod
    def toDate(column: str = '{column}'):
        return f'toDate({column})'

    @staticmethod
    def divide(amount: int, column: str = '{column}'):
        return f'intDiv({column}, {amount})'

    @staticmethod
    def substring(length: int, offset: int = 0, column: str = '{column}'):
        return f'substring({column}, {offset}, {length})'

    @staticmethod
    def modulo(amount: int, column: str = '{column}'):
        return f'modulo({column}, {amount})'

    @staticmethod
    def raw(column: str = '{column}'):
        return f'{column}'

    @staticmethod
    def disabled():
        return f''


class ClickHouseType(metaclass=Singleton):
    python_type: type = None
    clickhouse_type: str = None
    default_partition_by: str = PartitionBy.raw()
    select_key_wrapper = '{}'

    @classmethod
    def prepare_insert(cls, value: Any) -> Any:
        return value

    @classmethod
    def prepare_return(cls, value: Any) -> Any:
        return value

    @classmethod
    def cast_to_python_type(cls, value: Any) -> Any:
        return cls.python_type(value)

    #     ------------------need refactor----------------------
    # TODO need refactor
    @classmethod
    def cast_type(cls, value):
        if isinstance(value, bool):
            value = int(value)

        if not cls.check_value(value) and value is not None:
            try:
                value = cls.fix_value(value)
            except (TypeError, ValueError) as e:
                raise ValueError(f"wrong value: {value} for type {cls.__name__}") from e

        return value

    @classmethod
    def check_value(cls, value) -> bool:
        return True

    @classmethod
    def fix_value(cls, value):
        return cls.python_type(value)
#     ------------------need refactor----------------------


class Json(ClickHouseType):
    python_type = dict
    clickhouse_type = 'String'

    @classmethod
    def prepare_insert(cls, value) -> str:
        return json.dumps(value)

    @classmethod
    def prepare_return(cls, value: str):
        return json.loads(value)


class String(ClickHouseType):
    python_type = str
    clickhouse_type = 'String'

    @classmethod
    def prepare_return(cls, value):
        return cls.python_type(value)


class IntegerArray(ClickHouseType):
    python_type = list
    clickhouse_type = 'Array(Int32)'

    @classmethod
    def check_value(cls, value) -> False:
        if not isinstance(value, list):
            return False
        for v in value:
            if not isinstance(v, int) or v > 2 ** 30 or v < -(2 ** 30):
                return False
        return True

    @classmethod
    def fix_value(cls, value) -> List[int]:
        if not isinstance(value, list):
            return []
        for i, v in enumerate(value):
            if not isinstance(value, int):
                value[i] = int(v)
            elif value > 2 ** 30 or value < -(2 ** 30):
                raise ValueError('extremely big value')
        return value


class BigIntegerArray(ClickHouseType):
    python_type = list
    clickhouse_type = 'Array(Int64)'


class IntegerArrayOfArrays(ClickHouseType):
    python_type = list
    clickhouse_type = 'Array(Array(Int32))'

    @classmethod
    def prepare_return(cls, value: str):
        return json.loads(value)


class TinyIntegerArray(ClickHouseType):
    python_type = list
    clickhouse_type = 'Array(Int8)'


class Float(ClickHouseType):
    python_type = float
    clickhouse_type = 'Float32'

    @classmethod
    def prepare_return(cls, value):
        return cls.python_type(value)


class LongFloat(Float):
    clickhouse_type = 'Float64'


class Double(LongFloat):
    pass


class Integer(ClickHouseType):
    python_type = int
    clickhouse_type = 'Int32'

    @classmethod
    def prepare_return(cls, value):
        return cls.python_type(value)


class TinyInteger(Integer):
    clickhouse_type = 'Int8'

    @classmethod
    def prepare_return(cls, value):
        return cls.python_type(value)


class BigInteger(Integer):
    clickhouse_type = 'Int64'

    @classmethod
    def prepare_return(cls, value):
        return cls.python_type(value)


class UnsignedBigInteger(Integer):
    clickhouse_type = 'UInt64'


class SmallInteger(Integer):
    clickhouse_type = 'Int16'


class DateTime(Integer):
    select_key_wrapper = 'toUInt64({})'
    default_partition_by = PartitionBy.year_and_month()
    clickhouse_type = 'DateTime'
