import sqlite3
import os
import flask_bcrypt
import time
import config


class CheckSumCalculator:

    def __init__(self):
        pass

    def checksum(self, string: str):
        return flask_bcrypt.generate_password_hash(string, config.bcrypt_rounds)


class DuplicateDirectoryException(Exception):
    pass


class DuplicateUserException(Exception):
    pass


class User:
    """
    Data structure to hold user information
    """

    def __init__(self, username: str, hashed_password: bytes, admin: bool):
        self.username = username
        self.hashed_password = hashed_password
        self.admin = admin
        self.readable_directories = set()


class Option:
    """
    Data structure to hold a directory option
    """

    def __init__(self, key: str, value: str, dir_id: int=None, opt_id: int = None):
        self.key = key
        self.value = value
        self.id = opt_id
        self.dir_id = dir_id


class Directory:
    """
    Data structure to hold directory information
    """
    def __init__(self, path: str, enabled: bool, options: list, name: str):
        self.id = None
        self.path = path
        self.enabled = enabled
        self.options = options
        self.name = name

    def __str__(self):
        return self.path + " | enabled: " + str(self.enabled) + " | opts: " + str(self.options)
    
    def get_option(self, key):

        for option in self.options:
            if option.key == key:
                return option.value

        return None

    def set_default_options(self):

        self.options = []

        for option in config.default_options:
            self.options.append(Option(option, config.default_options[option]))


class Task:

    INDEX = 1
    GEN_THUMBNAIL = 2

    def __init__(self, task_type: int, dir_id: int, completed: bool = False, completed_time: time.time = None,
                 task_id: int = None):
        self.id = task_id
        self.type = task_type
        self.dir_id = dir_id
        self.completed = completed
        self.completed_time = completed_time


class LocalStorage:
    """
    Manages storage of application data to disk.
    Could be refactored into a abstract class to switch from SQLite3 to something else
    """

    def __init__(self, db_path):
        self.cached_dirs = {}
        self.cached_users = {}
        self.cached_tasks = {}
        self.db_path = db_path
        self.dir_cache_outdated = True  # Indicates that the database was changed since it was cached in memory
        self.user_cache_outdated = True
        self.task_cache_outdated = True

    def init_db(self, script_path):
        """Creates a blank database. Overwrites the old one"""
        if os.path.isfile(self.db_path):
            os.remove(self.db_path)

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        with open(script_path, "r") as f:
            c.executescript(f.read())

        conn.commit()
        c.close()
        conn.close()

    def save_directory(self, directory: Directory):
        """
        Save directory to storage
        :param directory: Directory to save
        :return: id
        """

        self.dir_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        try:
            c.execute("INSERT INTO Directory (path, enabled, name) VALUES (?, ?, ?)",
                      (directory.path, directory.enabled, directory.name))
            c.execute("SELECT last_insert_rowid()")

            dir_id = c.fetchone()[0]
            c.close()
            conn.commit()
            conn.close()

            for opt in directory.options:
                opt.dir_id = dir_id
                self.save_option(opt)

            return dir_id
        except sqlite3.IntegrityError:
            c.close()
            conn.close()
            raise DuplicateDirectoryException("Duplicate directory path: " + directory.path)

    def remove_directory(self, dir_id: int):
        """Remove a directory from the database"""

        self.dir_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()

        c.execute("DELETE FROM Option WHERE directory_id=?", (dir_id,))
        c.execute("DELETE FROM Task WHERE directory_id=?", (dir_id,))
        c.execute("DELETE FROM Directory WHERE id=?", (dir_id,))

        c.close()
        conn.commit()
        conn.close()

    def dirs(self) -> dict:

        if self.dir_cache_outdated:

            self.cached_dirs = {}

            conn = sqlite3.connect(self.db_path)
            c = conn.cursor()
            c.execute("SELECT id, path, enabled, name FROM Directory")
            db_directories = c.fetchall()
            c.execute("SELECT key, value, directory_id, id FROM Option")
            db_options = c.fetchall()

            for db_dir in db_directories:

                options = []
                directory = Directory(db_dir[1], db_dir[2], options, db_dir[3])

                for db_opt in db_options:
                    if db_opt[2] == db_dir[0]:
                        options.append(Option(db_opt[0], db_opt[1], db_opt[2], db_opt[3]))

                directory.id = db_dir[0]

                self.cached_dirs[directory.id] = directory

            self.dir_cache_outdated = False
            return self.cached_dirs

        else:
            return self.cached_dirs

    def save_user(self, user: User):
        """Save user to storage"""

        self.user_cache_outdated = True

        try:
            conn = sqlite3.connect(self.db_path)
            c = conn.cursor()
            c.execute("INSERT INTO User (username, password, is_admin) VALUES (?,?,?)",
                      (user.username, user.hashed_password, user.admin))
            c.close()
            conn.commit()
            conn.close()
        except sqlite3.IntegrityError:
            raise DuplicateUserException("Duplicate username: " + user.username)

    def users(self) -> dict:
        """Get user list"""

        if self.user_cache_outdated:

            self.cached_users = {}

            conn = sqlite3.connect(self.db_path)
            c = conn.cursor()
            c.execute("SELECT username, is_admin FROM User")

            db_users = c.fetchall()

            for db_user in db_users:
                user = User(db_user[0], b"", bool(db_user[1]))

                c.execute("SELECT username, directory_id FROM User_canRead_Directory WHERE username=?", (db_user[0],))
                db_accesses = c.fetchall()

                for db_access in db_accesses:
                    user.readable_directories.add(db_access[1])

                self.cached_users[db_user[0]] = user

            conn.close()

            return self.cached_users

        else:
            return self.cached_users

    def auth_user(self, username: str, password: str) -> bool:
        """Authenticates an user"""

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("SELECT username, password FROM User WHERE username=?", (username,))

        db_user = c.fetchone()

        if db_user is not None:
            return flask_bcrypt.check_password_hash(db_user[1], password)

        return False

    def update_user(self, user: User) -> None:
        """Updates an user. Will have no effect if the user does not exist"""

        self.user_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("UPDATE User SET is_admin=? WHERE username=?", (user.admin, user.username))

        c.execute("DELETE FROM User_canRead_Directory WHERE username=?", (user.username, ))
        conn.commit()

        for access in user.readable_directories:
            c.execute("INSERT INTO User_canRead_Directory VALUES (?,?)", (user.username, access))

        c.close()
        conn.commit()
        conn.close()

    def remove_user(self, username: str):
        """Remove an user from the database"""

        self.user_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("DELETE FROM User WHERE username=?", (username,))

        c.close()
        conn.commit()
        conn.close()

    def update_directory(self, directory):
        """Updated a directory (Options are left untouched). Will have no effect if the directory does not exist"""

        self.dir_cache_outdated = True

        try:
            conn = sqlite3.connect(self.db_path)
            c = conn.cursor()
            c.execute("UPDATE Directory SET name=?, path=?, enabled=? WHERE id=?",
                      (directory.name, directory.path, directory.enabled, directory.id))

            c.close()
            conn.commit()
            conn.close()
        except sqlite3.IntegrityError:
            raise DuplicateDirectoryException("Duplicate directory: " + directory.path)

    def save_option(self, option: Option):

        self.dir_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("INSERT INTO Option (key, value, directory_id) VALUES (?, ?, ?)",
                  (option.key, option.value, option.dir_id))
        c.execute("SELECT last_insert_rowid()")

        opt_id = c.fetchone()[0]
        c.close()
        conn.commit()
        conn.close()

        return opt_id

    def del_option(self, opt_id):
        """Delete an option from the database"""

        self.dir_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("DELETE FROM Option WHERE id=?", (opt_id, ))

        conn.commit()
        conn.close()

    def update_option(self, option: Option):
        """Updates an option"""

        self.dir_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("UPDATE Option SET key=?, value=? WHERE id=?", (option.key, option.value, option.id))

        conn.commit()
        conn.close()

    def save_task(self, task: Task):
        """Save a task to the database"""

        self.task_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("INSERT INTO Task (directory_id, type, completed, completed_time) VALUES (?,?,?,?)",
                  (task.dir_id, task.type, task.completed, task.completed_time))
        c.execute("SELECT last_insert_rowid()")

        task_id = c.fetchone()[0]

        conn.commit()
        c.close()
        conn.close()

        return task_id

    def tasks(self):
        """Get the (cached) list of taks"""

        if self.task_cache_outdated:

            self.cached_tasks = {}

            conn = sqlite3.connect(self.db_path)
            c = conn.cursor()
            c.execute("SELECT id, directory_id, type, completed, completed_time FROM Task")

            tasks = c.fetchall()

            c.close()
            conn.close()

            for db_task in tasks:

                task = Task(db_task[2], db_task[1], db_task[3], db_task[4], db_task[0])
                self.cached_tasks[task.id] = task

            self.task_cache_outdated = False
            return self.cached_tasks

        else:
            return self.cached_tasks

    def del_task(self, task_id):
        """Delete a task from the database"""

        self.task_cache_outdated = True

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute("DELETE FROM Task WHERE id=?", (task_id, ))

        conn.commit()
        c.close()
        conn.close()

    def set_access(self, username, dir_id, has_access):

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()

        if has_access:
            try:
                c.execute("INSERT INTO User_canRead_Directory VALUES (?,?)", (username, dir_id))
            except sqlite3.IntegrityError:
                pass
        else:
            c.execute("DELETE FROM User_canRead_Directory WHERE username=? AND directory_id=?", (username, dir_id))

        conn.commit()
        c.close()
        conn.close()

    def get_access(self, username):

        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()

        c.execute("SELECT * FROM User_canRead_Directory WHERE username=?", (username,))

        accesses = c.fetchall()
        access_list = []

        for access in accesses:
            access_list.append(access[1])

        return access_list