from typing import List, Any, Dict, Tuple
from motorized.keywords import Eq, Neq, In, Nin, Gte, Lte, Gt, Lt, Exists, Regex
from motorized.utils import merge_values, dict_deep_update


KEYWORDS = {
    "eq": Eq,
    "neq": Neq,
    "in": In,
    "nin": Nin,
    "gte": Gte,
    "ge": Gte,
    "lte": Lte,
    "le": Lte,
    "gt": Gt,
    "lt": Lt,
    "exists": Exists,
    "regex": Regex,
}


def dict_path(path: List[str], value: Any = None) -> Dict:
    """Construct a dictionary from a path to hold the given value,
    example:
    d = QuerySet.dict_path(['a', 'b', 'c'], 42)
    d == {'a': {'b': {'c': 42}}}
    """
    out = {}
    node = out
    last_node = None
    last_key = None
    for k in path:
        node[k] = {}
        last_node = node
        node = node[k]
        last_key = k
    last_node[last_key] = value
    return out


class QueryDict(dict):
    def __init__(self, **kwargs) -> None:
        data = {}
        for key, value in kwargs.items():
            path = key.split("__")
            node = dict_path(path, value)
            dict_deep_update(data, node, on_conflict=merge_values)
        super().__init__(**data)


class Q:
    def __init__(self, **kwargs) -> None:
        self.query = self.convert_kwargs_to_query(**kwargs)

    def __repr__(self) -> str:
        return f"<Q: {self.query}>"

    def copy(self) -> "Q":
        instance = Q()
        instance.query = self.query.copy()
        return instance

    @classmethod
    def raw(cls, query: Dict) -> "Q":
        if not isinstance(query, dict):
            raise TypeError(f"A dictionary was expected, got {type(query)}")
        instance = cls()
        instance.query = query
        return instance

    @classmethod
    def convert_kwargs_to_query(cls, invert=False, **kwargs) -> Dict:
        query = {}
        for key, value in kwargs.items():
            path, value = cls.apply_keywords(value, key.split("__"), invert=invert)
            filter_dict = dict_path(path, value)
            dict_deep_update(query, filter_dict, on_conflict=merge_values)
        return query

    @staticmethod
    def read_dict_path(data: dict, path: List["str"]) -> Any:
        x = data
        for node in path:
            x = x[node]
        return x

    @classmethod
    def apply_keywords(
        cls, raw_value: Any, path: List[str], invert: bool = False
    ) -> Tuple[str, Any]:
        if invert and path[-1] not in KEYWORDS:
            path.append("eq")

        for cmd in path:
            try:
                op = KEYWORDS[cmd](raw_value)
                return path[0:-1], op.as_mongo_expression(invert)
            except KeyError:
                pass
        return path, raw_value

    def __add__(self, other: "Q") -> "Q":
        instance = self.copy()
        dict_deep_update(instance.query, other.query, on_conflict=merge_values)
        return instance

    def is_empty(self) -> bool:
        return not self.query

    def __eq__(self, other: "Q") -> bool:
        return self.query == other.query

    def __or__(self, other: "Q") -> "Q":
        return Q.raw({"$or": [self.query, other.query]})

    def __and__(self, other: "Q") -> "Q":
        return Q.raw({"$and": [self.query, other.query]})
