import json
from os import path

import appdirs
from encryption_handler import EncryptionHandler


class DataHandler:
    """
    WARNING:-
        THere is no fail-safe for if the app is opened for the first time.

    A class that handles the stored data.
    This class does NOT handle encryption directly. Look at ./encryption_handler.py
    When a instance is created, the instance should be used for one user ONLY.

    In this class "data" is used. THe following is the format of data:-
        data:- {
            entry_name: {
                field_name: field_value
            }
        }
    Example for "data":
        {
            "Amazon": {
                "Username": "Kosh",
                "Password": "Pass1234"
            },
            "Matrix": {
                "Username": "Kosh",
                "Email": "kosh@fake.com",
                "id": "@kosh:matrix.com"
            }
        }

    Attributes:-
    __file_path (private):- The path to the file where the users encrypted data is stored.
    __password (private):- The users password.
    """

    __file_path: str
    __user_name: str
    __password: str

    def __init__(self, user_name: str, password: str) -> None:
        """
        Logs in the user
        Parameters:-
            user_name
            password
        Raises:-
            If user_name does not exist, ValueError will be thrown
            If password is wrong, ValueError will be thrown
        WARNING:-
            If this is the first time the app is opened, the user_name will be shown as wrong
        """
        self.__file_path = appdirs.user_data_dir()
        self.__file_path = path.join(self.__file_path, "Unnamed_Password_Manager")
        self.__user_name = user_name
        self.__file_path = path.join(self.__file_path, self.__user_name)
        self.__password = password

        if not path.exists(self.__file_path):
            raise ValueError(f"User {user_name} does not exist.")

        # This will call encryption handler,
        # which will throw an error if the password is wrong
        self.get_data()

    @staticmethod
    def create_user(user_name: str, password: str) -> None:
        """
        Creates a new user.
        Parameters:-
            user_name
            password
        Raises:-
            If user_name exists, ValueError is thrown
        """
        file_path = path.join(
                appdirs.user_data_dir(),
                "Unnamed_Password_Manager",
                user_name
        )
        if path.exists(file_path):
            raise ValueError(f"User {user_name} already exists")
        with open(file_path, "wb") as file:
            file.write(EncryptionHandler.encrypt(b"{}", password.encode()))

    def get_data(self) -> dict[str, dict[str, str]]:
        """
        Get the data of the user.
        Return:-
            data:- Read the class documentation for more information.
        """
        with open(self.__file_path, "rb") as file:
            data: str = \
                    EncryptionHandler.decrypt(file.read(), self.__password.encode())\
                    .decode()
        return json.loads(data)

    def write_data(self, data: dict[str, dict[str, str]]) -> None:
        """
        Write the data of the user.
        Parameters:-
            data:- Read the class documentation for more information
        """
        json_data: str = json.dumps(data)
        encrypted_data: bytes = EncryptionHandler.encrypt(
                json_data.encode(),
                self.__password.encode()
        )
        with open(self.__file_path, "wb") as file:
            file.write(encrypted_data)

    def change_password(self, password: str) -> None:
        """
        Changes the password of the user.
        Parameters:-
            password
        """
        data: dict[str, dict[str, str]] = self.get_data()
        self.__password = password
        self.write_data(data)

    def add_entry(self, entry_name: str, fields: dict[str, str]) -> None:
        """
        Add a new entry:-
        Parameters:-
            entry_name
            fields:- {
                field_name_1: field_value_1,
                field_name_2: field_value_2
            }
        Raises:-
            A ValueError is raised if entry_name already exists.
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if entry_name in data:
            raise ValueError(f"Entry {entry_name} already exists.")

        data[entry_name] = fields
        self.write_data(data)

    def delete_entry(self, entry_name: str) -> None:
        """
        Deletes the given entry.
        Parameters:-
            entry_name
        Errors:-
            If the requested entry does not exist, an error will be thrown
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if entry_name not in data:
            raise ValueError(f"{entry_name} does not exist.")
        data.pop(entry_name)
        self.write_data(data)

    def edit_entry_name(self, old_entry_name: str, new_entry_name: str) -> None:
        """
        Changes the name of the entry.
        Parameters:-
            old_entry_name
            new_entry_name
        Errors:-
            If the old_entry_name does not exist, a ValueError will be thrown.
            If new_entry_name already exists, a ValueError will be raised.
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if new_entry_name in data:
            raise ValueError(f"\"{new_entry_name}\" already exists")
        if old_entry_name not in data:
            raise ValueError(f"No entry with name \"{old_entry_name}\"")

        self.delete_entry(old_entry_name)
        self.add_entry(new_entry_name, data[old_entry_name])

    def add_field(self,
            entry_name: str,
            field_name: str,
            field_value: str,
    ) -> None:
        """
        Adds a new field to a given entry.
        Parameters:-
            entry_name
            field_name
            field_value
        Raises:-
            If entry_name does not exist, a ValueError will be raised.
            If field_name already exists, a ValueError will be raised.
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if entry_name not in data:
            raise ValueError(f"No entry with name \"{entry_name}\"")
        if field_name in data[entry_name]:
            raise ValueError(f"Field {field_name} already exists under entry {entry_name}")
        data[entry_name][field_name] = field_value
        self.write_data(data)

    def delete_field(self, entry_name: str, field_name: str) -> None:
        """
        Adds a new field to a given entry.
        Parameters:-
            entry_name
            field_name
        Raises:-
            If entry_name does not exist, a ValueError will be raised.
            If field_name does not exists, a ValueError will be raised.
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if entry_name not in data:
            raise ValueError(f"No entry with name \"{entry_name}\"")
        if field_name not in data[entry_name]:
            raise ValueError(f"No field {field_name} under entry {entry_name}")
        data[entry_name].pop(field_name)
        self.write_data(data)

    def edit_field_name(
            self,
            entry_name: str,
            old_field_name: str,
            new_field_name: str
    ) -> None:
        """
        Change the name of a field under the given entry.
        Parameters:-
            entry_name
            old_field_name
            new_field_name
        Raises:-
            If entry_name does not exist, a ValueError will be raised.
            If old_field_name does not exists, a ValueError will be raised.
            If field_name already exists, a ValueError will be raised.
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if entry_name not in data:
            raise ValueError(f"No entry with name \"{entry_name}\"")
        if old_field_name not in data[entry_name]:
            raise ValueError(f"No field {old_field_name} under entry {entry_name}")
        if new_field_name in data[entry_name]:
            raise ValueError(f"Field {new_field_name} already exists under {entry_name}")
        data[entry_name][new_field_name] = data[entry_name][old_field_name]
        data[entry_name].pop(old_field_name)
        self.write_data(data)

    def edit_field_value(self,
            entry_name: str,
            field_name: str,
            new_value: str,
    ) -> None:
        """
        Change the value of the given field under the given entry.
        Parameters:-
            entry_name
            field_name
            new_value
        Raises:-
            If entry_name does not exist, a ValueError will be raised.
            If field_name does not exists, a ValueError will be raised.
        """
        data: dict[str, dict[str, str]] = self.get_data()
        if entry_name not in data:
            raise ValueError(f"No entry with name \"{entry_name}\"")
        if field_name not in data[entry_name]:
            raise ValueError(f"No field {field_name} under entry {entry_name}")
        data[entry_name][field_name] = new_value
        self.write_data(data)