from copy import copy
from dataclasses import Field, asdict, dataclass
from enum import Enum
from typing import Any, ClassVar, Protocol, assert_never

from sqlalchemy import Select, and_, or_


class FilterSign(Enum):
    NOT_EQ = "not_eq"
    EQ = "eq"
    LT = "lt"
    LE = "le"
    GT = "gt"
    GE = "ge"
    IS = "is"
    IS_NOT = "is_not"
    OR = "or"
    AND = "and"
    IN = "in"


class FilterLeftField(Protocol):
    name: str


@dataclass(frozen=True)
class Filter:
    right: Any
    sign: FilterSign
    left: FilterLeftField | None = None

    @staticmethod
    def not_eq(f1: Any, f2: Any):
        return Filter(left=f1, right=f2, sign=FilterSign.NOT_EQ)

    @staticmethod
    def eq(f1: Any, f2: Any):
        return Filter(left=f1, right=f2, sign=FilterSign.EQ)

    @staticmethod
    def lt(f1: Any, f2: Any):
        return Filter(left=f1, right=f2, sign=FilterSign.LT)

    @staticmethod
    def le(f1: Any, f2: Any):
        return Filter(left=f1, right=f2, sign=FilterSign.LE)

    @staticmethod
    def gt(f1: Any, f2: Any):
        return Filter(left=f1, right=f2, sign=FilterSign.GT)

    @staticmethod
    def ge(f1: Any, f2: Any):
        return Filter(left=f1, right=f2, sign=FilterSign.GE)

    @staticmethod
    def is_none(f1: Any):
        return Filter(left=f1, right=None, sign=FilterSign.IS_NOT)

    @staticmethod
    def is_not_none(f1: Any):
        return Filter(left=f1, right=None, sign=FilterSign.IS)

    @staticmethod
    def or_(f1: list["Filter"]):
        return Filter(left=None, right=f1, sign=FilterSign.OR)

    @staticmethod
    def in_(f1: Any, f2: list[Any]):
        return Filter(left=f1, right=f2, sign=FilterSign.IN)


@dataclass
class RestrictionField:
    field: Any
    direction: str


@dataclass
class QueryRestriction:
    filters: list[Filter] | None = None
    limit: int | None = None
    offset: int | None = None
    sort: list[RestrictionField] | None = None


@dataclass(frozen=False)
class FilterQuery:
    filters: list[Filter]
    limit: int | None = None
    offset: int | None = None
    sort: list[RestrictionField] | None = None

    @staticmethod
    def mass_and(fields: list[object], values: list[Any]) -> "FilterQuery":
        return FilterQuery(filters=[Filter.eq(field, val) for field, val in zip(fields, values, strict=True)])

    @staticmethod
    def mass_or(fields: list[object], values: list[Any]) -> "FilterQuery":
        return FilterQuery(
            filters=[Filter.or_([Filter.eq(field, val) for field, val in zip(fields, values, strict=True)])]
        )

    @staticmethod
    def eq(field: object, value: Any) -> "FilterQuery":
        return FilterQuery(filters=[Filter.eq(field, value)])

    @staticmethod
    def in_(field: object, value: list[Any]) -> "FilterQuery":
        return FilterQuery(filters=[Filter.in_(field, value)])

    def add_and(self, field: object, value: Any):
        self.filters.append(Filter.eq(field, value))

    def add_query_restistions(self, q_restriction: QueryRestriction | None = None):
        if not q_restriction:
            return None
        if q_restriction.limit:
            self.limit = q_restriction.limit
        if q_restriction.offset:
            self.offset = q_restriction.offset
        if q_restriction.sort:
            self.sort = q_restriction.sort
        if q_restriction.filters:
            self.filters += q_restriction.filters
        return None


class DataclassInstance(Protocol):
    __dataclass_fields__: ClassVar[dict[str, Field[Any]]]


async def indexes_by_id(input_data: list, values: list[str], id_name="id") -> list[int] | None:
    r_data: list[int] = []
    for i, _ in enumerate(input_data):
        if getattr(input_data[i], id_name) in values:
            r_data.append(i)
    if not r_data:
        return None
    return r_data


def data_by_filter[T: DataclassInstance](input_data: list[T], q: FilterQuery) -> list[T]:
    # can't do query AND(OR() + AND())
    data: list[T] = []
    data_or: list[T] = []
    data_and: list[T] = []
    is_found = False
    for d in input_data:
        dict_class = asdict(d)
        for f in q.filters:
            if f.sign == FilterSign.OR:
                for f_or in f.right:
                    data_or += data_by_filter(input_data, q=FilterQuery(filters=[f_or]))
                continue
            if f.sign == FilterSign.AND:
                data_and += data_by_filter(input_data, q=FilterQuery(filters=f.right))
                continue
            if f.left is None:
                continue
            if f.sign != FilterSign.IS and dict_class.get(f.left.name) is None:
                break
            if f.sign != FilterSign.IS_NOT and dict_class.get(f.left.name) is None:
                break
            match f.sign:
                case FilterSign.NOT_EQ:
                    is_found = dict_class.get(f.left.name) != f.right
                    if is_found:
                        continue
                    break
                case FilterSign.EQ:
                    is_found = dict_class.get(f.left.name) == f.right
                    if is_found:
                        continue
                    break
                case FilterSign.LT:
                    is_found = dict_class.get(f.left.name) < f.right
                    if is_found:
                        continue
                    break
                case FilterSign.LE:
                    is_found = dict_class.get(f.left.name) <= f.right
                    if is_found:
                        continue
                    break
                case FilterSign.GT:
                    is_found = dict_class.get(f.left.name) > f.right
                    if is_found:
                        continue
                    break
                case FilterSign.GE:
                    is_found = dict_class.get(f.left.name) >= f.right
                    if is_found:
                        continue
                    break
                case FilterSign.IS:
                    is_found = dict_class.get(f.left.name) is None
                    if is_found:
                        continue
                    break
                case FilterSign.IS_NOT:
                    is_found = dict_class.get(f.left.name) is not None
                    if is_found:
                        continue
                    break
                case FilterSign.IN:
                    is_found = dict_class.get(f.left.name) in f.right
                    if is_found:
                        continue
                    break
                case _ as arg:
                    assert_never(arg)
        if is_found:
            data.append(copy(d))
    if q.limit:
        limit = q.limit
        if limit > len(data) - 1:
            limit = len(data) - 1
        return data[:limit]
    if data_and:
        if not data:
            data = data_and
        else:
            data = list(set(data) & set(data_and))
    if data_or:
        if not data:
            data = data_or
        else:
            data = list(set(data) & set(data_or))
    return data


def sqlalchemy_conditions(q: FilterQuery):
    conditions = []
    for f in q.filters:
        if f.sign == FilterSign.OR:
            conditions.append(or_(*sqlalchemy_conditions(q=FilterQuery(filters=f.right))))
            continue
        if f.sign == FilterSign.AND:
            conditions.append(and_(*sqlalchemy_conditions(q=FilterQuery(filters=f.right))))
            continue
        if f.left is None:
            continue
        match f.sign:
            case FilterSign.NOT_EQ:
                conditions.append(f.left != f.right)
            case FilterSign.EQ:
                conditions.append(f.left == f.right)
            case FilterSign.LT:
                conditions.append(f.left < f.right)
            case FilterSign.LE:
                if f.left is None:
                    continue
                conditions.append(f.left <= f.right)
            case FilterSign.GT:
                conditions.append(f.left > f.right)
            case FilterSign.GE:
                conditions.append(f.left >= f.right)
            case FilterSign.IS:
                conditions.append(f.left.is_(None))  # type: ignore
            case FilterSign.IS_NOT:
                conditions.append(f.left.is_not(None))  # type: ignore
            case FilterSign.IN:
                conditions.append(f.left.in_(f.right))  # type: ignore
            case _ as arg:
                assert_never(arg)
    return conditions


def sqlalchemy_restrictions(f: FilterQuery, q: Select, dict_to_sort: dict | None = None) -> Select:
    if f.limit:
        q = q.limit(f.limit)
    if f.offset:
        q = q.offset(f.offset)
    if f.sort:
        for s in f.sort:
            field = s.field
            if dict_to_sort and str(s.field) in dict_to_sort:
                if s.direction == "desc":
                    q = q.order_by(dict_to_sort[str(s.field)].desc())  # type: ignore
                    continue
                return q.order_by(dict_to_sort[str(s.field)])  # type: ignore
            if s.direction == "desc":
                field = s.field.desc()  # type: ignore
            q = q.order_by(field)  # type: ignore
    return q
