"""
Objects used to specify tables and subset of tables. The main class is the `Table`, representing a table of a given dataclass.
This table can be narrowed by using `.where(condition)`. A condition can be created from `Column(X)` using the operators `>, >=, <, <=, ==, !=`.
For example, the following specifies all rows of a Table X where the value y is in the range (0, 10) inclusive:

    Table(X).where((Column("y") >= 0) and (Column(y) <= 10))
"""

import typing
from typing import Union, Any, List, Literal, TypeVar, Generic, Optional, Type
import dataclasses

from hoernchen.column_types import COLUMN_TYPES, ColumnType
from hoernchen.typing import Q, JoinHow, Operator, Order, V, StoreID, T

from datetime import datetime
import decimal


def is_optional(field: dataclasses.Field) -> bool:
    for type in typing.get_args(field.type):
        if type is None:
            return True
    return False


def is_unique(field: dataclasses.Field) -> bool:
    return field.metadata.get("store.type", None) == "unique"


def get_type_from_optional(field: dataclasses.Field) -> Any:
    types = typing.get_args(field.type)
    if len(types) > 2:
        raise ValueError("Union fields are not supported (besides Optional)")
    t1, t2 = types
    if t1 is None:
        return t2
    return t1


class UniqueConstraint:

    def __init__(self, columns):
        self.columns = columns


class Table(Generic[T]):
    """
    Represents a single table in SQLite
    """

    type: Type[T]
    _name: str
    columns: List["Column"]
    _constraints: List[UniqueConstraint]

    def __init__(self, dc_type: Type[T], columns=None, constraints=None):
        self.type = dc_type
        self._name = dc_type.__name__
        self.columns = columns if columns is not None else []
        self._constraints = constraints if constraints is not None else []

    @property
    def name(self):
        return self._name

    @staticmethod
    def from_dataclass_type(dc_type: Type[T]) -> "Table":
        tablename = dc_type.__name__
        columns = []
        constraints = []

        fields_for_unique = []

        for field in dataclasses.fields(dc_type):
            optional = is_optional(field)

            if optional:
                field_type = get_type_from_optional(field)
            else:
                field_type = field.type

            if is_unique(field):
                fields_for_unique.append(field.name)

            column_type = COLUMN_TYPES.get(field_type)

            if column_type is None:
                raise KeyError(f"Type {field_type} is not supported")

            columns.append(Column(field.name, column_type, optional))
        if len(fields_for_unique) > 0:
            constraints.append(UniqueConstraint(fields_for_unique))
        return Table(dc_type, columns, constraints)

    def _to_sql_queries(self):
        queries = []
        create = "CREATE TABLE IF NOT EXISTS"
        tablename = self._name

        columns = ", ".join([column._as_sql() for column in self.columns])

        queries.append(f"{create} {tablename}({columns});")

        for constraint in self._constraints:
            column_names = constraint.columns
            index_name = f"{tablename}_unique_{'_'.join(constraint.columns)}"
            queries.append(f"CREATE UNIQUE INDEX IF NOT EXISTS {index_name} ON {tablename}({', '.join(constraint.columns)});")
        return queries

    def join(self, other: "Table", on: Union["Column", str], how: JoinHow = "inner") -> "Join":
        """Join table with another table

        Args:
            other: table to join.
            on: column to join on.
            how: How the join is evaluated. Only a subset of join methods is supported.

        Returns:
            Join object representing the joined tables
        """
        return Join(self, other, on, on, how)

    def where(self, condition: "Condition") -> "ConditionedTable":
        """Specify a subset of a table

        By default a table specifies every row in that table. With `where`, a condition can be applied, to
        only specify a subset of the table, where the condition evaluates to true.

        A `Condition` is generally created from a `Column`.

        Args:
            condition: Condition that specifies the subset of the table.

        Returns:
            Subset of a table with the condition
        """
        return ConditionedTable(self, condition)

    def order_by(self, col: "Column", order: Order = "asc") -> "OrderedTable":
        """Order table by a column in ascending or descending order

        Args:
            col: Column to order by.
            order: whether in ascending or descending order.

        Returns:
            table with order specified.
        """
        return OrderedTable(self, col, order)


class Column(Generic[V]):
    """Represents a single column of a table. Generally used to specify conditions"""

    name: str
    type: Optional[ColumnType[V]]
    optional: Optional[bool]

    def __init__(self, name: str, type: Optional[ColumnType[V]]=None, optional: Optional[bool]= None):
        self.name = name
        self.type = type
        self.optional = optional

    def __lt__(self, other: V | "Column[V]") -> "Condition":
        return Condition(self, other, "<")

    def __le__(self, other: V | "Column[V]") -> "Condition":
        return Condition(self, other, "<=")

    def __eq__(self, other: V | "Column[V]") -> "Condition":  # type: ignore
        return Condition(self, other, "=")

    def __ne__(self, other: V | "Column[V]") -> "Condition":  # type: ignore
        return Condition(self, other, "!=")

    def __ge__(self, other: V | "Column[V]") -> "Condition":
        return Condition(self, other, ">=")

    def __gt__(self, other: V | "Column[V]") -> "Condition":
        return Condition(self, other, ">")

    def __repr__(self):
        s = ["Column(\"", self.name, "\""]
        if self._type is not None:
            s.append(", ")
            s.append(self._type.sqlite_type)
        s.append(")")
        return "".join(s)

    def _as_sql(self):
        l = [self.name, " ", self.type.sqlite_type]
        if self.optional:
            l.append(" NOT NULL")
        return "".join(l)


class Condition:
    """Represents a condition to specify a subset of a table

    Conditions can be combined by using `and`, `or` or inverted by `~`.
    """

    c1: Column
    c2: Union[Any, Column]

    def __init__(self, c1: Column, c2: Union[Any, Column], operator: Operator):
        self.c1 = c1
        self.c2 = c2
        self.operator = operator

    def __and__(self, other: "Condition") -> "AndCondition":
        return AndCondition([self, other])

    def __or__(self, other: "Condition") -> "OrCondition":
        return OrCondition([self, other])

    def __invert__(self) -> "Condition":
        return ICondition(self)

    def __str__(self) -> str:
        return f"Condition({self.c1} {self.operator} {self.c2})"


class AndCondition(Condition):
    """Represents `cond1 AND cond2 AND ... condN`"""

    cs: List[Condition]

    def __init__(self, cs: List[Condition]):
        self.cs = cs

    def __and__(self, other: Condition) -> "AndCondition":
        return AndCondition(self.cs + [other])


class OrCondition(Condition):
    """Represents `cond1 OR cond2 OR ... OR condN`"""

    cs: List[Condition]

    def __init__(self, cs: List[Condition]):
        self.cs = cs

    def __or__(self, other: Condition) -> "OrCondition":
        return OrCondition(self.cs + [other])


class ICondition(Condition):
    """Represents `NOT cond`"""

    c: Condition

    def __init__(self, c: Condition):
        self.c = c

    def __invert__(self) -> Condition:
        return self.c


class Join(Table):
    """Represents `table1 JOIN table2 ON on1 = on2`"""

    t1: Table
    t2: Table
    on1: Union[Column, str]
    on2: Union[Column, str]
    how: JoinHow

    def __init__(self, t1: Table, t2: Table, on1: Union[Column, str], on2: Union[Column, str], how: JoinHow) -> None:
        self.t1 = t1
        self.t2 = t2
        self.on1 = on1
        self.on2 = on2
        self.how = how

    @property
    def name(self) -> str:
        return self.t2.name

    @property
    def inner_name(self) -> str:
        return self.t1.name


class ConditionedTable(Table):
    """Represents `table WHERE condition`"""

    table: Table
    condition: Condition

    def __init__(self, table: Table, condition: Condition):
        self.table = table
        self.condition = condition

    @property
    def name(self) -> str:
        return self.table.name

    def where(self, condition: Condition) -> "ConditionedTable":
        return ConditionedTable(self.table, self.condition & condition)


class OrderedTable(Table):
    """Represents `table ORDERED BY column`"""

    table: Table
    column: Column
    order: Order

    def __init__(self, table: Table, column: Column, order: Order):
        self.table = table
        self.column = column
        self.order = order

    @property
    def name(self) -> str:
        return self.table.name
