##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

"""A blueprint module implementing the workspace."""
import json
import config
from config import PG_DEFAULT_DRIVER
from flask import request, current_app
from pgadmin.user_login_check import pga_login_required
from flask_babel import gettext
from flask_security import current_user
from pgadmin.utils import PgAdminModule
from pgadmin.model import db, Server
from pgadmin.utils.server_access import get_server
from pgadmin.utils.driver import get_driver
from pgadmin.utils.ajax import bad_request, make_json_response
from pgadmin.browser.server_groups.servers.utils import (
    is_valid_ipaddress, convert_connection_parameter, check_ssl_fields)
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
from pgadmin.browser.server_groups.servers.utils import (
    disconnect_from_all_servers, delete_adhoc_servers)
from pgadmin.tools.psql import get_open_psql_connections

MODULE_NAME = 'workspace'


class WorkspaceModule(PgAdminModule):

    def get_exposed_url_endpoints(self):
        """
        Returns:
            list: URL endpoints for Workspace module
        """
        return [
            'workspace.adhoc_connect_server',
            'workspace.layout_changed'
        ]


blueprint = WorkspaceModule(MODULE_NAME, __name__,
                            url_prefix='/misc/workspace')


@blueprint.route("/")
@pga_login_required
def index():
    return bad_request(
        errormsg=gettext('This URL cannot be requested directly.')
    )


@blueprint.route(
    '/adhoc_connect_server',
    methods=["POST"],
    endpoint="adhoc_connect_server"
)
@pga_login_required
def adhoc_connect_server():
    required_args = ['server_name', 'did']

    data = request.form if request.form else json.loads(
        request.data
    )

    # Loop through data and if found any value is blank string then
    # convert it to None as after porting into React, from frontend
    # '' blank string is coming as a value instead of null.
    for item in data:
        if data[item] == '':
            data[item] = None

    # Some fields can be provided with service file so they are optional
    if 'service' in data and not data['service']:
        required_args.extend([
            'host',
            'port',
            'user'
        ])

    for arg in required_args:
        if arg not in data:
            return make_json_response(
                status=410,
                success=0,
                errormsg=gettext(
                    "Could not find the required parameter ({})."
                ).format(arg)
            )

    # convert_connection_parameter() is bidirectional. For a save path we
    # want the storage shape (dict). If the input is already a dict, keep
    # it; otherwise assume frontend list shape and convert.
    raw_params = data.get('connection_params', [])
    if isinstance(raw_params, dict):
        connection_params = raw_params
    else:
        connection_params = convert_connection_parameter(raw_params)

    if connection_params is not None:
        if 'hostaddr' in connection_params and \
                not is_valid_ipaddress(connection_params['hostaddr']):
            return make_json_response(
                success=0,
                status=400,
                errormsg=gettext('Not a valid Host address')
            )

        # To check ssl configuration
        _, connection_params = check_ssl_fields(connection_params)
        # set the connection params again in the data
        if 'connection_params' in data:
            data['connection_params'] = connection_params

    # Fetch all the new data in case of non-existing servers
    new_host = data.get('host', None)
    new_port = int(data.get('port', 0))
    new_db = data.get('database_name', None)
    if new_db is None:
        new_db = data.get('did')
    new_username = data.get('user')
    new_role = data.get('role', None)
    new_server_name = data.get('server_name', None)
    new_service = data.get('service', None)

    try:
        server = None
        if config.CONFIG_DATABASE_URI is not None and \
                len(config.CONFIG_DATABASE_URI) > 0:
            # Filter out all the servers with the below combination.
            servers = Server.query.filter_by(host=new_host,
                                             port=new_port,
                                             maintenance_db=new_db,
                                             username=new_username,
                                             name=new_server_name,
                                             role=new_role,
                                             service=new_service,
                                             user_id=current_user.id
                                             ).all()

            # If found matching servers then compare the connection_params as
            # with external database (PostgreSQL) comparing two json objects
            # are not supported.
            for existing_server in servers:
                if existing_server.connection_params == connection_params:
                    server = existing_server
                    break
        else:
            server = Server.query.filter_by(
                host=new_host, port=new_port,
                maintenance_db=new_db,
                username=new_username,
                name=new_server_name,
                role=new_role,
                service=new_service,
                connection_params=connection_params,
                user_id=current_user.id
            ).first()

        # If server is none then no server with the above combination is found.
        if server is None:
            # Check if sid is present in data if it is then used that sid.
            if ('sid' in data and data['sid'] is not None and
                    int(data['sid']) > 0):
                server = get_server(data['sid'])
                if server is None:
                    return bad_request(gettext(
                        "Could not find the required server."
                    ))

                # Clone the server object
                server = server.clone()

                # Replace the following with the new/changed value.
                server.maintenance_db = new_db
                server.username = new_username
                server.role = new_role
                server.connection_params = connection_params
                server.is_adhoc = 1

                db.session.add(server)
                db.session.commit()
            else:
                server = Server(
                    user_id=current_user.id,
                    servergroup_id=data.get('gid', 1),
                    name=new_server_name,
                    host=new_host,
                    port=new_port,
                    maintenance_db=new_db,
                    username=new_username,
                    role=new_role,
                    service=new_service,
                    connection_params=connection_params,
                    is_adhoc=1
                )
                db.session.add(server)
                db.session.commit()

        view = SchemaDiffRegistry.get_node_view('server')
        return view.connect(1, server.id, is_qt=False, server=server)
    except Exception as e:
        current_app.logger.exception(e)
        return make_json_response(
            status=410,
            success=0,
            errormsg=str(e)
        )


@blueprint.route(
    '/layout_changed',
    methods=["DELETE"],
    endpoint="layout_changed"
)
@pga_login_required
def layout_changed():
    # if layout is changed from 'Workspace' to 'Classic', disconnect all
    # servers.
    disconnect_from_all_servers()
    delete_adhoc_servers()

    return make_json_response(status=200)


def check_and_delete_adhoc_server(sid):
    """
    This function is used to check for adhoc server and if all Query Tool
    and PSQL connections are closed then delete that server.
    """
    server = get_server(sid)
    if server is None:
        # Server may be deleted or inaccessible; still attempt
        # best-effort cleanup of adhoc state.
        delete_adhoc_servers(sid)
        return
    if not server.is_adhoc:
        return

    # Check PSQL connections. If more connections are open for
    # the given sid return from the function.
    psql_connections = get_open_psql_connections()
    if sid in psql_connections.values():
        return

    # Check Query Tool connections for the given sid
    manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
    for key, value in manager.connections.items():
        if key.startswith('CONN') and value.connected():
            return

    # Assumption at this point all the Query Tool and PSQL connections
    # is closed, so now we can release the manager
    manager.release()

    # Delete the adhoc server from the pgadmin database
    delete_adhoc_servers(sid)
