import asyncio
import hashlib
import os
import sqlite3

from asyncio import Task
from pathlib import Path
from string import Template
from typing import Optional, List, Union

from server.models.extra_models import TokenModel
from sqliteclient.data.Models import (
    ViewUsersPasswords,
    ViewUsersApiTokens,
    ViewUsersGroups,
    ViewAllJoinedColumns,
    TableApiTokens,
    TablePasswords,
)


class SqliteClient:
    """
    This Client is for retrieving the data from the webui.db.
    The database is a sqlite database and understands the SQL query language.
    """

    # Todo: with get env from systemd service, replace default db path
    DEFAULT_DATABASE_PATH = "/mnt/persistent-data/evtec-webui-data/database/webui.db"
    TASK_NAME_FETCH_DATABASE = "fetchDatabase"

    COLUMN_NAME = 1

    PRAGMA_TABLE_PASSWORD = "PRAGMA table_info(passwords)"
    SEED = "seed"

    SELECT_VIEW_USERS_PASSWORDS = "SELECT * FROM view_users_passwords"
    SELECT_VIEW_USERS_API_TOKENS = "SELECT * FROM view_users_apiTokens"
    SELECT_VIEW_USERS_PERMISSIONS = "SELECT * FROM view_users_groups"
    SELECT_VIEW_ALL_JOINED_COLUMNS = "SELECT * FROM view_all_data"
    SELECT_TABLE_UGROUPS = "SELECT * FROM uGroups"
    SELECT_TABLE_PASSWORDS = "SELECT * FROM passwords"

    INSERT_INTO_API_TOKENS = (
        "INSERT INTO apiTokens (users_userId, token, validityStart, validityEnd, historyState, isSessionToken, tokenName) "
        "VALUES ($userID, '$token', '$validityStart', '$validityEnd', '$historyState', $isSessionToken, '$tokenName')"
    )
    INSERT_INTO_USER_GROUPS = "INSERT INTO users_groups (users_userId, groups_groupId) VALUES ($userID, $groupID)"
    INSERT_INTO_UGROUPS = "INSERT INTO uGroups (groupName) VALUES ('$groupName')"

    DELETE_FROM_API_TOKENS = (
        "DELETE FROM apiTokens WHERE users_userId=$userID AND isSessionToken=$isSessionToken AND tokenName='$tokenName'"
    )
    UPDATE_PASSWORD = "UPDATE passwords SET password = '$newPassword', seed = '$newSeed' WHERE passwordId = $passwordID AND users_userId = $userID"
    ALTER_TABLE_SEED = "ALTER TABLE passwords ADD COLUMN seed varchar(50)"
    DROP_VIEW_PASSWORDS = "DROP VIEW IF EXISTS view_users_passwords"

    CREATE_VIEW_WITH_SEED = """
    -- Update view view_users_passwords to include the seed column
    CREATE VIEW view_users_passwords AS
    SELECT
        users.userId,
        users.userName,
        users.name,
        users.surName,
        passwords.passwordId,
        passwords.password,
        passwords.seed

    FROM
        passwords
        JOIN users ON users.userId = passwords.users_userId
    """

    def __init__(self, databasePath: Optional[Path] = None):
        self.databasePath = databasePath or SqliteClient.DEFAULT_DATABASE_PATH

        self.connection: Optional[sqlite3.Connection] = None
        self.cursor: Optional[sqlite3.Cursor] = None

        self._usersPasswords: Optional[List[ViewUsersPasswords]] = None
        self._usersApiTokens: Optional[List[ViewUsersApiTokens]] = None
        self._usersPermissions: Optional[List[ViewUsersGroups]] = None
        self._allJoinedColumns: Optional[List[ViewAllJoinedColumns]] = None

        # Start the loadTables method as a task
        self.loadTablesTask: Optional[Task] = None
        self.wasLoaded: bool = False

    def startLoadTablesTask(self):
        """Start the loadTables method as a task"""
        self.loadTablesTask = asyncio.create_task(self._fetchDatabase(), name=SqliteClient.TASK_NAME_FETCH_DATABASE)

    def checkUserExists(self, whereUser: str) -> bool:
        """Check if a user exists in the database"""
        matchingUsers = [fetchedUser for fetchedUser in self._usersPasswords if fetchedUser.userName == whereUser]
        return len(matchingUsers) > 0

    def checkPassword(self, user: str, password: str) -> bool:
        """Check if the given password matches the database entry"""
        matchingUser = [fetchedUser for fetchedUser in self._usersPasswords if fetchedUser.userName == user][0]

        # Concatenate the hashed password and the seed
        dataToHash = password.encode("utf-8") + bytes.fromhex(matchingUser.seed)

        # Hash the concatenated data using SHA256
        finalHash = hashlib.sha256(dataToHash).hexdigest()

        return finalHash == matchingUser.password

    def checkTokenExist(self, token: str) -> bool:
        """
        Check if the token does exist in the database

        :param token: The token for checking the database.
        :return: True if the token was found, otherwise False.
        """
        return len([row for row in self._usersApiTokens if row.token == token]) >= 1

    def checkTokenNameExist(self, tokenName: str) -> bool:
        """
        Check if the tokenNAme does exist in the database

        :param tokenName: The tokenName for checking the database.
        :return: True if the tokenName was found, otherwise False.
        """
        return len([row for row in self._usersApiTokens if row.tokenName == tokenName]) >= 1

    def getTokenData(self, tokenName: str) -> Union[ViewUsersApiTokens, None]:
        """
        Gets the tokenData for the given tokenName

        :param tokenName: The tokenName for checking the database.
        :return: ViewUsersApiTokens row if was found else None.
        """
        tokenData = [row for row in self._usersApiTokens if row.tokenName == tokenName]

        return tokenData[0] if len(tokenData) > 0 else None

    def getTokensUser(self, token: TokenModel) -> Union[str, None]:
        """
        Return the assigned user for the given token.

        :param token: The token for checking the database.
        :return: The assigned user for the given token.
        """
        rowMatch = [row for row in self._usersApiTokens if row.token == token.sub]
        userName = rowMatch[0].userName if rowMatch else None
        return userName

    def getTokensUserByTokenName(self, tokenName: str) -> Union[str, None]:
        """
        Return the assigned user for the given token.

        :param token: The token for checking the database.
        :return: The assigned user for the given token.
        """
        rowMatch = [row for row in self._usersApiTokens if row.tokenName == tokenName]
        userName = rowMatch[0].userName if rowMatch else None
        return userName

    def checktokenHasPriviliges(self, token: str, group: str) -> bool:
        """
        Check if the token has the priviliges for the group action.

        :param token: The token for checking the database.
        :return: True if the token has the priviliges for the group action, otherwise False.
        """
        return len([row for row in self._allJoinedColumns if row.token == token and row.groupName == group]) >= 1

    def getUsersPasswords(self, whereUser: Optional[str] = None) -> List[Union[ViewUsersPasswords, None]]:
        """
        Gets the data from the view_users_passwords.

        :param whereUser: Give a user as a string when you want to filter the data.
        :return: The data filtered or unfiltered from the view_users_passwords.
        """
        if whereUser:
            return [fetchedUser for fetchedUser in self._usersPasswords if fetchedUser.userName == whereUser]

        return self._usersPasswords

    def getUsersApiTokens(
        self, whereUser: Optional[str] = None, getSessionToken: bool = False
    ) -> List[Union[ViewUsersApiTokens, None]]:
        """
        Gets the data from the view_users_apiTokens.

        :param whereUser: Give a user as a string when you want to filter the data.
        :return: The data filtered or unfiltered from the view_users_apiTokens.
        """
        if whereUser:
            return [
                fetchedUser
                for fetchedUser in self._usersApiTokens
                if fetchedUser.userName == whereUser and fetchedUser.isSessionToken == getSessionToken
            ]

        return [fetchedUser for fetchedUser in self._usersApiTokens if fetchedUser.isSessionToken == getSessionToken]

    def getUsersGroups(self, whereUser: Optional[str] = None) -> List[Union[ViewUsersGroups, None]]:
        """
        Gets the data from the view_users_permissions.

        :param whereUser: Give a user as a string when you want to filter the data.
        :return: The data filtered or unfiltered from the view_users_permissions.
        """
        if whereUser:
            return [fetchedUser for fetchedUser in self._usersPermissions if fetchedUser.userName == whereUser]

        return self._usersPermissions

    def getAllJoinedColumns(self, whereUser: Optional[str] = None):
        """
        Gets the data from the view_all_data.

        :param whereUser: Give a user as a string when you want to filter the data.
        :return: The data filtered or unfiltered from the view_all_data.
        """
        # Todo: add filter
        if not whereUser:
            return self._allJoinedColumns

    async def createSessionToken(self, userName: str, newTokenRow: TableApiTokens) -> str:
        """ """
        # Lock the database
        self.wasLoaded = False

        # Arrange the statement for the new session token
        template = Template(SqliteClient.INSERT_INTO_API_TOKENS)
        statement = template.substitute(
            userID=newTokenRow.users_userId,
            token=newTokenRow.token,
            validityStart=newTokenRow.validityStart,
            validityEnd=newTokenRow.validityEnd,
            historyState=newTokenRow.historyState,
            isSessionToken=newTokenRow.isSessionToken,
            tokenName=newTokenRow.tokenName,
        )
        # Write into the database
        await self._writeIntoDatabase(statement=statement)

        # Re fetch the data into the internal lists
        await self._fetchDatabase()

        return self.getUsersApiTokens(whereUser=userName, getSessionToken=newTokenRow.isSessionToken)[-1].token

    async def deleteApiToken(self, userData: ViewUsersPasswords, isSessionToken: bool = True, tokenName="") -> None:
        """
        Does delete an invalid session token to the given user.
        """
        # Lock the database
        self.wasLoaded = False

        # Arrange the statement for the removing the session token
        template = Template(SqliteClient.DELETE_FROM_API_TOKENS)
        statement = template.substitute(userID=userData.userId, isSessionToken=isSessionToken, tokenName=tokenName)

        # Write into the database
        await self._writeIntoDatabase(statement=statement)

        # Re fetch the data into the internal lists
        await self._fetchDatabase()

    async def setPassword(self, userData: Union[TablePasswords]) -> None:
        """
        Does set a new password for the given user.

        :param userData: The new password for the given user.
        """
        # Lock the database
        self.wasLoaded = False

        # Arrange the statement for the password update
        template = Template(SqliteClient.UPDATE_PASSWORD)
        statement = template.substitute(
            newPassword=userData.password,
            newSeed=userData.seed,
            userID=userData.users_userId,
            passwordID=userData.passwordId,
        )

        # Write into the database
        await self._writeIntoDatabase(statement=statement)

        # Re fetch the data into the internal lists
        await self._fetchDatabase()

    async def applyPasswordPatch(self) -> None:
        """
        Does adapt the database structure and password self for having more secure password with hash and seed.
        """
        try:
            # Build the connection and cursor
            await self._buildConnection()

            # Check if the column 'seed' is there
            res = self.cursor.execute(SqliteClient.PRAGMA_TABLE_PASSWORD)

            for column in res:
                if column[SqliteClient.COLUMN_NAME] == SqliteClient.SEED:
                    # Leave if it was detected
                    return

            # Add the column seed into table passwords
            self.cursor.execute(SqliteClient.ALTER_TABLE_SEED)

            # Replace the view for retrieving also the seed ot the table
            self.cursor.execute(SqliteClient.DROP_VIEW_PASSWORDS)
            self.cursor.execute(SqliteClient.CREATE_VIEW_WITH_SEED)

            # Retrieve the content of the table passwords
            res = self.cursor.execute(SqliteClient.SELECT_TABLE_PASSWORDS)
            passwords = res.fetchall()

            # Correct the new passwords with hash and seed
            for row in passwords:
                # Check if the value is not None, then the seed was not generated
                if not row[3]:
                    passwordId = row[0]
                    userId = row[1]
                    password = row[2]
                    seed = os.urandom(32)

                    # Hash the origin password with sha256 (this will be retrieved from the webui)
                    passwordHashed = hashlib.sha256(password.encode("utf-8")).hexdigest()

                    # Concatenate the hashed password and the seed
                    dataToHash = passwordHashed.encode("utf-8") + seed

                    # Hash the concatenated data using SHA256
                    finalHash = hashlib.sha256(dataToHash).hexdigest()

                    user = TablePasswords(
                        passwordId=passwordId, users_userId=userId, password=finalHash, seed=seed.hex()
                    )

                    await self.setPassword(user)

            # Close the connection clean
            await self._closeConnection()

        except sqlite3.Error as error:
            print(error)
            return

    async def applyApitokenRemovalPatch(self) -> None:
        """This Patch does remove the builtin default development Api Token"""
        for token in self._usersApiTokens:
            if "live" in token.token and token.tokenName == "developmentToken":
                userData = self.getUsersPasswords(whereUser=token.userName)[0]
                await self.deleteApiToken(userData=userData, isSessionToken=False, tokenName=token.tokenName)

    async def applyCustomerRightsPatch(self) -> None:
        """
        This patch checks if the customer has same rights as the service user.
        If its assigned the user customer will be added to the following groups:
        - editConfig
        - editCustomGraphics
        - editPromotionScreen
        """
        # Definitions
        USER_ID_CUSTOMER = 5
        GROUD_ID_EDIT_CONFIG = 3
        GROUD_ID_EDIT_CUSTOM_GRAPHICS = 8
        GROUD_ID_EDIT_PROMO_SCREEN = 11
        GROUPS_TO_BE_ADDED = ["editCustomGraphics", "editPromotionScreen", "editConfig"]

        customerGroups = []
        for userPermission in self._usersPermissions:
            if userPermission.userName == "customer":
                customerGroups.append(userPermission.groupName)

        # Check if the group is assigned to customer, if not adds customer to this groups
        # Groups: "editCustomGraphics", "editPromotionScreen"
        if not any(group in customerGroups for group in GROUPS_TO_BE_ADDED):
            # Lock the database
            self.wasLoaded = False

            # Arrange the statement for the editConfig assignment
            editConfigTemplate = Template(SqliteClient.INSERT_INTO_USER_GROUPS)
            editConfigStatement = editConfigTemplate.substitute(userID=USER_ID_CUSTOMER, groupID=GROUD_ID_EDIT_CONFIG)

            # Arrange the statement for the editCustomGraphics assignment
            editCustomGraphicsTemplate = Template(SqliteClient.INSERT_INTO_USER_GROUPS)
            editCustomGraphicsStatement = editCustomGraphicsTemplate.substitute(
                userID=USER_ID_CUSTOMER, groupID=GROUD_ID_EDIT_CUSTOM_GRAPHICS
            )

            # Arrange the statement for the editPromotionScreen assignment
            editPromotionScreenTemplate = Template(SqliteClient.INSERT_INTO_USER_GROUPS)
            editPromotionScreenStatement = editPromotionScreenTemplate.substitute(
                userID=USER_ID_CUSTOMER, groupID=GROUD_ID_EDIT_PROMO_SCREEN
            )

            # Write into the database
            for statement in [editConfigStatement, editCustomGraphicsStatement, editPromotionScreenStatement]:
                await self._writeIntoDatabase(statement=statement)

            # Re fetch the data into the internal lists
            await self._fetchDatabase()

    async def applyApiTokenPermissionsPatch(self) -> None:
        """Detects if the group 'tokenAdministration' is missing and add/assign to the users."""
        GROUPS_TO_BE_ADDED = ["tokenAdministration"]
        USER_ID_TO_ADD = [1, 2, 3, 4, 5]

        # Retrieve all active groups from user admin
        adminGroups = []
        for row in self._usersPermissions:
            if row.userName == "admin":
                adminGroups.append(row.groupName)

        # Check if the group 'tokenAdministration' is assigned to admin
        # GROUPS_TO_BE_ADDED: "tokenAdministration"
        if not any(group in adminGroups for group in GROUPS_TO_BE_ADDED):
            # Lock the database
            self.wasLoaded = False

            # Add the group 'tokenAdministration' in the table uGroups
            groupAddTemplate = Template(SqliteClient.INSERT_INTO_UGROUPS)
            groupAddStatement = groupAddTemplate.substitute(groupName=GROUPS_TO_BE_ADDED[0])
            await self._writeIntoDatabase(statement=groupAddStatement)

            # Get the new groupID of the created group 'tokenAdministration'
            groudID = None
            await self._buildConnection()
            res = self.cursor.execute(SqliteClient.SELECT_TABLE_UGROUPS)
            resTuples = res.fetchall()
            for groupRow in resTuples:
                if groupRow[1] == GROUPS_TO_BE_ADDED[0]:  # (20, 'tokenAdministration')
                    groudID = int(groupRow[0])

            if groudID:
                # Assign all the users to the new 'tokenAdministration' group
                for user in USER_ID_TO_ADD:
                    # Arrange the statement for the editConfig assignment
                    userUgroupTemplate = Template(SqliteClient.INSERT_INTO_USER_GROUPS)
                    userUgroupStatement = userUgroupTemplate.substitute(userID=user, groupID=groudID)
                    await self._writeIntoDatabase(statement=userUgroupStatement)

            # Re fetch the data into the internal lists
            await self._fetchDatabase()

    async def _buildConnection(self) -> None:
        """Builds the connection to the database and creates a cursor for the command execution."""
        if self.connection is None and self.cursor is None:
            self.connection = sqlite3.connect(self.databasePath)
            self.cursor = self.connection.cursor()

    async def _closeConnection(self) -> None:
        """Closes the connection cleanly and removes the cursor."""
        if self.connection is not None and self.cursor is not None:
            self.connection.close()
            self.cursor = None
            self.connection = None

    async def _fetchDatabase(self) -> None:
        """This coroutine does fetch all views from the database."""
        await self._fetchUsersPasswords()
        await self._fetchUsersApiTokens()
        await self._fetchUsersPermissions()
        await self._fetchAllJoinedColumns()
        self.wasLoaded = True

    async def _fetchUsersPasswords(self) -> None:
        """Does fetch the view_users_passwords data and parse it into a List of ViewUsersPasswords model."""
        try:
            # Build the connection and cursor
            await self._buildConnection()

            # Get the data from the database
            res = self.cursor.execute(SqliteClient.SELECT_VIEW_USERS_PASSWORDS)
            resTuples = res.fetchall()

            # Transform into a dataclass model
            self._usersPasswords = [
                ViewUsersPasswords(userId, userName, name, surName, passwordId, password, seed)
                for userId, userName, name, surName, passwordId, password, seed in resTuples
            ]

            # Close the connection clean
            await self._closeConnection()
        except sqlite3.Error as error:
            print(error)

    async def _fetchUsersApiTokens(self) -> None:
        """Does fetch the view_users_apiTokens data and parse it into a List of ViewUsersApiTokens model."""
        try:
            # Build the connection and cursor
            await self._buildConnection()

            # Get the data from the database
            res = self.cursor.execute(SqliteClient.SELECT_VIEW_USERS_API_TOKENS)
            resTuples = res.fetchall()

            # Transform into a dataclass model
            self._usersApiTokens = [
                ViewUsersApiTokens(
                    userId,
                    userName,
                    name,
                    surName,
                    tokenId,
                    token,
                    tokenName,
                    validityStart,
                    validityEnd,
                    historyState,
                    isUserToken,
                )
                for userId, userName, name, surName, tokenId, token, tokenName, validityStart, validityEnd, historyState, isUserToken in resTuples
            ]

            # Close the connection clean
            await self._closeConnection()
        except sqlite3.Error as error:
            print(error)

    async def _fetchUsersPermissions(self) -> None:
        """Does fetch the view_users_permissions data and parse it into a List of ViewUsersGroups model."""
        try:
            # Build the connection and cursor
            await self._buildConnection()

            # Get the data from the database
            result = self.cursor.execute(SqliteClient.SELECT_VIEW_USERS_PERMISSIONS)
            resultTuples = result.fetchall()

            # Transform into a dataclass model
            self._usersPermissions = [
                ViewUsersGroups(userId, userName, name, surName, permissionId, permissionName)
                for userId, userName, name, surName, permissionId, permissionName in resultTuples
            ]

            # Close the connection clean
            await self._closeConnection()
        except sqlite3.Error as error:
            print(error)

    async def _fetchAllJoinedColumns(self) -> None:
        """Does fetch the view_all_data data and parse it into a List of ViewAllJoinedColumns model."""
        try:
            # Build the connection and cursor
            await self._buildConnection()

            # Get the data from the database
            res = self.cursor.execute(SqliteClient.SELECT_VIEW_ALL_JOINED_COLUMNS)
            resTuples = res.fetchall()

            # Transform into a dataclass model
            self._allJoinedColumns = [
                ViewAllJoinedColumns(
                    userId,
                    userName,
                    name,
                    surName,
                    passwordId,
                    password,
                    tokenId,
                    token,
                    tokenName,
                    validityStart,
                    validityEnd,
                    historyState,
                    isSessionToken,
                    groups_groupId,
                    groupName,
                )
                for userId, userName, name, surName, passwordId, password, tokenId, token, tokenName, validityStart, validityEnd, historyState, isSessionToken, groups_groupId, groupName, in resTuples
            ]

            # Close the connection clean
            await self._closeConnection()
        except sqlite3.Error as error:
            print(error)

    async def _writeIntoDatabase(self, statement: str) -> None:
        """ """
        try:
            await self._buildConnection()

            # Get the data from the database
            res = self.cursor.execute(statement)
            self.connection.commit()

            # Close the connection clean
            await self._closeConnection()
        except sqlite3.Error as error:
            print(error)
