"""
Helper methods related to safe exec.
"""

import json
import logging
from importlib import import_module

import requests
from codejail.safe_exec import SafeExecException, json_safe
from django.conf import settings
from django.utils.translation import gettext as _
from edx_toggles.toggles import SettingToggle
from requests.exceptions import HTTPError, RequestException
from simplejson import JSONDecodeError

from .exceptions import CodejailServiceParseError, CodejailServiceStatusError, CodejailServiceUnavailable

log = logging.getLogger(__name__)

# .. toggle_name: ENABLE_CODEJAIL_REST_SERVICE
# .. toggle_implementation: SettingToggle
# .. toggle_default: False
# .. toggle_description: Set this to True if you want to run Codejail code using
#   a separate VM or container and communicate with edx-platform using REST API.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2021-08-19
ENABLE_CODEJAIL_REST_SERVICE = SettingToggle("ENABLE_CODEJAIL_REST_SERVICE", default=False, module_name=__name__)

# .. toggle_name: ENABLE_CODEJAIL_DARKLAUNCH
# .. toggle_implementation: SettingToggle
# .. toggle_default: False
# .. toggle_description: Turn on to send requests to both the codejail service and the installed codejail library for
#   testing and evaluation purposes. The results from the installed codejail library will be the ones used.
# .. toggle_warning: This toggle will only behave as expected when ENABLE_CODEJAIL_REST_SERVICE is not enabled and when
#   CODE_JAIL_REST_SERVICE_REMOTE_EXEC, CODE_JAIL_REST_SERVICE_HOST, CODE_JAIL_REST_SERVICE_READ_TIMEOUT,
#   and CODE_JAIL_REST_SERVICE_CONNECT_TIMEOUT are configured.
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2025-04-03
# .. toggle_target_removal_date: 2025-05-01
ENABLE_CODEJAIL_DARKLAUNCH = SettingToggle("ENABLE_CODEJAIL_DARKLAUNCH", default=False, module_name=__name__)


def is_codejail_rest_service_enabled():
    """Return whether the codejail REST service is enabled."""
    return ENABLE_CODEJAIL_REST_SERVICE.is_enabled()


def is_codejail_in_darklaunch():
    """
    Returns whether codejail dark launch is enabled.

    Codejail dark launch can only be enabled if ENABLE_CODEJAIL_REST_SERVICE is not enabled.
    """
    return not is_codejail_rest_service_enabled() and ENABLE_CODEJAIL_DARKLAUNCH.is_enabled()


def get_remote_exec(*args, **kwargs):
    """Get remote exec function based on setting and executes it."""
    remote_exec_function_name = settings.CODE_JAIL_REST_SERVICE_REMOTE_EXEC
    try:
        mod_name, func_name = remote_exec_function_name.rsplit(".", 1)
        remote_exec_module = import_module(mod_name)
        remote_exec_function = getattr(remote_exec_module, func_name)
        if not remote_exec_function:
            remote_exec_function = send_safe_exec_request_v0
    except ModuleNotFoundError:
        return send_safe_exec_request_v0(*args, **kwargs)
    return remote_exec_function(*args, **kwargs)


def get_codejail_rest_service_endpoint():
    """Return the endpoint URL for the codejail REST service."""
    return f"{settings.CODE_JAIL_REST_SERVICE_HOST}/api/v0/code-exec"


def send_safe_exec_request_v0(data):
    """
    Sends a request to a codejail api service forwarding required code and files.
    Arguments:
        data: Dict containing code and other parameters required for jailed code execution.
        It also includes extra_files (python_lib.zip) required by the codejail execution.
    Returns:
        Response received from codejail api service
    """
    globals_dict = data["globals_dict"]
    extra_files = data.pop("extra_files")

    codejail_service_endpoint = get_codejail_rest_service_endpoint()

    # In rare cases an XBlock might introduce `bytes` objects (or other
    # non-JSON-serializable objects) into the globals dict. The codejail service
    # (via the codejail library) will call `json_safe` on the globals before
    # JSON-encoding for the sandbox input, but here we need to call it earlier
    # in the process so we can even transport the globals *to* the codejail
    # service. Otherwise, we may get a TypeError when constructing the payload.
    #
    # This is a lossy operation (non-serializable objects will be dropped, and
    # bytes converted to strings) but it is the same lossy operation that
    # codejail will perform anyhow -- and it should be idempotent.
    data_send = {**data}
    data_send["globals_dict"] = json_safe(data_send["globals_dict"])

    payload = json.dumps(data_send)

    try:
        response = requests.post(
            codejail_service_endpoint,
            files=extra_files,
            data={"payload": payload},
            timeout=(settings.CODE_JAIL_REST_SERVICE_CONNECT_TIMEOUT, settings.CODE_JAIL_REST_SERVICE_READ_TIMEOUT),
        )

    except RequestException as err:
        log.error(
            "Failed to connect to codejail api service: url=%s, params=%s", codejail_service_endpoint, str(payload)
        )
        raise CodejailServiceUnavailable(
            _("Codejail API Service is unavailable. Please try again in a few minutes.")
        ) from err

    try:
        response.raise_for_status()
    except HTTPError as err:
        raise CodejailServiceStatusError(_("Codejail API Service invalid response.")) from err

    try:
        response_json = response.json()
    except JSONDecodeError as err:
        log.error("Invalid JSON response received from codejail api service: Response_Content=%s", response.content)
        raise CodejailServiceParseError(_("Invalid JSON response received from codejail api service.")) from err

    emsg = response_json.get("emsg")
    exception = None

    if emsg:
        exception_msg = f"{emsg}. For more information check Codejail Service logs."
        exception = SafeExecException(exception_msg)

    globals_dict.update(response_json.get("globals_dict"))

    return emsg, exception
