from dataclasses import dataclass
from typing import List, Dict, Optional

""" Define param names"""


class IndexName:
    # Vector
    AUTOINDEX = "AUTOINDEX"
    FLAT = "FLAT"
    IVF_FLAT = "IVF_FLAT"
    IVF_SQ8 = "IVF_SQ8"
    IVF_PQ = "IVF_PQ"
    IVF_HNSW = "IVF_HNSW"
    HNSW = "HNSW"
    DISKANN = "DISKANN"
    SCANN = "SCANN"
    # binary
    BIN_FLAT = "BIN_FLAT"
    BIN_IVF_FLAT = "BIN_IVF_FLAT"
    # Sparse
    SPARSE_WAND = "SPARSE_WAND"
    SPARSE_INVERTED_INDEX = "SPARSE_INVERTED_INDEX"
    # GPU
    GPU_IVF_FLAT = "GPU_IVF_FLAT"
    GPU_IVF_PQ = "GPU_IVF_PQ"
    GPU_CAGRA = "GPU_CAGRA"
    GPU_BRUTE_FORCE = "GPU_BRUTE_FORCE"

    # Scalar
    INVERTED = "INVERTED"
    BITMAP = "BITMAP"
    Trie = "Trie"
    STL_SORT = "STL_SORT"


class MetricType:
    L2 = "L2"
    IP = "IP"
    COSINE = "COSINE"
    JACCARD = "JACCARD"


""" expressions """


@dataclass
class ExprBase:
    expr: str

    @property
    def subset(self):
        return f"({self.expr})"

    def __repr__(self):
        return self.expr

    @property
    def value(self) -> str:
        return self.expr


class Expr:
    # BooleanConstant: 'true' | 'True' | 'TRUE' | 'false' | 'False' | 'FALSE'

    @staticmethod
    def LT(left, right):
        return ExprBase(expr=f"{left} < {right}")

    @staticmethod
    def LE(left, right):
        return ExprBase(expr=f"{left} <= {right}")

    @staticmethod
    def GT(left, right):
        return ExprBase(expr=f"{left} > {right}")

    @staticmethod
    def GE(left, right):
        return ExprBase(expr=f"{left} >= {right}")

    @staticmethod
    def EQ(left, right):
        return ExprBase(expr=f"{left} == {right}")

    @staticmethod
    def NE(left, right):
        return ExprBase(expr=f"{left} != {right}")

    @staticmethod
    def like(left, right):
        return ExprBase(expr=f'{left} like "{right}"')

    @staticmethod
    def LIKE(left, right):
        return ExprBase(expr=f'{left} LIKE "{right}"')

    @staticmethod
    def exists(name):
        return ExprBase(expr=f'exists {name}')

    @staticmethod
    def EXISTS(name):
        return ExprBase(expr=f'EXISTS {name}')

    @staticmethod
    def ADD(left, right):
        return ExprBase(expr=f"{left} + {right}")

    @staticmethod
    def SUB(left, right):
        return ExprBase(expr=f"{left} - {right}")

    @staticmethod
    def MUL(left, right):
        return ExprBase(expr=f"{left} * {right}")

    @staticmethod
    def DIV(left, right):
        return ExprBase(expr=f"{left} / {right}")

    @staticmethod
    def MOD(left, right):
        return ExprBase(expr=f"{left} % {right}")

    @staticmethod
    def POW(left, right):
        return ExprBase(expr=f"{left} ** {right}")

    @staticmethod
    def SHL(left, right):
        # Note: not supported
        return ExprBase(expr=f"{left}<<{right}")

    @staticmethod
    def SHR(left, right):
        # Note: not supported
        return ExprBase(expr=f"{left}>>{right}")

    @staticmethod
    def BAND(left, right):
        # Note: not supported
        return ExprBase(expr=f"{left} & {right}")

    @staticmethod
    def BOR(left, right):
        # Note: not supported
        return ExprBase(expr=f"{left} | {right}")

    @staticmethod
    def BXOR(left, right):
        # Note: not supported
        return ExprBase(expr=f"{left} ^ {right}")

    @staticmethod
    def AND(left, right):
        return ExprBase(expr=f"{left} && {right}")

    @staticmethod
    def And(left, right):
        return ExprBase(expr=f"{left} and {right}")

    @staticmethod
    def OR(left, right):
        return ExprBase(expr=f"{left} || {right}")

    @staticmethod
    def Or(left, right):
        return ExprBase(expr=f"{left} or {right}")

    @staticmethod
    def BNOT(name):
        # Note: not supported
        return ExprBase(expr=f"~{name}")

    @staticmethod
    def NOT(name):
        return ExprBase(expr=f"!{name}")

    @staticmethod
    def Not(name):
        return ExprBase(expr=f"not {name}")

    @staticmethod
    def In(left, right):
        return ExprBase(expr=f"{left} in {right}")

    @staticmethod
    def Nin(left, right):
        return ExprBase(expr=f"{left} not in {right}")

    @staticmethod
    def json_contains(left, right):
        return ExprBase(expr=f"json_contains({left}, {right})")

    @staticmethod
    def JSON_CONTAINS(left, right):
        return ExprBase(expr=f"JSON_CONTAINS({left}, {right})")

    @staticmethod
    def json_contains_all(left, right):
        return ExprBase(expr=f"json_contains_all({left}, {right})")

    @staticmethod
    def JSON_CONTAINS_ALL(left, right):
        return ExprBase(expr=f"JSON_CONTAINS_ALL({left}, {right})")

    @staticmethod
    def json_contains_any(left, right):
        return ExprBase(expr=f"json_contains_any({left}, {right})")

    @staticmethod
    def JSON_CONTAINS_ANY(left, right):
        return ExprBase(expr=f"JSON_CONTAINS_ANY({left}, {right})")

    @staticmethod
    def array_contains(left, right):
        return ExprBase(expr=f"array_contains({left}, {right})")

    @staticmethod
    def ARRAY_CONTAINS(left, right):
        return ExprBase(expr=f"ARRAY_CONTAINS({left}, {right})")

    @staticmethod
    def array_contains_all(left, right):
        return ExprBase(expr=f"array_contains_all({left}, {right})")

    @staticmethod
    def ARRAY_CONTAINS_ALL(left, right):
        return ExprBase(expr=f"ARRAY_CONTAINS_ALL({left}, {right})")

    @staticmethod
    def array_contains_any(left, right):
        return ExprBase(expr=f"array_contains_any({left}, {right})")

    @staticmethod
    def ARRAY_CONTAINS_ANY(left, right):
        return ExprBase(expr=f"ARRAY_CONTAINS_ANY({left}, {right})")

    @staticmethod
    def array_length(name):
        return ExprBase(expr=f"array_length({name})")

    @staticmethod
    def ARRAY_LENGTH(name):
        return ExprBase(expr=f"ARRAY_LENGTH({name})")


"""" Define pass in params """


@dataclass
class BasePrams:
    @property
    def to_dict(self):
        return {k: v for k, v in vars(self).items() if v is not None}


@dataclass
class FieldParams(BasePrams):
    description: str = None

    # varchar
    max_length: int = None

    # array
    max_capacity: int = None

    # for vector
    dim: int = None

    # scalar
    is_primary: bool = None
    # auto_id: bool = None
    is_partition_key: bool = None
    is_clustering_key: bool = None
    nullable: bool = None

    # warmup (tiered storage)
    warmup: str = None

    # text match (varchar with analyzer)
    enable_analyzer: bool = None
    enable_match: bool = None


@dataclass
class IndexPrams(BasePrams):
    index_type: str = None
    params: dict = None
    metric_type: str = None

@dataclass
class SearchInsidePrams(BasePrams):
    # inside params
    radius: Optional[float] = None
    range_filter: Optional[float] = None
    group_by_field: Optional[str] = None

@dataclass
class SearchPrams(BasePrams):
    metric_type: str = MetricType.L2
    params: dict = None


""" Define default params """


class DefaultVectorIndexParams:

    @staticmethod
    def FLAT(field: str, metric_type=MetricType.L2):
        return {field: IndexPrams(index_type=IndexName.FLAT, params={}, metric_type=metric_type)}

    @staticmethod
    def IVF_FLAT(field: str, nlist: int = 1024, metric_type=MetricType.L2):
        return {
            field: IndexPrams(index_type=IndexName.IVF_FLAT, params={"nlist": nlist}, metric_type=metric_type)
        }

    @staticmethod
    def IVF_PQ(field: str, nlist: int = 1024, m: int = 8, nbits: int = 8, metric_type=MetricType.L2):
        return {
            field: IndexPrams(index_type=IndexName.IVF_PQ, params={"nlist": nlist, "m": m, "nbits": nbits},
                              metric_type=metric_type)
        }

    @staticmethod
    def IVF_SQ8(field: str, nlist: int = 1024, metric_type=MetricType.L2):
        return {
            field: IndexPrams(index_type=IndexName.IVF_SQ8, params={"nlist": nlist}, metric_type=metric_type)
        }

    @staticmethod
    def HNSW(field: str, m: int = 8, efConstruction: int = 200, metric_type=MetricType.L2):
        return {
            field: IndexPrams(index_type=IndexName.HNSW, params={"M": m, "efConstruction": efConstruction}, metric_type=metric_type)
        }

    @staticmethod
    def SCANN(field: str, nlist: int = 128, metric_type=MetricType.L2):
        return {
            field: IndexPrams(index_type=IndexName.SCANN, params={"nlist": nlist}, metric_type=metric_type)
        }

    @staticmethod
    def DISKANN(field: str, metric_type=MetricType.L2):
        return {field: IndexPrams(index_type=IndexName.DISKANN, params={}, metric_type=metric_type)}

    @staticmethod
    def BIN_FLAT(field: str, nlist: int = 1024, metric_type=MetricType.JACCARD):
        return {
            field: IndexPrams(index_type=IndexName.BIN_FLAT, params={"nlist": nlist}, metric_type=metric_type)
        }

    @staticmethod
    def BIN_IVF_FLAT(field: str, nlist: int = 1024, metric_type=MetricType.JACCARD):
        return {
            field: IndexPrams(index_type=IndexName.BIN_IVF_FLAT, params={"nlist": nlist},
                              metric_type=metric_type)
        }

    @staticmethod
    def SPARSE_WAND(field: str, drop_ratio_build: float = 0.2, metric_type=MetricType.IP):
        return {
            field: IndexPrams(index_type=IndexName.SPARSE_WAND, params={"drop_ratio_build": drop_ratio_build},
                              metric_type=metric_type)
        }

    @staticmethod
    def SPARSE_INVERTED_INDEX(field: str, drop_ratio_build: float = 0.2, metric_type=MetricType.IP):
        return {
            field: IndexPrams(index_type=IndexName.SPARSE_INVERTED_INDEX, params={"drop_ratio_build": drop_ratio_build},
                              metric_type=metric_type)
        }


class DefaultIndexSearchParams:
    @staticmethod
    def FLAT(**kwargs):
        metric_type = kwargs.get("metric_type", MetricType.L2)
        return {
            "metric_type": metric_type,
            "params": {}
        }

    @staticmethod
    def IVF_FLAT(**kwargs):
        """
        nprobe: [1, nlist]
        """
        metric_type = kwargs.get("metric_type", MetricType.L2)
        nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
        return {
            "metric_type": metric_type,
            "params": {"nprobe": nprobe}
        }

    @staticmethod
    def IVF_PQ(**kwargs):
        """
        nprobe: [1, nlist]
        """
        metric_type = kwargs.get("metric_type", MetricType.L2)
        nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
        return {
            "metric_type": metric_type,
            "params": {"nprobe": nprobe}
        }

    @staticmethod
    def IVF_SQ8(**kwargs):
        """
        nprobe: [1, nlist]
        """
        metric_type = kwargs.get("metric_type", MetricType.L2)
        nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
        return {
            "metric_type": metric_type,
            "params": {"nprobe": nprobe}
        }

    @staticmethod
    def HNSW(**kwargs):
        """
        ef: [top_k, int_max]
        """
        metric_type = kwargs.get("metric_type", MetricType.L2)
        limit = kwargs.get("limit", 64)
        ef = max(limit, 128)
        return {
            "metric_type": metric_type,
            "params": {"ef": ef}
        }

    @staticmethod
    def SCANN(**kwargs):
        """
        nprobe: [1, nlist]
        reorder_k: [top_k, ∞]
        """
        metric_type = kwargs.get("metric_type", MetricType.L2)
        nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
        limit = kwargs.get("limit", 64)
        reorder_k = max(limit, 128)
        return {
            "metric_type": metric_type,
            "params": {"nprobe": nprobe, "reorder_k": reorder_k}
        }

    @staticmethod
    def DISKANN(**kwargs):
        """
        search_list: [top_k, int_max]
        """
        metric_type = kwargs.get("metric_type", MetricType.L2)
        limit = kwargs.get("limit", 64)
        search_list = max(limit, 128)
        return {
            "metric_type": metric_type,
            "params": {"search_list": search_list}
        }

    @staticmethod
    def BIN_FLAT(**kwargs):
        metric_type = kwargs.get("metric_type", MetricType.JACCARD)
        return {
            "metric_type": metric_type,
            "params": {}
        }

    @staticmethod
    def BIN_IVF_FLAT(**kwargs):
        """
        nprobe: [1, nlist]
        """
        metric_type = kwargs.get("metric_type", MetricType.JACCARD)
        nprobe = max(1, int(kwargs.get("nlist", 256)) // 8)
        return {
            "metric_type": metric_type,
            "params": {"nprobe": nprobe}
        }

    @staticmethod
    def SPARSE_WAND(**kwargs):
        """
        drop_ratio_search: [0.0, 1.0]
        """
        metric_type = kwargs.get("metric_type", MetricType.IP)
        drop_ratio_search = kwargs.get("drop_ratio_build", 0.2)
        return {
            "metric_type": metric_type,
            "params": {"drop_ratio_search": drop_ratio_search}
        }

    @staticmethod
    def SPARSE_INVERTED_INDEX(**kwargs):
        """
        drop_ratio_search: [0.0, 1.0]
        """
        metric_type = kwargs.get("metric_type", MetricType.IP)
        drop_ratio_search = kwargs.get("drop_ratio_build", 0.2)
        return {
            "metric_type": metric_type,
            "params": {"drop_ratio_search": drop_ratio_search}
        }


class DefaultScalarIndexParams:

    @staticmethod
    def Default(field: str):
        return {field: IndexPrams()}

    @staticmethod
    def list_default(fields: List[str]) -> Dict[str, IndexPrams]:
        return {n: IndexPrams() for n in fields}

    @staticmethod
    def Trie(field: str):
        return {field: IndexPrams(index_type=IndexName.Trie)}

    @staticmethod
    def STL_SORT(field: str):
        return {field: IndexPrams(index_type=IndexName.STL_SORT)}

    @staticmethod
    def INVERTED(field: str):
        return {field: IndexPrams(index_type=IndexName.INVERTED)}

    @staticmethod
    def list_inverted(fields: List[str]) -> Dict[str, IndexPrams]:
        return {n: IndexPrams(index_type=IndexName.INVERTED) for n in fields}

    @staticmethod
    def BITMAP(field: str):
        return {field: IndexPrams(index_type=IndexName.BITMAP)}

    @staticmethod
    def list_bitmap(fields: List[str]) -> Dict[str, IndexPrams]:
        return {n: IndexPrams(index_type=IndexName.BITMAP) for n in fields}


class AlterIndexParams:

    @staticmethod
    def index_offset_cache(enable: bool = True):
        return {'indexoffsetcache.enabled': enable}

    @staticmethod
    def index_mmap(enable: bool = True):
        return {'mmap.enabled': enable}

class DefaultVectorSearchParams:

    @staticmethod
    def FLAT(metric_type=MetricType.L2, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def IVF_FLAT(metric_type=MetricType.L2, nprobe: int = 32, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"nprobe": nprobe}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def IVF_PQ(metric_type=MetricType.L2, nprobe: int = 16, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"nprobe": nprobe}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def IVF_SQ8(metric_type=MetricType.L2, nprobe: int = 32, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"nprobe": nprobe}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def HNSW(metric_type=MetricType.L2, ef: int = 200, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"ef": ef}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def SCANN(metric_type=MetricType.L2, nprobe: int = 32, reorder_k: int = 200, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"nprobe": nprobe, "reorder_k": reorder_k}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def DISKANN(metric_type=MetricType.L2, search_list: int = 30, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"search_list": search_list}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def BIN_FLAT(metric_type=MetricType.JACCARD, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def BIN_IVF_FLAT(metric_type=MetricType.JACCARD, nprobe: int = 32, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"nprobe": nprobe}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def SPARSE_WAND(metric_type=MetricType.IP, drop_ratio_search: float = 0.2, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"drop_ratio_search": drop_ratio_search}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

    @staticmethod
    def SPARSE_INVERTED_INDEX(metric_type=MetricType.IP, drop_ratio_search: float = 0.2, inside_params: SearchInsidePrams = None, **kwargs):
        inside_params_dict = {"drop_ratio_search": drop_ratio_search}
        if inside_params is not None:
            inside_params_dict.update(inside_params.to_dict)

        sp = SearchPrams(params=inside_params_dict, metric_type=metric_type).to_dict
        sp.update(kwargs)
        return sp

@dataclass
class ExprCheckParams:
    field: str
    field_expr: str
    rex: str
