# Copyright (c) 2020, 2026, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is designed to work with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms, as
# designated in a particular file or component or in included license
# documentation.  The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have either included with
# the program or referenced in the documentation.
#
# This program is distributed in the hope that it will be useful,  but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
# the GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

from mysqlsh.plugin_manager import plugin_function  # pylint: disable=no-name-in-module
import json
import datetime
from gui_plugin.core.Db import BackendDatabase, BackendTransaction
from . import backend
from gui_plugin.core.Error import MSGException
import gui_plugin.core.Error as Error
from gui_plugin.core.Context import get_context


@plugin_function("gui.modules.addData", shell=False, web=True)
def add_data(
    caption,
    content,
    data_category_id,
    tree_identifier,
    folder_path=None,
    profile_id=None,
    be_session=None,
):
    """Creates a new Module Data record for the given module
       and associates it to the active user profile and personal user group.

    Args:
        caption (str): The data caption
        content (str): The content of data module
        data_category_id (int): The id of data category
        tree_identifier (str): The identifier of the tree
        folder_path (str): The folder path f.e. "/scripts/server1"
        profile_id (int): The id of profile
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Returns:
        int: the id of the new record.
    """

    if caption.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER, f"Parameter 'caption' cannot be empty."
        )

    if tree_identifier.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'tree_identifier' cannot be empty.",
        )

    if folder_path.strip() == "" or folder_path.strip() == "/":
        folder_path = None

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            db.execute(
                """INSERT INTO data (data_category_id, caption,
                            content, created, last_update)
                            VALUES(?, ?, ?, ?, ?)""",
                (
                    data_category_id,
                    caption,
                    json.dumps(content),
                    datetime.datetime.now(),
                    datetime.datetime.now(),
                ),
            )

            id = db.get_last_row_id()
            context = get_context()

            backend.add_data_associations(
                db,
                id,
                tree_identifier,
                folder_path,
                (
                    profile_id
                    if profile_id
                    else context.web_handler.session_active_profile_id
                ),
                context.web_handler.user_personal_group_id,
            )

            return id


@plugin_function("gui.modules.listData", shell=False, web=True)
def list_data(folder_id, data_category_id=None, be_session=None):
    """Get list of data

    Args:
        folder_id (int): The id of the folder
        data_category_id (int): The id of data category
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Returns:
        list: the list of the data.
    """

    with BackendDatabase(be_session) as db:
        sql = """SELECT d.id, d.data_category_id, d.caption, d.created, d.last_update
                 FROM data d
                 JOIN data_folder_has_data dfhd ON dfhd.data_id=d.id
                    WHERE dfhd.data_folder_id=?"""
        args = (folder_id,)
        if data_category_id:
            CATEGORIES_SQL = """WITH RECURSIVE
                                categories(id) AS (
                                    SELECT id FROM data_category
                                        WHERE id=?
                                    UNION ALL
                                    SELECT dc.id
                                        FROM categories c
                                        JOIN data_category dc ON c.id=dc.parent_category_id
                                )
                                SELECT DISTINCT id FROM categories"""
            sql += f" AND d.data_category_id in ({CATEGORIES_SQL})"
            args += (data_category_id,)
        return db.select(sql, args)


@plugin_function("gui.modules.getDataContent", shell=False, web=True)
def get_data_content(id, be_session=None):
    """Gets content of the given module

    Args:
        id (int): The id of the data
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        dict: the content of the data.
    """
    with BackendDatabase(be_session) as db:
        res = db.execute(
            """SELECT content
                            FROM data
                            WHERE id=?""",
            (id,),
        ).fetch_one()
        # First we nee to make sure that data content exists for given module id,
        # then we can check if user have privileges for it.
        if res is None:
            raise MSGException(
                Error.MODULES_INVALID_MODULE_ID,
                f"There is no data for the given module id: {id}.",
            )

        # If user have any privileges either assigned to user group or profile
        # he can see data content, otherwise Exception will be raised
        context = get_context()
        backend.get_user_privileges_for_data(
            db, id, context.web_handler.session_user_id
        )

        try:
            content = json.loads(res["content"])
        except Exception as e:
            raise MSGException(
                Error.CORE_INVALID_DATA_FORMAT, f"Error decoding data content: {str(e)}"
            )

        return content


@plugin_function("gui.modules.shareDataToUserGroup", shell=False, web=True)
def share_data_to_user_group(
    id, user_group_id, read_only, tree_identifier, folder_path=None, be_session=None
):
    """Shares data to user group

    Args:
        id (int): The id of the data
        user_group_id (int): The id of user group
        read_only (int): The flag that specifies whether the data is read only
        tree_identifier (str): The identifier of the tree
        folder_path (str): The folder path f.e. "/scripts/server1"
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        int: the id of the folder to which the data was shared.
    """

    if tree_identifier.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'tree_identifier' cannot be empty.",
        )

    if folder_path.strip() == "" or folder_path.strip() == "/":
        folder_path = None

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            context = get_context()
            privileges = backend.get_user_privileges_for_data(
                db, id, context.web_handler.session_user_id
            )
            max_privilege = min([p["read_only"] for p in privileges])
            if max_privilege <= read_only:
                user_group_root_folder_id = backend.get_root_folder_id(
                    db, tree_identifier, "group", user_group_id
                )

                if user_group_root_folder_id is None:
                    user_group_root_folder_id = backend.create_user_group_data_tree(
                        db, tree_identifier, user_group_id
                    )

                if folder_path:
                    folder_user_group_id, _ = backend.create_folder(
                        db, folder_path, user_group_root_folder_id
                    )
                else:
                    folder_user_group_id = user_group_root_folder_id

                backend.add_data_to_folder(db, id, folder_user_group_id, read_only)
            else:
                raise MSGException(
                    Error.MODULES_SHARING_WITH_HIGHER_PERMISSIONS,
                    "Cannot share data with higher permission than user has.",
                )

            return folder_user_group_id


@plugin_function("gui.modules.addDataToProfile", shell=False, web=True)
def add_data_to_profile(
    id, profile_id, read_only, tree_identifier, folder_path=None, be_session=None
):
    """Shares data to profile

    Args:
        id (int): The id of the data
        profile_id (int): The id of profile
        read_only (int): The flag that specifies whether the data is read only
        tree_identifier (str): The identifier of the tree
        folder_path (str): The folder path f.e. "/scripts/server1"
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        The id of the folder to which the data was shared.
    """

    if tree_identifier.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'tree_identifier' cannot be empty.",
        )

    if folder_path.strip() == "" or folder_path.strip() == "/":
        folder_path = None

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            context = get_context()
            privileges = backend.get_user_privileges_for_data(
                db, id, context.web_handler.session_user_id
            )
            max_privilege = min([p["read_only"] for p in privileges])
            # We check if the user is owner of given profile
            if (
                backend.get_profile_owner(db, profile_id)
                == context.web_handler.session_user_id
            ):
                if max_privilege <= read_only:
                    profile_root_folder_id = backend.get_root_folder_id(
                        db, tree_identifier, "profile", profile_id
                    )

                    if profile_root_folder_id is None:
                        profile_root_folder_id = backend.create_profile_data_tree(
                            db, tree_identifier, profile_id
                        )

                    if folder_path:
                        folder_profile_id, _ = backend.create_folder(
                            db, folder_path, profile_root_folder_id
                        )
                    else:
                        folder_profile_id = profile_root_folder_id

                    backend.add_data_to_folder(db, id, folder_profile_id, read_only)
                else:
                    raise MSGException(
                        Error.MODULES_SHARING_WITH_HIGHER_PERMISSIONS,
                        "Cannot assign data to profile with higher permission than user has.",
                    )
            else:
                raise MSGException(
                    Error.MODULES_USER_HAVE_NO_PRIVILEGES,
                    "User have no privileges to perform operation.",
                )

            return folder_profile_id


@plugin_function("gui.modules.updateData", shell=False, web=True)
def update_data(id, caption=None, content=None, be_session=None):
    """Update data of the given module

    Args:
        id (int): The id of the data
        caption (str): Caption
        content (str): The content data
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Returns:
        int: the id of the updated record.
    """

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            context = get_context()
            privileges = backend.get_user_privileges_for_data(
                db, id, context.web_handler.session_user_id
            )
            if any([p["read_only"] == 0 for p in privileges]):
                actions = []
                args = tuple()

                if caption:
                    actions.append("caption=?")
                    args += (caption,)
                if content:
                    actions.append("content=?")
                    args += (json.dumps(content),)

                db.execute(
                    f"""UPDATE data SET {",".join(actions)}
                            WHERE id=?""",
                    args + (id,),
                )
            else:
                raise MSGException(
                    Error.MODULES_USER_HAVE_NO_PRIVILEGES,
                    "User have no privileges to perform operation.",
                )

            return id


@plugin_function("gui.modules.deleteData", shell=False, web=True)
def delete_data(id, folder_id, be_session=None):
    """Deletes data

    Args:
        id (int): The id of the data
        folder_id (int): The id of the folder
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Returns:
        int: the id of the deleted record.
    """

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            context = get_context()
            backend.get_user_privileges_for_data(
                db, id, context.web_handler.session_user_id
            )

            return backend.delete_data(db, id, folder_id)


@plugin_function("gui.modules.listDataCategories", shell=False, web=True)
def list_data_categories(category_id=None, be_session=None):
    """Gets the list of available data categories and sub categories
       for the given name.

    Args:
        category_id (int): The id of the data category
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        The list of available data categories
    """
    with BackendDatabase(be_session) as db:
        if category_id is None:
            rows = db.select(
                """SELECT id, name, parent_category_id
                                FROM data_category
                                WHERE parent_category_id is NULL""",
                (),
            )
        else:
            rows = db.select(
                """WITH RECURSIVE
                                categories(id, name, parent_category_id) AS (
                                    SELECT id, name, parent_category_id FROM data_category
                                        WHERE id=?
                                    UNION ALL
                                    SELECT dc.id, dc.name, dc.parent_category_id
                                        FROM categories c
                                        JOIN data_category dc ON c.id=dc.parent_category_id
                                )
                                SELECT DISTINCT id, name, parent_category_id FROM categories""",
                (category_id,),
            )
            if not rows:
                raise MSGException(
                    Error.MODULES_INVALID_DATA_CATEGORY, "Data category does not exist."
                )

        status = db.get_last_status()
        if status["type"] != "OK":
            raise MSGException(Error.DB_ERROR, status["msg"])

        return rows


@plugin_function("gui.modules.addDataCategory", shell=False, web=True)
def add_data_category(name, parent_category_id=None, be_session=None):
    """Add a new data category to the list of available data categories for this module

    Args:
        name (str): The name of the data category
        parent_category_id (int): The id of the parent category
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        int: the id of added category.
    """

    if name.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER, f"Parameter 'name' cannot be empty."
        )

    with BackendDatabase(be_session) as db:
        search = db.execute(
            """SELECT id from data_category
                                WHERE name=?""",
            (name,),
        ).fetch_one()

        if search:
            raise MSGException(
                Error.MODULES_INVALID_DATA_CATEGORY, "Data category already exists."
            )
        else:
            res = db.execute("""SELECT MAX(id) FROM data_category""").fetch_one()
            # First 100 ids are reserved for predefined data categories
            id = res[0] + 1 if res[0] > 100 else 101
            db.execute(
                """INSERT INTO data_category (id, parent_category_id, name)
                            VALUES (?, ?, ?)""",
                (
                    id,
                    parent_category_id,
                    name,
                ),
            )
            category_id = db.get_last_row_id()

        return category_id


@plugin_function("gui.modules.removeDataCategory", shell=False, web=True)
def remove_data_category(category_id, be_session=None):
    """Remove a data category from the list of available data categories for this module

    Args:
        category_id (int): The id of the data category
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        int: the id of the removed category.
    """
    if category_id <= 100:
        raise MSGException(
            Error.MODULES_CANT_DELETE_MODULE_CATEGORY,
            "Can't delete predefined data category.",
        )

    with BackendDatabase(be_session) as db:
        res = db.execute(
            """SELECT data_category_id
                                FROM data
                                WHERE data_category_id=?
                            LIMIT 1""",
            (category_id,),
        ).fetch_all()
        if res:
            raise MSGException(
                Error.MODULES_CANT_DELETE_MODULE_CATEGORY,
                "Can't delete data category associated with data.",
            )

        res = db.execute(
            """SELECT id
                                FROM data_category
                                WHERE parent_category_id=?
                            LIMIT 1""",
            (category_id,),
        ).fetch_all()
        if res:
            raise MSGException(
                Error.MODULES_CANT_DELETE_MODULE_CATEGORY,
                "Can't delete data category associated with sub categories.",
            )

        db.execute(
            """DELETE FROM data_category
                      WHERE id=?""",
            (category_id,),
        ).fetch_one()

        if db.rows_affected == 0:
            raise MSGException(
                Error.MODULES_INVALID_DATA_CATEGORY, "Data category does not exist."
            )

        return category_id


@plugin_function("gui.modules.getDataCategoryId", shell=False, web=True)
def get_data_category_id(name, be_session=None):
    """Gets id for given name and module id.

    Args:
        name (str): The name of the data category
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        int: the id of the data category.
    """

    with BackendDatabase(be_session) as db:
        res = db.execute(
            """SELECT id
                            FROM data_category
                                WHERE name=?""",
            (name,),
        ).fetch_one()

        if not res:
            raise MSGException(
                Error.MODULES_INVALID_DATA_CATEGORY, "Data category does not exist."
            )

        return res["id"]


@plugin_function("gui.modules.createProfileDataTree", shell=False, web=True)
def create_profile_data_tree(tree_identifier, profile_id=None, be_session=None):
    """Creates the profile data tree for the given tree identifier and profile id.

    Args:
        tree_identifier (str): The identifier of the tree
        profile_id (int): The id of profile
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        int: the id of the root folder.
    """

    if tree_identifier.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'tree_identifier' cannot be empty.",
        )

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            context = get_context()
            root_folder_id = backend.create_profile_data_tree(
                db,
                tree_identifier,
                (
                    profile_id
                    if profile_id
                    else context.web_handler.session_active_profile_id
                ),
            )

            return root_folder_id


@plugin_function("gui.modules.getProfileDataTree", shell=False, web=True)
def get_profile_data_tree(tree_identifier, profile_id=None, be_session=None):
    """Gets the profile data tree for the given tree identifier and profile id.

    Args:
        tree_identifier (str): The identifier of the tree
        profile_id (int): The id of profile
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        list: the list of all folders in data tree.
    """

    with BackendDatabase(be_session) as db:
        context = get_context()
        root_folder_id = backend.get_root_folder_id(
            db,
            tree_identifier,
            "profile",
            profile_id if profile_id else context.web_handler.session_active_profile_id,
        )

        return db.select(backend.FOLDERS_TREE_SQL, (root_folder_id,))


@plugin_function("gui.modules.createUserGroupDataTree", shell=False, web=True)
def create_user_group_data_tree(tree_identifier, user_group_id=None, be_session=None):
    """Creates the user group data tree for the given tree identifier and user group id.

    Args:
        tree_identifier (str): The identifier of the tree
        user_group_id (int): The id of user group
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        int: the id of the root folder.
    """

    if tree_identifier.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'tree_identifier' cannot be empty.",
        )

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            context = get_context()
            root_folder_id = backend.create_user_group_data_tree(
                db,
                tree_identifier,
                (
                    user_group_id
                    if user_group_id
                    else context.web_handler.user_personal_group_id
                ),
            )

            return root_folder_id


@plugin_function("gui.modules.getUserGroupDataTree", shell=False, web=True)
def get_user_group_data_tree(tree_identifier, user_group_id=None, be_session=None):
    """Gets the user group data tree for the given tree identifier and user group id.

    Args:
        tree_identifier (str): The identifier of the tree
        user_group_id (int): The id of user group
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        list: the list of all folders in data tree.
    """

    with BackendDatabase(be_session) as db:
        context = get_context()
        root_folder_id = backend.get_root_folder_id(
            db,
            tree_identifier,
            "group",
            (
                user_group_id
                if user_group_id
                else context.web_handler.user_personal_group_id
            ),
        )

        return db.select(backend.FOLDERS_TREE_SQL, (root_folder_id,))


@plugin_function("gui.modules.getProfileTreeIdentifiers", shell=False, web=True)
def get_profile_tree_identifiers(profile_id=None, be_session=None):
    """Gets the tree identifiers associated with the given profile.

    Args:
        profile_id (int): The id of profile
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        list: the list of tree identifiers.
    """

    with BackendDatabase(be_session) as db:
        context = get_context()
        return db.select(
            """SELECT tree_identifier
                            FROM data_profile_tree
                            WHERE profile_id=?""",
            (
                (
                    profile_id
                    if profile_id
                    else context.web_handler.session_active_profile_id
                ),
            ),
        )


@plugin_function("gui.modules.moveData", shell=False, web=True)
def move_data(
    id, tree_identifier, linked_to, link_id, source_path, target_path, be_session=None
):
    """Moves data from source path to target path.

    Args:
        id (int): The id of the data
        tree_identifier (str): The identifier of the tree
        linked_to (str): ['profile'|'group']
        link_id (int): The profile id or the group id (depending on linked_to)
        source_path (str): The source folder path f.e. "/scripts/server1"
        target_path (str): The target folder path f.e. "/scripts/server2"
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Returns:
        int: the id of the moved record.
    """
    if linked_to not in ["profile", "group"]:
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'linked_to' can only take value 'profile' or 'group'.",
        )

    if tree_identifier.strip() == "":
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameter 'tree_identifier' cannot be empty.",
        )

    if source_path.strip() == "" or source_path.strip() == "/":
        source_path = None

    if target_path.strip() == "" or target_path.strip() == "/":
        target_path = None

    if source_path == target_path:
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            f"Parameters 'source_path' and 'target_path' are the same.",
        )

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            root_folder_id = backend.get_root_folder_id(
                db, tree_identifier, linked_to, link_id
            )

            if root_folder_id is None:
                raise MSGException(
                    Error.CORE_INVALID_PARAMETER,
                    f"Cannot find root folder id for the given 'tree_identifier'.",
                )

            source_folder_id = backend.get_folder_id(db, root_folder_id, source_path)
            if source_folder_id is None:
                raise MSGException(
                    Error.CORE_INVALID_PARAMETER,
                    f"Cannot find the given 'source_path'.",
                )
            target_folder_id = backend.get_folder_id(db, root_folder_id, target_path)

            if target_folder_id is None:
                target_folder_id, _ = backend.create_folder(
                    db, target_path, root_folder_id
                )

            backend.add_data_to_folder(db, id, target_folder_id, read_only=0)

            return backend.delete_data(db, id, source_folder_id)
