# Copyright (c) 2021, 2025, 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

"""Sub-Module for managing MRS services"""

# cSpell:ignore mysqlsh, mrs

from mysqlsh.plugin_manager import plugin_function
import mrs_plugin.lib as lib
from .interactive import resolve_service, resolve_options, resolve_file_path, resolve_overwrite_file, service_query_selection
from pathlib import Path
import os
import shutil
import json
import datetime
import base64


def verify_value_keys(**kwargs):
    for key in kwargs["value"].keys():
        if key not in ["url_host_id",  "url_context_root",  "url_protocol", "url_host_name",
                       "enabled",  "comments", "options",
                       "auth_path", "auth_completed_url", "auth_completed_url_validation",
                       "auth_completed_page_content", "auth_apps", "metadata",
                       "in_development", "published", "name"] and key != "delete":
            raise Exception(f"Attempting to change an invalid service value.")


def resolve_service_ids(**kwargs):
    value = kwargs.get("value")
    session = kwargs.get("session")

    service_id = kwargs.pop("service_id", None)
    url_context_root = kwargs.pop("url_context_root", None)
    url_host_name = kwargs.pop("url_host_name", None)
    interactive = lib.core.get_interactive_default()
    allow_multi_select = kwargs.pop("allow_multi_select", False)
    kwargs.pop("url_protocol", None)

    kwargs["service_ids"] = []

    if service_id is not None:
        kwargs["service_ids"] = [service_id]
    else:
        # Get the right service_id(s) if service_id is not given
        if not url_context_root:
            # Check if there already is at least one service
            rows = lib.core.select(table="service",
                                   cols=["COUNT(*) AS service_count",
                                         "MAX(id) AS id"]
                                   ).exec(session).items
            if len(rows) == 0 or rows[0]["service_count"] == 0:
                Exception("No service available.")

            # If there are more services, let the user select one or all
            if interactive:
                if allow_multi_select:
                    caption = ("Please select a service index, type "
                               "'hostname/root_context' or type '*' "
                               "to select all: ")
                else:
                    caption = ("Please select a service index or type "
                               "'hostname/root_context'")

                services = lib.services.get_services(session=session)
                selection = lib.core.prompt_for_list_item(
                    item_list=services,
                    prompt_caption=caption,
                    item_name_property="host_ctx",
                    given_value=None,
                    print_list=True,
                    allow_multi_select=allow_multi_select)
                if not selection or selection == "":
                    raise ValueError("Operation cancelled.")

                if allow_multi_select:
                    kwargs["service_ids"] = [item["id"] for item in selection]
                else:
                    kwargs["service_ids"].append(selection["id"])
        else:
            # Lookup the service id
            res = session.run_sql(
                """
                SELECT se.id FROM `mysql_rest_service_metadata`.`service` se
                    LEFT JOIN `mysql_rest_service_metadata`.url_host h
                        ON se.url_host_id = h.id
                WHERE h.name = ? AND se.url_context_root = ?
                """,
                [url_host_name if url_host_name else "", url_context_root])
            row = res.fetch_one()
            if row:
                kwargs["service_ids"].append(row.get_field("id"))

    if len(kwargs["service_ids"]) == 0:
        raise ValueError("The specified service was not found.")

    for service_id in kwargs["service_ids"]:
        service = lib.services.get_service(
            service_id=service_id, session=session)

        # Determine changes in the url_context_root for this service
        if value is not None and "url_context_root" in value:
            url_ctx_root = value["url_context_root"]

            if interactive and not url_ctx_root:
                url_ctx_root = lib.services.prompt_for_url_context_root(
                    default=service.get('url_context_root'))

            # If the context root has changed, check if the new one is valid
            if service["url_context_root"] != url_ctx_root:
                if (not url_ctx_root or not url_ctx_root.startswith('/')):
                    raise ValueError(
                        "The url_context_root has to start with '/'.")

    return kwargs


def resolve_url_context_root(required=False, **kwargs):
    url_context_root = kwargs.get("url_context_root")
    if url_context_root is None and lib.core.get_interactive_default():
        url_context_root = kwargs["url_context_root"] = lib.services.prompt_for_url_context_root(
        )

    if required and url_context_root is None:
        raise Exception("No context path given. Operation cancelled.")
    if url_context_root is not None and not url_context_root.startswith('/'):
        raise Exception(
            f"The url_context_root [{url_context_root}] has to start with '/'.")

    return kwargs


def resolve_url_host_name(required=False, **kwargs):
    url_host_name = kwargs.get("url_host_name")

    if lib.core.get_interactive_default():
        if url_host_name is None:
            url_host_name = lib.core.prompt(
                "Please enter the host name for this service (e.g. "
                "None or localhost) [None]: ",
                {'defaultValue': 'None'}).strip()

    if url_host_name and url_host_name.lower() == 'none':
        url_host_name = None

    kwargs["url_host_name"] = url_host_name

    return kwargs


def resolve_url_protocol(**kwargs):
    if kwargs.get("url_protocol") is None:
        if lib.core.get_interactive_default():
            kwargs["url_protocol"] = lib.services.prompt_for_service_protocol()
        else:
            kwargs["url_protocol"] = ["HTTP", "HTTPS"]

    return kwargs


def resolve_comments(**kwargs):
    if lib.core.get_interactive_default():
        if kwargs.get("comments") is None:
            kwargs["comments"] = lib.core.prompt_for_comments()

    return kwargs


def call_update_service(op_text, **kwargs):

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        kwargs["session"] = session
        kwargs = resolve_service_ids(**kwargs)

        with lib.core.MrsDbTransaction(session):
            lib.services.update_services(**kwargs)

            if lib.core.get_interactive_result():
                if len(kwargs['service_ids']) == 1:
                    return f"The service has been {op_text}."
                return f"The services have been {op_text}."
            return True
    return False


def file_name_using_language_convention(name, sdk_language):
    if sdk_language == "Python":
        return lib.core.convert_to_snake_case(name)
    return name


def default_copyright_header(sdk_language):
    header = "Copyright (c) 2023, 2025, Oracle and/or its affiliates."

    if sdk_language == "TypeScript":
        return f"// {header}"

    if sdk_language == "Python":
        return f"# {header}"


def generate_create_statement(**kwargs):
    lib.core.convert_ids_to_binary(["service_id"], kwargs)
    lib.core.try_convert_ids_to_binary(["service"], kwargs)

    include_database_endpoints = kwargs.get("include_database_endpoints", False)
    include_static_endpoints = kwargs.get("include_static_endpoints", False)
    include_dynamic_endpoints = kwargs.get("include_dynamic_endpoints", False)
    service_query = service_query_selection(**kwargs)

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        service = resolve_service(session, service_query=service_query)

        if service is None:
            raise ValueError("The specified service was not found.")

        return lib.services.get_service_create_statement(
            session, service,
            include_database_endpoints, include_static_endpoints, include_dynamic_endpoints)


def store_create_statement(**kwargs):
    lib.core.convert_ids_to_binary(["service_id"], kwargs)
    lib.core.try_convert_ids_to_binary(["service"], kwargs)

    include_database_endpoints = kwargs.get("include_database_endpoints", False)
    include_static_endpoints = kwargs.get("include_static_endpoints", False)
    include_dynamic_endpoints = kwargs.get("include_dynamic_endpoints", False)
    service_query = service_query_selection(**kwargs)
    file_path = kwargs.get("file_path")
    zip = kwargs.get("zip", False)



    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        service = resolve_service(session, service_query=service_query)

        if service is None:
            raise ValueError("The specified service was not found.")

        lib.services.store_service_create_statement(session, service,
                                                    file_path, zip,
                                                    include_database_endpoints, include_static_endpoints, include_dynamic_endpoints)


@plugin_function('mrs.add.service', shell=True, cli=True, web=True)
def add_service(**kwargs):
    """Adds a new MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        enabled (bool): Whether the new service should be enabled
        url_protocol (list): The protocols supported by this service
        comments (str): Comments about the service
        options (dict): Options for the service
        auth_path (str): The authentication path
        auth_completed_url (str): The redirection URL called after authentication
        auth_completed_url_validation (str): The regular expression that validates the
            app redirection URL specified by the /login?onCompletionRedirect parameter
        auth_completed_page_content (str): The custom page content to use of the
            authentication completed page
        metadata (dict): Metadata of the service
        published (bool): Whether the new service should be published immediately
        name (str): The name of the service
        session (object): The database session to use.

    Returns:
        Text confirming the service creation with its id or a dict holding the new service id otherwise
    """
    if "options" in kwargs:
        kwargs["options"] = lib.core.convert_json(kwargs["options"])

    if "metadata" in kwargs:
        kwargs["metadata"] = lib.core.convert_json(kwargs["metadata"])

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        options = kwargs.get("options")

        kwargs["session"] = session

        # Get url_context_root
        kwargs = resolve_url_context_root(required=True, **kwargs)
        url_context_root = kwargs["url_context_root"]

        # Get url_host_name
        kwargs = resolve_url_host_name(required=False, **kwargs)
        url_host_name = kwargs["url_host_name"]
        if url_host_name is None:
            url_host_name = ""

        # Get url_protocol
        kwargs = resolve_url_protocol(**kwargs)

        kwargs = resolve_comments(**kwargs)

        defaultOptions = {
            "headers": {
                "Access-Control-Allow-Credentials": "true",
                "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
                "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
            },
            "http": {
                "allowedOrigin": "auto"
            },
            "logging": {
                "request": {
                    "headers": True,
                    "body": True
                },
                "response": {
                    "headers": True,
                    "body": True
                },
                "exceptions": True
            },
            "returnInternalErrorDetails": True,
            "includeLinksInResults": False
        }

        options = resolve_options(options, defaultOptions)

        with lib.core.MrsDbTransaction(session):
            service_id = lib.services.add_service(session, url_host_name, {
                "url_context_root": url_context_root,
                "url_protocol": kwargs.get("url_protocol"),
                "enabled": int(kwargs.get("enabled", True)),
                "comments": kwargs.get("comments"),
                "options": options,
                "auth_path": kwargs.get("auth_path", '/authentication'),
                "auth_completed_url": kwargs.get("auth_completed_url"),
                "auth_completed_url_validation": kwargs.get("auth_completed_url_validation"),
                "auth_completed_page_content": kwargs.get("auth_completed_page_content"),
                "metadata": kwargs.get("metadata"),
                "published": int(kwargs.get("published", False)),
                "name": kwargs.get("name"),
            })

        service = lib.services.get_service(session, service_id)

        if lib.core.get_interactive_result():
            return f"\nService '{service['host_ctx']}' created successfully."
        else:
            return service


@plugin_function('mrs.get.service', shell=True, cli=True, web=True)
def get_service(**kwargs):
    """Gets a specific MRS service

    If no service is specified, the service that is set as current service is
    returned if it was defined before

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        get_default (bool): Whether to return the default service
        auto_select_single (bool): If there is a single service only, use that
        session (object): The database session to use.

    Returns:
        The service as dict or None on error in interactive mode
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    url_context_root = kwargs.get("url_context_root")
    url_host_name = kwargs.get("url_host_name")
    service_id = kwargs.get("service_id")
    get_default = kwargs.get("get_default", False)
    auto_select_single = kwargs.get("auto_select_single", False)

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        # If there are no selective parameters given and interactive mode
        if (not url_context_root and not service_id and not get_default
                and lib.core.get_interactive_default()):
            # See if there is a current service, if so, return that one
            service = lib.services.get_current_service(session=session)
            if service:
                return lib.services.format_service_listing([service], True)

            # Check if there already is at least one service
            row = lib.core.select(table="service",
                                  cols="COUNT(*) as service_count, MIN(id) AS id"
                                  ).exec(session).first
            service_count = row.get("service_count", 0) if row else 0

            if service_count == 0:
                raise ValueError("No services available. Use "
                                 "mrs.add.`service`() to add a new service.")
            if auto_select_single and service_count == 1:
                service_id = row["id"]

            # If there are more services, let the user select one or all
            if not service_id:
                services = lib.services.get_services(session)
                print("MRS Service Listing")
                item = lib.core.prompt_for_list_item(
                    item_list=services,
                    prompt_caption=("Please select a service index or type "
                                    "'hostname/root_context': "),
                    item_name_property="host_ctx",
                    given_value=None,
                    print_list=True)
                if not item:
                    raise ValueError("Operation cancelled.")
                else:
                    return lib.services.format_service_listing([item], True)

        service = lib.services.get_service(url_context_root=url_context_root, url_host_name=url_host_name,
                                           service_id=service_id, get_default=get_default, session=session)

        if lib.core.get_interactive_result():
            # in interactive mode, if there is no service, we should display an empty listing
            if service:
                return lib.services.format_service_listing([service], True)
            else:
                return "The specified service was not found."
        else:
            return service


@plugin_function('mrs.list.services', shell=True, cli=True, web=True)
def get_services(**kwargs):
    """Get a list of MRS services

    Args:
        **kwargs: Additional options

    Keyword Args:
        session (object): The database session to use.

    Returns:
        Either a string listing the services when interactive is set or list
        of dicts representing the services
    """
    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        services = lib.services.get_services(session)

        if lib.core.get_interactive_result():
            return lib.services.format_service_listing(services, True)
        else:
            return services


@plugin_function('mrs.enable.service', shell=True, cli=True, web=True)
def enable_service(**kwargs):
    """Enables a MRS service

    If there is no service yet, a service with default values will be
    created and set as default.

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    kwargs["value"] = {"enabled": True}
    kwargs["allow_multi_select"] = True

    return call_update_service("enabled", **kwargs)


@plugin_function('mrs.disable.service', shell=True, cli=True, web=True)
def disable_service(**kwargs):
    """Disables a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    kwargs["value"] = {"enabled": False}
    kwargs["allow_multi_select"] = True

    return call_update_service("disabled", **kwargs)


@plugin_function('mrs.delete.service', shell=True, cli=True, web=True)
def delete_service(**kwargs):
    """Deletes a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        session (object): The database session to use.

    Returns:
        The result message as string
    """

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        lib.core.convert_ids_to_binary(["service_id"], kwargs)

        kwargs["session"] = session
        kwargs["allow_multi_select"] = True
        kwargs = resolve_service_ids(**kwargs)

        with lib.core.MrsDbTransaction(session):
            lib.services.delete_services(session, kwargs["service_ids"])

        if lib.core.get_interactive_result():
            if len(kwargs['service_ids']) == 1:
                return f"The service has been deleted."
            return f"The services have been deleted."

        return True


@plugin_function('mrs.set.service.contextPath', shell=True, cli=True, web=True)
def set_url_context_root(**kwargs):
    """Sets the url_context_root of a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        value (str): The context_path
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    kwargs["value"] = {"url_context_root": kwargs["value"]}
    if "service_id" not in kwargs:
        kwargs = resolve_url_context_root(required=False, **kwargs)
        kwargs = resolve_url_host_name(required=False, **kwargs)
        kwargs.pop("url_context_root", None)

    return call_update_service("updated", **kwargs)


@plugin_function('mrs.set.service.protocol', shell=True, cli=True, web=True)
def set_protocol(**kwargs):
    """Sets the protocol of a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        value (str): The protocol either 'HTTP', 'HTTPS' or 'HTTP,HTTPS'
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    kwargs["value"] = {"url_protocol": kwargs["value"]}
    if "service_id" not in kwargs:
        kwargs = resolve_url_context_root(required=False, **kwargs)
        kwargs = resolve_url_host_name(required=False, **kwargs)
        kwargs.pop("url_protocol", None)

    return call_update_service("updated", **kwargs)


@plugin_function('mrs.set.service.comments', shell=True, cli=True, web=True)
def set_comments(**kwargs):
    """Sets the comments of a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        value (str): The comments
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    kwargs["value"] = {"comments": kwargs["value"]}
    if "service_id" not in kwargs:
        kwargs = resolve_url_context_root(required=False, **kwargs)
        kwargs = resolve_url_host_name(required=False, **kwargs)
        kwargs.pop("comments", None)

    return call_update_service("updated", **kwargs)


@plugin_function('mrs.set.service.options', shell=True, cli=True, web=True)
def set_options(**kwargs):
    """Sets the options of a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        value (str): The comments
        service_id (str): The id of the service
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    kwargs["value"] = {"options": kwargs["value"]}
    if "service_id" not in kwargs:
        kwargs = resolve_url_context_root(required=False, **kwargs)
        kwargs = resolve_url_host_name(required=False, **kwargs)
        kwargs.pop("options", None)

    return call_update_service("updated", **kwargs)


@plugin_function('mrs.update.service', shell=True, cli=True, web=True)
def update_service(**kwargs):
    """Sets all properties of a MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        value (dict): The values as dict
        session (object): The database session to use.

    Allowed options for value:
        url_context_root (str): The context root for this service
        url_protocol (list): The protocol either 'HTTP', 'HTTPS' or 'HTTP,HTTPS'
        url_host_name (str): The host name for this service
        enabled (bool): Whether the service should be enabled
        comments (str): Comments about the service
        options (dict): Options of the service
        auth_path (str): The authentication path
        auth_completed_url (str): The redirection URL called after authentication
        auth_completed_url_validation (str): The regular expression that validates the
            app redirection URL specified by the /login?onCompletionRedirect parameter
        auth_completed_page_content (str): The custom page content to use of the
            authentication completed page
        metadata (dict): The metadata of the service
        in_development (dict): The development settings
        published (bool): Whether the service is published
        name (str): The name of the service

    Returns:
        The result message as string
    """
    if kwargs.get("value") is not None:
        # create a copy so that the dict won't change for the caller...and convert to dict
        kwargs["value"] = lib.core.convert_json(kwargs["value"])

    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    verify_value_keys(**kwargs)

    return call_update_service("updated", **kwargs)


@plugin_function('mrs.get.serviceRequestPathAvailability', shell=True, cli=True, web=True)
def get_service_request_path_availability(**kwargs):
    """Checks the availability of a given request path for the given service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        request_path (str): The request path to check
        session (object): The database session to use.

    Returns:
        True or False
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    service_id = kwargs.get("service_id")
    request_path = kwargs.get("request_path")

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        service = resolve_service(session, service_id, True)

        # Get request_path
        if not request_path and lib.core.get_interactive_default():
            request_path = lib.core.prompt(
                "Please enter the request path for this content set ["
                f"/content]: ",
                {'defaultValue': '/content'}).strip()

        if not request_path.startswith('/'):
            raise Exception("The request_path has to start with '/'.")

        try:
            in_development = service.get("in_development", None)
            if in_development is None:
                in_development = {}

            lib.core.check_request_path(
                session,
                in_development.get("developers", "")
                + service["host_ctx"] + request_path)
        except:
            return False

        return True


@plugin_function('mrs.get.currentServiceMetadata', shell=True, cli=True, web=True)
def get_current_service_metadata(**kwargs):
    """Gets information about the current service

    This function returns the id of the current MRS service as well as the last id of the metadata audit_log
    related to this MRS service as metadata_version. If there are no entries for the service in the audit_log, the
    string noChange is returned instead.

    Args:
        **kwargs: Additional options

    Keyword Args:
        session (object): The database session to use.

    Returns:
        {id: string, host_ctx: string, metadata_version: string}
    """
    session = kwargs.get("session")
    if session.database_type != "MySQL":
        return {}

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        status = lib.general.get_status(session)
        if status.get("service_configured", False) == False:
            return {}

        service_id = lib.services.get_current_service_id(session)

        service = lib.services.get_service(
            session=session, service_id=service_id)

        # Lookup the last entry in the audit_log table that affects the service and use that as the
        # version int
        res = session.run_sql(
            """
            SELECT max(id) AS version FROM `mysql_rest_service_metadata`.`audit_log`
            """)
        row = res.fetch_one()

        metadata_version = row.get_field("version") if row is not None else "0"
        if metadata_version is None:
            metadata_version = "0"

        if service is None:
            lib.services.set_current_service_id(
                session=session, service_id=None)
            return {
                "metadata_version": metadata_version
            }

        metadata = {
            "id": lib.core.convert_id_to_string(service.get("id")),
            "host_ctx": service.get("host_ctx"),
            "metadata_version": metadata_version
        }

        if not lib.core.get_interactive_result():
            return metadata
        else:
            return lib.services.format_metadata(metadata["host_ctx"], metadata["metadata_version"])


@plugin_function('mrs.set.currentService', shell=True, cli=True, web=True)
def set_current_service(**kwargs):
    """Sets the default MRS service

    Args:
        **kwargs: Additional options

    Keyword Args:
        service_id (str): The id of the service
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        session (object): The database session to use.

    Returns:
        The result message as string
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    service_id = kwargs.get("service_id")

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        if service_id is None:
            kwargs["session"] = session
            kwargs = resolve_url_context_root(required=False, **kwargs)
            kwargs = resolve_url_host_name(required=False, **kwargs)
            kwargs = resolve_service_ids(**kwargs)

            if kwargs["service_ids"]:
                service_id = kwargs["service_ids"][0]

        if service_id is None:
            if lib.core.get_interactive_result():
                return "The specified service was not found."
            return False

        lib.services.set_current_service_id(session, service_id)

    if lib.core.get_interactive_result():
        return "The service has been made the default."
    return True


@plugin_function('mrs.get.sdkBaseClasses', shell=True, cli=True, web=True)
def get_sdk_base_classes(**kwargs):
    """Returns the SDK base classes source for the given language

    Args:
        **kwargs: Options to determine what should be generated.

    Keyword Args:
        sdk_language (str): The SDK language to generate
        prepare_for_runtime (bool): Prepare code to be used in Monaco at runtime
        session (object): The database session to use.

    Returns:
        The SDK base classes source
    """
    sdk_language = kwargs.get("sdk_language", "TypeScript")
    prepare_for_runtime = kwargs.get("prepare_for_runtime", False)

    return lib.sdk.get_base_classes(sdk_language=sdk_language, prepare_for_runtime=prepare_for_runtime)


@plugin_function('mrs.get.sdkServiceClasses', shell=True, cli=True, web=True)
def get_sdk_service_classes(**kwargs):
    """Returns the SDK service classes source for the given language

    Args:
        **kwargs: Options to determine what should be generated.

    Keyword Args:
        service_id (str): The id of the service
        service_url (str): The url of the service
        sdk_language (str): The SDK language to generate
        prepare_for_runtime (bool): Prepare code to be used in Monaco at runtime
        session (object): The database session to use.

    Returns:
        The SDK base classes source
    """
    lib.core.convert_ids_to_binary(["service_id"], kwargs)

    service_id = kwargs.get("service_id")
    sdk_language = kwargs.get("sdk_language", "TypeScript")
    prepare_for_runtime = kwargs.get("prepare_for_runtime", False)
    service_url = kwargs.get("service_url")

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        service = resolve_service(
            session=session, service_query=service_id, required=False, auto_select_single=True)

        return lib.sdk.generate_service_sdk(
            service=service, sdk_language=sdk_language, session=session, prepare_for_runtime=prepare_for_runtime,
            service_url=service_url)


@plugin_function('mrs.dump.sdkServiceFiles', shell=True, cli=True, web=True)
def dump_sdk_service_files(**kwargs):
    """Dumps the SDK service files for a REST Service

    Args:
        **kwargs: Options to determine what should be generated.

    Keyword Args:
        directory (str): The directory to store the .mrs.sdk folder with the files
        options (dict): Several options how the SDK should be created
        session (object): The database session to use.

    Allowed options for options:
        service_id (str): The ID of the service the SDK should be generated for. If not specified, the default service
            is used.
        db_connection_uri (str): The dbConnectionUri that was used to export the SDK files
        sdk_language (str): The SDK language to generate
        add_app_base_class (str): The additional AppBaseClass file name
        service_url (str): The url of the service
        version (integer): The version of the generated files
        generationDate (str): The generation date of the SDK files
        header (str): The header to use for the SDK files

    Returns:
        True on success
    """
    directory = kwargs.get("directory")
    options = kwargs.get("options", {})

    if not directory:
        if lib.core.get_interactive_default():
            directory = lib.core.prompt(
                "Please enter the directory the folder with the SDK files should be placed:")
            if not directory:
                print("Cancelled.")
                return False
        else:
            raise Exception("No directory given.")

    # Ensure the directory path exists
    Path(directory).mkdir(parents=True, exist_ok=True)

    # Try to read the mrs_config from the directory
    mrs_config = get_stored_sdk_options(directory=directory)
    if mrs_config is None and options is None:
        raise Exception(
            f"No SDK options given and no existing SDK config found in the directory {directory}")

    if mrs_config is None:
        mrs_config = {}

    mrs_config["serviceId"] = options.get("service_id", mrs_config.get("serviceId"))
    mrs_config["sdkLanguage"] = options.get("sdk_language", mrs_config.get("sdkLanguage", "TypeScript"))
    mrs_config["serviceUrl"] = options.get("service_url", mrs_config.get("serviceUrl"))
    mrs_config["addAppBaseClass"] = options.get("add_app_base_class", mrs_config.get("addAppBaseClass"))
    mrs_config["dbConnectionUri"] = options.get("db_connection_uri", mrs_config.get("dbConnectionUri"))

    mrs_config["generationDate"] = datetime.datetime.now(
        datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")

    if mrs_config.get("serviceUrl") is None:
        raise Exception("The service URL is required.")

    mrs_config["header"] = options.get(
        "header", default_copyright_header(mrs_config["sdkLanguage"]))

    sdk_language = mrs_config["sdkLanguage"]

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        if mrs_config.get("serviceId") is None:
            mrs_config["serviceId"] = lib.core.convert_id_to_base64_string(
                    lib.services.get_current_service_id(session))

        serviceId = lib.core.id_to_binary(
            mrs_config.get("serviceId"), "mrs.config.json", True)

        if serviceId is None:
            raise Exception(
                "No serviceId defined in mrs.config.json. Please export the MRS SDK again.")

        service = resolve_service(session, serviceId, True, True)

        service_name = lib.core.convert_path_to_camel_case(
            service.get("url_context_root"))

        if sdk_language == "TypeScript":
            file_type = "ts"
            base_classes_file = os.path.join(directory, "MrsBaseClasses.ts")
        elif sdk_language == "Python":
            file_type = "py"
            base_classes_file = os.path.join(directory, "mrs_base_classes.py")

        base_classes = get_sdk_base_classes(
            sdk_language=sdk_language, session=session)
        with open(base_classes_file, 'w') as f:
            f.write(base_classes)

        file_name = file_name_using_language_convention(
            service_name, sdk_language)

        service_classes = get_sdk_service_classes(
            service_id=serviceId, service_url=mrs_config["serviceUrl"],
            sdk_language=sdk_language, session=session)
        with open(os.path.join(directory, f"{file_name}.{file_type}"), 'w') as f:
            f.write(service_classes)

        add_app_base_class = mrs_config.get("addAppBaseClass")

        if add_app_base_class is not None and isinstance(add_app_base_class, str) and add_app_base_class != '':
            path = os.path.abspath(__file__)
            file_path = Path(os.path.dirname(path), "sdk", sdk_language.lower(), add_app_base_class)
            shutil.copy(file_path, os.path.join(directory, add_app_base_class))

    # cspell:ignore timespec
    conf_file = Path(directory, "mrs.config.json")
    with open(conf_file, 'w') as f:
        f.write(json.dumps(mrs_config, indent=4))

    # TODO: this should be in a separate function (maybe context-aware for each language)
    if sdk_language == "Python":
        # In Python, we should create a "__init__.py" file to be able to import the directory as a regular package
        package_file = Path(directory, "__init__.py")
        with open(package_file, "w") as f:
            copyright_header = mrs_config["header"]
            f.write(copyright_header)

    return True


@plugin_function('mrs.get.sdkOptions', shell=True, cli=True, web=True)
def get_stored_sdk_options(directory):
    """Reads the SDK service option file located in a given directory

    Args:
        directory (str): The directory where the mrs.config.json file is stored

    Returns:
        The SDK options stored in that directory otherwise None
    """

    # Try to read the mrs_config from the directory
    mrs_config = {}
    conf_file = Path(directory, "mrs.config.json")
    if conf_file.is_file():
        try:
            with open(conf_file) as f:
                mrs_config = json.load(f)
        except:
            pass

    if "addAppBaseClass" in mrs_config:
        if isinstance(mrs_config["addAppBaseClass"], int):
            del mrs_config["addAppBaseClass"]

    return mrs_config


@plugin_function('mrs.get.runtimeManagementCode', shell=True, cli=True, web=True)
def get_runtime_management_code(**kwargs):
    """Returns the SDK service classes source for the given language

    Args:
        **kwargs: Options to determine what should be generated.

    Keyword Args:
        session (object): The database session to use.

    Returns:
        The SDK base classes source
    """

    with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
        return lib.sdk.get_mrs_runtime_management_code(session=session)


@plugin_function('mrs.get.serviceCreateStatement', shell=True, cli=True, web=True)
def get_service_create_statement(**kwargs):
    """Returns the corresponding CREATE REST SERVICE SQL statement of the given MRS service object.

    When using the 'service' parameter, you can choose either of these formats:
        - '0x11EF8496143CFDEC969C7413EA499D96' - Hexadecimal string ID
        - 'Ee+ElhQ8/eyWnHQT6kmdlg==' - Base64 string ID
        - 'localhost/myService' - Human readable string ID

    The service search parameters will be prioritized in the following order:
        - service_id
        - service
        - url_host_name + url_context_root

    Args:
        **kwargs: Options to determine what should be generated.

    Keyword Args:
        service_id (str): The ID of the service to generate.
        service (str): The identifier of the service.
        url_context_root (str): The context root for this service
        url_host_name (str): The host name for this service
        include_database_endpoints (bool): Include database objects that belong to the service.
        session (object): The database session to use.

    Returns:
        The SQL that represents the create statement for the MRS service
    """
    return generate_create_statement(**kwargs)


@plugin_function('mrs.dump.serviceSqlScript', shell=True, cli=True, web=True)
def dump_service_create_statement(**kwargs):
    """Dump a REST Service into a REST SQL file. The database and the dynamic endpoints will be included.

    When using the 'service' parameter, you can choose either of these formats:
        - '0x11EF8496143CFDEC969C7413EA499D96' - Hexadecimal string ID
        - 'Ee+ElhQ8/eyWnHQT6kmdlg==' - Base64 string ID
        - 'localhost/myService' - Human readable string ID

    The service search parameters will be prioritized in the following order:
        - service_id
        - service
        - url_host_name + url_context_root

    Args:
        **kwargs: Options to determine what should be generated.

    Keyword Args:
        service (str): The identifier of the service.
        endpoints (str): The endpoints to be included in the script
        file_path (str): The path where to store the file.
        overwrite (bool): Overwrite the file, if already exists.
        zip (bool): The final file is to be zipped.
        session (object): The database session to use.

    Returns:
        True if the file was saved.
    """
    file_path = kwargs.get("file_path")
    overwrite = kwargs.get("overwrite")

    match kwargs.get("endpoints", ""):
        case "all" | "dynamic":
            kwargs["include_database_endpoints"] = True
            kwargs["include_static_endpoints"] = True
            kwargs["include_dynamic_endpoints"] = True
        case "static":
            kwargs["include_database_endpoints"] = True
            kwargs["include_static_endpoints"] = True
        case "database":
            kwargs["include_database_endpoints"] = True


    file_path = resolve_file_path(file_path)
    resolve_overwrite_file(file_path, overwrite)

    kwargs["file_path"] = file_path
    store_create_statement(**kwargs)

    if lib.core.get_interactive_result():
        return f"File created in {file_path}."

    return True
