# 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

import json

from mysqlsh.plugin_manager import plugin_function  # pylint: disable=no-name-in-module

import gui_plugin.core.Error as Error
from gui_plugin.core.backend_db import db_connections
from gui_plugin.core.Db import BackendDatabase, BackendTransaction
from gui_plugin.core.dbms import DbSessionFactory
from gui_plugin.core.Error import MSGException
from gui_plugin.core.modules.DbModuleSession import DbModuleSession


@plugin_function("gui.dbConnections.addDbConnection", cli=True, shell=True, web=True)
def add_db_connection(profile_id, connection, folder_path_id=None, be_session=None):
    """Add a new db_connection and associate the connection with a profile

    Args:
        profile_id (int): The id of the profile
        connection (dict): The connection information as a dict, e.g. {
            "db_type": "MySQL",
            "caption": "Local MySQL Server",
            "description": "Connection to local MySQL Server on 3306",
            "options": {
                "uri": "mysql://mike@localhost:3306/test",
                "password": "myPassword2BeStoredEncrypted"
            },
            "settings": {
                "defaultEditor": "DB Notebook",
            }}
        folder_path_id (int): The id of the folder path used for grouping and nesting connections, optional
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Allowed options for connection:
        db_type (str): The db type name
        caption (str): A name for this connection
        description (str): A longer description for this connection
        options (dict): The options specific for the current database type
        settings (dict): The additional settings for the given connection

    Returns:
        tuple: A tuple containing (connection_id, folder_path_id, index)
    """
    # Verify connection parameters
    if (
        not "caption" in connection
        or not isinstance(connection["caption"], str)
        or connection["caption"].strip() == ""
    ):
        raise MSGException(
            Error.CORE_INVALID_PARAMETER, "The connection must contain valid caption."
        )

    if (
        not "db_type" in connection
        or not isinstance(connection["db_type"], str)
        or not connection["db_type"].upper() in ["MYSQL", "SQLITE"]
    ):
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            "The connection must contain valid database type.",
        )

    with BackendDatabase(be_session) as db:
        # TODO: Encrypt stored password. The password will be inside "options", but we
        # don't know the structure of that object. Maybe the FE should pass the keys that should
        # be encrypted.

        with BackendTransaction(db):
            # Insert new db_connection
            db.execute(
                """INSERT INTO db_connection(
                db_type, caption, description, options, settings)
                VALUES(?, ?, ?, ?, ?)""",
                (
                    connection.get("db_type", "MySQL"),
                    connection.get("caption", "New Connection"),
                    connection.get("description", ""),
                    json.dumps(connection.get("options", {})),
                    json.dumps(connection.get("settings", {})),
                ),
            )

            connection_id = db.get_last_row_id()

            if folder_path_id is None:
                folder_path_id = 1  # Root folder

            index = db_connections.get_next_connection_index(
                db, profile_id, folder_path_id
            )

            # Insert n:m profile_has_db_connection to associate the connection with
            # a profile
            db.execute(
                """INSERT INTO profile_has_db_connection(
                profile_id, db_connection_id, folder_path_id, `index`)
                VALUES(?, ?, ?, ?)""",
                (profile_id, connection_id, folder_path_id, index),
            )

    return (connection_id, folder_path_id, index)


@plugin_function("gui.dbConnections.updateDbConnection", shell=True, web=True)
def update_db_connection(
    profile_id, connection_id, connection=None, folder_path_id=None, be_session=None
):
    """Update the data for a database connection

    Args:
        profile_id (int): The id of the profile
        connection_id (int): The id of the connection to update
        connection (dict): The connection information as a dict, optional, e.g. {
                "caption": "Local MySQL Server",
                "description": "Connection to local MySQL Server on 3306",
                "options": {
                    "uri": "mysql://mike@localhost:3306/test",
                    "password": "myPassword2BeStoredEncrypted"
                },
                "settings": {
                    "defaultEditor": "DB Notebook",
                }
            }
        folder_path_id (int): The id of the folder path used for grouping and nesting connections, optional
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Allowed options for connection:
        db_type (str): The db type name
        caption (str): A name for this connection
        description (str): A longer description for this connection
        options (dict): The options specific for the current database type
        settings (dict): The additional settings for the given connection

    Returns:
        None
    """

    if connection is None and folder_path_id is None:
        raise MSGException(
            Error.CORE_INVALID_PARAMETER,
            "At least one of connection or folder_path_id must be provided.",
        )

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            if connection is not None:
                if "db_type" in connection:
                    db.execute(
                        "UPDATE db_connection SET db_type=? WHERE id=?",
                        (connection["db_type"], connection_id),
                    )
                if "caption" in connection:
                    db.execute(
                        "UPDATE db_connection SET caption=? WHERE id=?",
                        (connection["caption"], connection_id),
                    )
                if "description" in connection:
                    db.execute(
                        "UPDATE db_connection SET description=? WHERE id=?",
                        (connection["description"], connection_id),
                    )
                if "options" in connection:
                    db.execute(
                        "UPDATE db_connection SET options=? WHERE id=?",
                        (json.dumps(connection["options"]), connection_id),
                    )
                if "settings" in connection:
                    db.execute(
                        "UPDATE db_connection SET settings=? WHERE id=?",
                        (json.dumps(connection["settings"]), connection_id),
                    )
            if folder_path_id is not None:
                index = db_connections.get_next_connection_index(
                    db, profile_id, folder_path_id
                )
                db.execute(
                    """UPDATE profile_has_db_connection
                              SET folder_path_id=?
                              WHERE profile_id=? AND db_connection_id=?""",
                    (folder_path_id, profile_id, connection_id),
                )
                db.execute(
                    """UPDATE profile_has_db_connection
                              SET `index`=?
                              WHERE profile_id=? AND folder_path_id=? AND db_connection_id=?""",
                    (index, profile_id, folder_path_id, connection_id),
                )


@plugin_function("gui.dbConnections.removeDbConnection", cli=True, shell=True, web=True)
def remove_db_connection(profile_id, connection_id, be_session=None):
    """Remove a db_connection by disassociating the connection from a profile

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

    Returns:
        None
    """

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):

            # Remove the connection for this profile
            db.execute(
                """DELETE FROM profile_has_db_connection WHERE
                profile_id=? AND db_connection_id=?""",
                (profile_id, connection_id),
            )

            # Check if some other profile is still using the connection
            result = db.select(
                """SELECT COUNT(*) as cnt FROM profile_has_db_connection
                WHERE db_connection_id=?""",
                (connection_id,),
            )

            # If no other profile is using this connection, remove it.
            if result[0]["cnt"] == 0:
                db.execute(
                    """DELETE FROM db_connection
                    WHERE id=?""",
                    (connection_id,),
                )


@plugin_function("gui.dbConnections.listDbConnections", cli=True, shell=True, web=True)
def list_db_connections(profile_id, folder_path_id=None, be_session=None):
    """Lists the db_connections for the given profile

    Args:
        profile_id (int): The id of the profile
        folder_path_id (int): The folder path ID used for grouping and nesting
            connections, optional
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.

    Returns:
        list: the list of connections
    """
    with BackendDatabase(be_session) as db:
        return db.select(
            """SELECT dc.id, p_dc.folder_path_id, dc.caption,
            dc.description, dc.db_type, dc.options, dc.settings, p_dc.`index`
            FROM profile_has_db_connection p_dc
                LEFT JOIN db_connection dc ON
                    p_dc.db_connection_id = dc.id
            WHERE p_dc.profile_id = ? AND p_dc.folder_path_id = ?""",
            (profile_id, "1" if folder_path_id is None else folder_path_id),
        )


@plugin_function("gui.dbConnections.getDbConnection", cli=True, shell=True, web=True)
def get_db_connection(db_connection_id, be_session=None):
    """Get the db_connection

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

    Returns:
        dict: The db connection
    """
    with BackendDatabase(be_session) as db:
        return db.select(
            "SELECT * FROM db_connection WHERE id = ?", (db_connection_id,)
        )[0]


@plugin_function("gui.dbConnections.getDbTypes", cli=True, shell=True, web=True)
def get_db_types():
    """Get the list of db_types

    Returns:
        list: The list of db types
    """

    return DbSessionFactory.getSessionTypes()


@plugin_function("gui.dbConnections.setCredential", cli=True, shell=True, web=True)
def set_credential(url, password):
    """Set the password of a db_connection url

    Args:
        url (str): The URL needs to be in the following form
            user@(host[:port]|socket).
        password (str): The password

    Returns:
        None
    """

    import mysqlsh

    mysqlsh.globals.shell.store_credential(url, password)


@plugin_function("gui.dbConnections.deleteCredential", cli=True, shell=True, web=True)
def delete_credential(url):
    """Deletes the password of a db_connection url

    Args:
        url (str): The URL needs to be in the following form
            user@(host[:port]|socket).

    Returns:
        None
    """

    import mysqlsh

    mysqlsh.globals.shell.delete_credential(url)


@plugin_function("gui.dbConnections.listCredentials", cli=True, shell=True, web=True)
def list_credentials():
    """Lists the db_connection urls that have a password stored

    Returns:
        list: The list of db_connection urls that have a password stored
    """

    import mysqlsh

    return mysqlsh.globals.shell.list_credentials()


@plugin_function("gui.dbConnections.testConnection", cli=True, shell=True, web=True)
def test_connection(connection, password=None):
    """Opens test connection

    Args:
        connection (object): The id of the db_connection or connection information
        password (str): The password to use when opening the connection. If not supplied, then use the password defined in the database options.

    Allowed options for connection:
        db_type (str): The db type name
        options (dict): The options specific for the current database type

    Returns:
        None
    """
    new_session = DbModuleSession()
    new_session.open_connection(connection, password)
    if (
        not new_session.completion_event.has_errors
        and password is None
        and not "password" in connection["options"]
    ):
        return {"module_session_id": new_session.module_session_id}

    new_session.completion_event.wait()

    if not new_session.completion_event.has_errors:
        new_session.close()


@plugin_function("gui.dbConnections.moveConnection", cli=True, shell=True, web=True)
def move_connection(
    profile_id,
    folder_id,
    connection_id_to_move,
    connection_id_offset,
    before=False,
    be_session=None,
):
    """Updates the connections sort order for the given profile

    Args:
        profile_id (int): The id of the profile
        folder_id (int): The folder id used for grouping and nesting connections
        connection_id_to_move (int): The id of the connection to move
        connection_id_offset (int): The id of the offset connection
        before (bool): Indicates whether connection_id_to_move should be moved before connection_id_offset or after
        be_session (object):  A session to the GUI backend database
            where the operation will be performed.


    Returns:
        None
    """

    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            index_to_move = db_connections.get_connection_index(
                db, profile_id, folder_id, connection_id_to_move
            )
            index_offset = db_connections.get_connection_index(
                db, profile_id, folder_id, connection_id_offset
            )

            if index_to_move > index_offset:
                index = index_offset if before else index_offset + 1
                db.execute(
                    """UPDATE profile_has_db_connection
                          SET `index`=`index`+1
                          WHERE profile_id=? AND folder_path_id=? AND `index`>=? AND `index`<?""",
                    (profile_id, folder_id, index, index_to_move),
                )
                db.execute(
                    """UPDATE folder_path
                          SET `index`=`index`+1
                          WHERE `index`>=? AND `index`<?""",
                    (index, index_to_move),
                )
            else:
                index = index_offset - 1 if before else index_offset
                db.execute(
                    """UPDATE profile_has_db_connection
                          SET `index`=`index`-1
                          WHERE profile_id=? AND folder_path_id=? AND `index`<=? AND `index`>?""",
                    (profile_id, folder_id, index, index_to_move),
                )
                db.execute(
                    """UPDATE folder_path
                          SET `index`=`index`-1
                          WHERE `index`<=? AND `index`>?""",
                    (index, index_to_move),
                )

            db.execute(
                """UPDATE profile_has_db_connection
                          SET `index`=?
                          WHERE profile_id=? AND folder_path_id=? AND db_connection_id=?""",
                (index, profile_id, folder_id, connection_id_to_move),
            )


@plugin_function("gui.dbConnections.addFolderPath", cli=True, shell=True, web=True)
def add_folder_path(
    profile_id, caption, settings=None, parent_folder_id=None, be_session=None
):
    """Add a new folder path

    Args:
        profile_id (int): The id of the profile
        caption (str): The caption of the folder
        settings (dict): Additional settings for the folder, optional
        parent_folder_id (int): The id of the parent folder, optional
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Allowed options for settings:
        color (str): The color of the tile

    Returns:
        int: The folder path ID
    """
    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            if parent_folder_id is None:
                parent_folder_id = 1  # Root folder
            folder_path_id = db_connections.folder_exists(db, caption, parent_folder_id)
            if folder_path_id is None:
                index = db_connections.get_next_connection_index(
                    db, profile_id, parent_folder_id
                )
                db.execute(
                    """INSERT INTO folder_path (parent_folder_id, caption, `index`, settings)
                            VALUES (?, ?, ?, ?)""",
                    (parent_folder_id, caption, index, json.dumps(settings or {})),
                )
                folder_path_id = db.get_last_row_id()
    return db.select("""SELECT * FROM folder_path WHERE id=?""", (folder_path_id,))[0]


@plugin_function("gui.dbConnections.removeFolderPath", cli=True, shell=True, web=True)
def remove_folder_path(folder_path_id, be_session=None):
    """Remove a folder path

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

    Returns:
        None
    """
    with BackendDatabase(be_session) as db:
        db.execute("""DELETE FROM folder_path WHERE id=?""", (folder_path_id,))


@plugin_function("gui.dbConnections.removeEmptyFolders", cli=True, shell=True, web=True)
def remove_empty_folders(be_session=None):
    """Remove all empty folders

    Args:
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Returns:
        None
    """
    with BackendDatabase(be_session) as db:
        with BackendTransaction(db):
            no_more_empty_folders = False
            while not no_more_empty_folders:
                db.execute("""DELETE FROM folder_path WHERE id != 1 AND id NOT IN (
                    SELECT parent_folder_id FROM folder_path WHERE parent_folder_id IS NOT NULL
                    UNION
                    SELECT folder_path_id FROM profile_has_db_connection)""")

                result = db.select(
                    """SELECT COUNT(*) as cnt FROM folder_path WHERE id != 1 AND id NOT IN (
                    SELECT parent_folder_id FROM folder_path WHERE parent_folder_id IS NOT NULL
                    UNION
                    SELECT folder_path_id FROM profile_has_db_connection)"""
                )

                no_more_empty_folders = result[0]["cnt"] == 0


@plugin_function("gui.dbConnections.renameFolderPath", cli=True, shell=True, web=True)
def rename_folder_path(folder_path_id, new_caption, be_session=None):
    """Rename a folder path

    Args:
        folder_path_id (int): The id of the folder path to rename
        new_caption (str): The new caption for the folder path
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Returns:
        None
    """
    with BackendDatabase(be_session) as db:
        db.execute(
            """UPDATE folder_path SET caption=? WHERE id=?""",
            (new_caption, folder_path_id),
        )


@plugin_function("gui.dbConnections.moveFolder", cli=True, shell=True, web=True)
def move_folder(folder_path_id, new_parent_folder_id, be_session=None):
    """Move a folder path to a new parent folder

    Args:
        folder_path_id (int): The id of the folder path to move
        new_parent_folder_id (int): The id of the new parent folder
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Returns:
        None
    """
    with BackendDatabase(be_session) as db:
        db.execute(
            """UPDATE folder_path SET parent_folder_id=? WHERE id=?""",
            (new_parent_folder_id, folder_path_id),
        )


@plugin_function("gui.dbConnections.listFolderPaths", cli=True, shell=True, web=True)
def list_folder_paths(parent_folder_id=None, recursive=False, be_session=None):
    """List folder paths

    Args:
        parent_folder_id (int): The id of the parent folder to list child folders, optional
        recursive (bool): If True, returns all nested subfolders; if False, returns only immediate children
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Returns:
        list: The list of folder paths
    """
    with BackendDatabase(be_session) as db:
        if not recursive:
            if parent_folder_id is None:
                return db.select(
                    """SELECT * FROM folder_path WHERE parent_folder_id IS NULL ORDER BY `index` ASC"""
                )
            else:
                return db.select(
                    """SELECT * FROM folder_path WHERE parent_folder_id=? ORDER BY `index` ASC""",
                    (parent_folder_id,),
                )
        else:
            result = []

            def get_folders_recursive(parent_id):
                if parent_id is None:
                    folders = db.select(
                        """SELECT * FROM folder_path WHERE parent_folder_id IS NULL ORDER BY `index` ASC"""
                    )
                else:
                    folders = db.select(
                        """SELECT * FROM folder_path WHERE parent_folder_id=? ORDER BY `index` ASC""",
                        (parent_id,),
                    )

                result.extend(folders)

                for folder in folders:
                    get_folders_recursive(folder["id"])

            get_folders_recursive(parent_folder_id)

            return result


@plugin_function("gui.dbConnections.listAll", cli=True, shell=True, web=True)
def list_all(profile_id, folder_id=None, be_session=None):
    """Lists all connections and folder paths for the given profile and folder

    Args:
        profile_id (int): The id of the profile
        folder_id (int): The id of the folder, optional (for None use root folder)
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Returns:
        list: A list of dictionaries containing connections and folders sorted by index
    """
    with BackendDatabase(be_session) as db:
        combined_list = db.select(
            """
            SELECT dc.id, dc.caption, dc.description, dc.db_type, dc.options, dc.settings, p_dc.`index`, 'connection' AS type
                FROM profile_has_db_connection p_dc
                LEFT JOIN db_connection dc ON p_dc.db_connection_id = dc.id
                WHERE p_dc.profile_id = ? AND p_dc.folder_path_id = ?
            UNION ALL
            SELECT fp.id, fp.caption, NULL AS description, NULL AS db_type, NULL AS options, fp.settings, fp.`index`, 'folder' AS type
                FROM folder_path fp
                WHERE fp.parent_folder_id = ?
                ORDER BY `index` ASC
        """,
            (
                profile_id,
                folder_id if folder_id is not None else 1,
                folder_id if folder_id is not None else 1,
            ),
        )

    return combined_list


@plugin_function(
    "gui.dbConnections.updateFolderSettings", cli=True, shell=True, web=True
)
def update_folder_settings(folder_path_id, new_settings, be_session=None):
    """Rename a folder path

    Args:
        folder_path_id (int): The id of the folder path to rename
        new_settings (dict): The new settings for the folder path
        be_session (object): A session to the GUI backend database where the operation will be performed.

    Allowed options for new_settings:
        color (str): The color of the tile

    Returns:
        None
    """
    with BackendDatabase(be_session) as db:
        db.execute(
            """UPDATE folder_path SET settings=? WHERE id=?""",
            (new_settings, folder_path_id),
        )


@plugin_function("gui.dbConnections.getFolder", cli=True, shell=True, web=True)
def get_folder(folder_path_id, be_session=None):
    """Get folder

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

    Returns:
        dict: The folder
    """
    with BackendDatabase(be_session) as db:
        return db.select("SELECT * FROM folder_path WHERE id = ?", (folder_path_id,))[0]
