import logging
import sqlite3
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Any, Callable, Tuple, List, Dict

CACHE_FOLDER = Path("~/.cache/bagsy/").expanduser()
USER_FOLDER = Path("~/.local/share/bagsy/").expanduser()

logger = logging.getLogger(__name__)


@dataclass
class PushQueueElement:
    push_queue_id: int
    server_uid: str
    checksum: Optional[str]
    path: str
    pushed: bool
    server_checked: bool


class Database:

    def __init__(self, path: Path, read_only: bool):
        path.parent.mkdir(parents=True, exist_ok=True)
        self.connector = sqlite3.connect(path)
        self.read_only = read_only

        self.exec_safely_at_once(self.__create_if_not__exists__)

    @staticmethod
    def __create_if_not__exists__(cursor: sqlite3.Cursor):
        cursor.execute("""CREATE TABLE IF NOT EXISTS options
                          (
                              id    integer
                                  constraint options_pk primary key,
                              name  TEXT,
                              value TEXT
                          );""")
        cursor.execute("""CREATE TABLE IF NOT EXISTS tokens
                          (
                              id           integer
                                  constraint tokens_pk primary key,
                              username     TEXT,
                              instance_url TEXT,
                              token        TEXT
                          );""")
        cursor.execute("""CREATE TABLE IF NOT EXISTS push_queue
                          (
                              id             integer
                                  constraint push_queue_pk primary key,
                              server_uid     TEXT,
                              checksum       TEXT,
                              path           TEXT,
                              pushed         boolean,
                              server_checked boolean
                          );""")

    def select(self, query: str, option: Optional[Tuple | Dict] = None) -> Optional[Any]:
        def func(c: sqlite3.Cursor):
            if option is None:
                c.execute(query)
            else:
                c.execute(query, option)

        r = self.exec_safely_at_once(func)
        if r[0]:
            return list(r[1])
        return None

    def select_one(self, query: str, option: Optional[Tuple | Dict] = None) -> Optional[Any]:
        r = self.select(query, option)
        return None if r is None or len(r) == 0 else r[0]

    def edit_query(self, query: str, option: Optional[Tuple | Dict] = None) -> bool:
        def func(c: sqlite3.Cursor):
            if option is None:
                c.execute(query)
            else:
                c.execute(query, option)

        return self.exec_safely_at_once(func)[0]

    def exec_safely_at_once(self,
                            func: Callable[[sqlite3.Cursor],
                            Optional[sqlite3.Cursor]]) -> Tuple[bool, sqlite3.Cursor]:
        c = self.connector.cursor()
        try:
            r = func(c)
            if self.read_only:
                self.connector.rollback()
            else:
                self.connector.commit()
            return True, r if r is not None else c
        except self.connector.Error as e:
            logger.error(f"An error occurred will writing to the db: {e}")
            self.connector.rollback()
        return False, c

    # ========================= OPTION =========================

    def set_option(self, option: str, value: str):
        def _exec(cursor: sqlite3.Cursor):
            cursor.execute("DELETE FROM options WHERE name=?;", (option,))
            cursor.execute("INSERT INTO options (name, value) VALUES (?, ?);", (option, value))

        self.exec_safely_at_once(_exec)

    def get_option(self, name: str) -> Optional[str]:
        r = self.select_one("SELECT value FROM options WHERE name=:name;", {name: name})
        return None if r is None else r[0]

    # ========================= TOKENS =========================

    def save_token(self, instance_url: str, username: str, token: str) -> bool:
        return self.edit_query("INSERT INTO tokens (username, instance_url, token) VALUES (?, ?, ?);",
                               (username, instance_url, token))

    def get_token(self, token_id: int) -> Optional[Tuple[int, str, str]]:
        return self.select_one("SELECT username, instance_url, token FROM tokens WHERE id=?", (token_id,))

    def list_token(self) -> Optional[List[Tuple[int, str, str]]]:
        return self.select("SELECT id, username, instance_url FROM tokens;")

    # ========================= PUSH =========================

    def get_push_queue_element(self, path: str, checksum: Optional[str]) -> Optional[PushQueueElement]:
        if checksum is not None:
            r = self.select_one("SELECT * FROM push_queue WHERE checksum=?;", (checksum,))
            if r is not None:
                return PushQueueElement(*r)

        r = self.select_one("SELECT * FROM push_queue WHERE path=?;", (path,))
        if r is not None:
            return PushQueueElement(*r)
        return None

    def add_to_push_queue(self, uid: str, path: str, checksum: Optional[str]) -> Optional[PushQueueElement]:
        if self.edit_query("INSERT INTO push_queue (server_uid, path, checksum, pushed, server_checked) "
                           "VALUES (?, ?, ?, False, False);",
                           (uid, path, checksum)):
            return self.get_push_queue_element(path, checksum)
        return None

    def update_push_queue_element(self, e: PushQueueElement) -> bool:
        return self.edit_query("UPDATE push_queue SET pushed=true WHERE id=?", (e.push_queue_id,))
