import sqlite3
import dataclasses
from datetime import datetime
import decimal

import typing
from typing import Set, Type, Any, TypeVar, List, Union, Dict, NoReturn, Optional, TYPE_CHECKING, Protocol, ClassVar
from types import TracebackType

from hoernchen.language import Column, Table, Condition, AndCondition, OrCondition, ICondition, Join, ConditionedTable, OrderedTable, JoinHow
from hoernchen.typing import Q, JoinHow, Operator, Order, StoreID, V, T
from hoernchen.column_types import COLUMN_TYPES, SQLITE_TYPES, DefaultType


def assert_never(value: NoReturn) -> NoReturn:
    assert False, f"Unhandled value: {value} ({type(value).__name__})"


def _sqlite_to_decimal(s: bytes) -> decimal.Decimal:
    return decimal.Decimal(s.decode("utf8"))


def _decimal_to_sqlite(d: decimal.Decimal) -> bytes:
    return str(d).encode("utf8")


class Store:

    def __init__(self, path: str):
        self._path = path
        self._tables: Set[str] = set()
        self._types: Set[type] = set()

        self._initialized = False
        self._in_transaction = 0

    def __enter__(self) -> "Store":
        self._begin()
        return self

    def __exit__(self, exc_type: Optional[type[BaseException]], exc_value: Optional[BaseException], exc_traceback: Optional[TracebackType]) -> None:
        if self._in_transaction:
            if exc_type is None:
                self._commit()
            else:
                self._rollback()

    def _register_dataclass(self, dc_type: Type[T]) -> None:
        self._types.add(dc_type)

    def _get_primary(self, dc_type: Type[T]) -> str:
        for field in dataclasses.fields(dc_type):
            if field.type == StoreID:
                return field.name
            if field.metadata.get("store.type", None) == "primary":
                return field.name
        return "rowid"

    def _create_table_queries(self, dc_type: Type[T]) -> list[str]:
        queries = Table.from_dataclass_type(dc_type)._to_sql_queries()
        return queries

    def _begin(self) -> None:
        if not self._initialized:
            self.create_db()
        if self._in_transaction == 0:
            self._connection.execute("BEGIN TRANSACTION;")
        self._in_transaction += 1

    def _commit(self) -> None:
        if not self._initialized:
            raise Exception("store._commit() called with uninitialized store")
        self._in_transaction -= 1
        if self._in_transaction == 0:
            self._connection.execute("COMMIT;")

    def _rollback(self) -> None:
        if not self._initialized:
            raise Exception("store.__rollback() called with uninitialized store")
        self._connection.execute("ROLLBACK;")
        self._in_transaction = 0

    def create_db(self) -> None:
        for column_type in COLUMN_TYPES.values():
            if isinstance(column_type, DefaultType):
                continue
            sqlite3.register_converter(column_type.sqlite_type, column_type.to_python)
            sqlite3.register_adapter(column_type.handles, column_type.to_sqlite)
        self._connection = sqlite3.connect(self._path, detect_types=sqlite3.PARSE_DECLTYPES, isolation_level=None)
        cur = self._connection.cursor()
        cur.execute("pragma journal_mode = WAL;")
        cur.execute("pragma synchronous = normal;")
        cur.execute("pragma temp_store = memory;")
        cur.execute("pragma mmap_size = 30000000000;")
        # cur.execute("pragma page_size = 32768;")
        self._initialized = True

        with self:
            for dc_type in self._types:
                for q in self._create_table_queries(dc_type):
                    cur.execute(q)
            for dc_type in self._types:
                self._tables.add(dc_type.__name__)

    def create_table(self, dc_type: Type[T]) -> None:
        qs = self._create_table_queries(dc_type)
        cur = self._connection.cursor()
        with self:
            for q in qs:
                cur.execute(q)
        self._tables.add(dc_type.__name__)

    def insert(self, dc: T | List[T], update: bool = False) -> None:
        if not isinstance(dc, list):
            dc = [dc]
        dc0 = dc[0]
        dc_type = type(dc0)
        tablename = dc_type.__name__

        if not self._initialized:
            self.create_db()

        if tablename not in self._tables:
            self.create_table(dc_type)

        aliases = [field.name for field in dataclasses.fields(dc_type)]
        placeholders = ["?" for field in dataclasses.fields(dc_type)]

        q = f"INSERT INTO {tablename} ({', '.join(aliases)}) VALUES ({', '.join(placeholders)})"

        if update:
            num_fillers = 1
            upsert_clause_l_fields = []
            for field in dataclasses.fields(dc_type):
                upsert_clause_l_fields.append(f"{field.name}=?")
            set_data_clause = ", ".join(upsert_clause_l_fields)

            primary = self._get_primary(dc_type)
            if primary != "rowid":
                q += f" ON CONFLICT({primary}) DO UPDATE SET {set_data_clause} "
                num_fillers += 1

            for constraint in Table.from_dataclass_type(dc_type)._constraints:
                q += f" ON CONFLICT({', '.join(constraint.columns)}) DO UPDATE SET {set_data_clause} "
                num_fillers += 1

            fillers = []
            for instance in dc:
                tup = dataclasses.astuple(instance)
                fillers.append(tuple([value for _ in range(num_fillers) for value in tup]))
        else:
            fillers = [dataclasses.astuple(instance) for instance in dc]

        q += ";"

        cur = self._connection.cursor()
        with self:
            cur.executemany(q, fillers)

    def get_single(self, dc_type: Type[T], **kwargs: Any) -> Optional[T]:
        ts = self.get(dc_type, **kwargs)
        if len(ts) == 1:
            return ts[0]
        elif len(ts) == 0:
            return None
        else:
            raise KeyError("Multiple values for query")

    def get_where(self, dc_type: Type[T], kwargs: Any = {}, order_by: Optional[str] = None, reverse: bool = False, limit: Optional[int] = None) -> List[T]:
        fields = [field.name for field in dataclasses.fields(dc_type)]
        if (order_by is not None) and (order_by not in fields):
            raise ValueError(f'OrderBy parameter must be an existing column "{fields}" but is "{order_by}".')

        tablename = dc_type.__name__

        if not self._initialized:
            self.create_db()

        if tablename not in self._tables:
            self.create_table(dc_type)

        where_clause = " and ".join([f"{key} = ?" for key in kwargs])
        if where_clause:
            where_clause = "WHERE " + where_clause
        else:
            where_clause = ""
        select_clause = f"SELECT {', '.join(fields)} from {tablename} "

        order_clause = ""
        if order_by:
            order = "DESC" if reverse else "ASC"
            order_clause = f"ORDER BY {order_by} {order}"

        limit_clause = ""
        if limit:
            assert isinstance(limit, int)
            limit_clause = f"LIMIT {limit}"

        q = " ".join([select_clause, where_clause, order_clause, limit_clause, ";"])

        cur = self._connection.cursor()
        parameters = [value for value in kwargs.values()]
        cur.execute(q, tuple(parameters))
        rows = cur.fetchall()

        result = [dc_type(*args) for args in rows]
        return result

    def get(self, dc_type: Type[T], **kwargs: Any) -> List[T]:
        return self.get_where(dc_type, kwargs)

    def _get_table_postfix(self, table_counter: Dict[str, int], name: str, increment: bool = True) -> str:
        table_count = table_counter.get(name, 0)
        if increment:
            table_counter[name] = table_count + 1
        if table_count == 0:
            table_postfix = ""
        else:
            table_postfix = str(table_count)
        return table_postfix

    def _get_value_as_str(self, tableid: str, value: Any, param_container: List[Any]) -> str:
        if isinstance(value, Column):
            return f"{tableid}.{value.name}"
        else:
            param_container.append(value)
            return "?"

    def _get_cond_str(self, tableid: str, cond: Condition, param_container: List[Union[str, int]]) -> str:
        if isinstance(cond, OrCondition):
            return "(" + " or ".join([self._get_cond_str(tableid, c, param_container) for c in cond.cs]) + ")"
        elif isinstance(cond, AndCondition):
            return "(" + " and ".join([self._get_cond_str(tableid, c, param_container) for c in cond.cs]) + ")"
        elif isinstance(cond, ICondition):
            return "NOT " + self._get_cond_str(tableid, cond.c, param_container)
        elif isinstance(cond, Condition):
            left = self._get_value_as_str(tableid, cond.c1, param_container)
            if cond.c2 is None:
                return f"{left} IS NULL"
            right = self._get_value_as_str(tableid, cond.c2, param_container)
            return f"{left} {cond.operator} {right}"
        else:
            assert_never(cond)

    def select(self, query: Union[Table, ConditionedTable, OrderedTable, Join], limit: Optional[int] = None) -> List[Q]:
        table_clauses: List[str] = []
        where_clauses: List[str] = []
        order_by = ""

        param_container: List[Union[str, int]] = []
        table_counter: Dict[str, int] = {}

        table: Optional[Table] = query

        while table is not None:
            if isinstance(table, OrderedTable):
                assert order_by == ""

                table_name = table.name
                table_postfix = self._get_table_postfix(table_counter, table_name, increment=False)
                table_id = table_name + table_postfix

                column = table.column.name if isinstance(table.column, Column) else table.column
                order_by = f"ORDER BY {table_id}.{column}" + (" ASC" if table.order == "asc" else " DESC")
                table = table.table
            elif isinstance(table, ConditionedTable):
                condition = table.condition
                table = table.table

                table_name = table.name
                table_postfix = self._get_table_postfix(table_counter, table_name, increment=False)
                table_id = table_name + table_postfix

                where_clauses.append(self._get_cond_str(table_id, condition, param_container))

            elif isinstance(table, Join):
                table_name = table.t2.name
                table_postfix = self._get_table_postfix(table_counter, table.t2.name)
                table_id = table_name + table_postfix

                inner_table_name = table.inner_name
                inner_table_postfix = self._get_table_postfix(table_counter, inner_table_name, increment=False)
                inner_table_id = inner_table_name + inner_table_postfix

                how: JoinHow = table.how
                if how == "inner":
                    join_op = "INNER JOIN"
                elif how == "left":
                    join_op = "LEFT JOIN"
                else:
                    assert_never(table.how)

                table_clauses.insert(0, f"{join_op} {table_name} {table_id} ON {inner_table_id}.{table.on1} = {table_id}.{table.on2}")

                table = table.t1
            elif isinstance(table, Table):
                dc_type = table.type

                tablename = dc_type.__name__

                if tablename not in self._tables:
                    self.create_table(dc_type)

                table_postfix = self._get_table_postfix(table_counter, tablename)
                table_id = tablename + table_postfix

                fields = [f"{table_id}.{field.name}" for field in dataclasses.fields(dc_type)]

                table_clauses.insert(0, f"SELECT {', '.join(fields)} from {tablename}{table_postfix}")
                table = None
            else:
                assert_never(table)

        select_clause = " ".join(table_clauses)
        where_clause = ("WHERE " + " and ".join(where_clauses)) if where_clauses else ""

        limit_clause = f"LIMIT {limit}" if limit else ""

        clauses = [select_clause, where_clause, order_by, limit_clause]

        q = " ".join([clause for clause in clauses if clause])

        cur = self._connection.cursor()
        cur.execute(q, param_container)
        rows = cur.fetchall()

        result = [dc_type(*args) for args in rows]
        return result

    def delete_where(self, dc_type: Type[T], kwargs: Any = {}) -> None:
        tablename = dc_type.__name__

        if not self._initialized:
            self.create_db()

        if tablename not in self._tables:
            self.create_table(dc_type)

        where_clause = " and ".join([f"{key} = ?" for key in kwargs])
        if where_clause:
            where_clause = "WHERE " + where_clause
        else:
            where_clause = ""
        delete_clause = f"DELETE FROM {tablename} "

        q = " ".join([delete_clause, where_clause, ";"])

        cur = self._connection.cursor()
        parameters = [value for value in kwargs.values()]
        cur.execute(q, tuple(parameters))
        return None
