"""The IPython kernel spec for Jupyter"""

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

from __future__ import annotations

import errno
import json
import os
import platform
import shutil
import stat
import sys
import tempfile
from pathlib import Path
from typing import Any

from jupyter_client.kernelspec import KernelSpecManager
from traitlets import Unicode
from traitlets.config import Application

pjoin = os.path.join

KERNEL_NAME = "python%i" % sys.version_info[0]

# path to kernelspec resources
RESOURCES = pjoin(Path(__file__).parent, "resources")


def make_ipkernel_cmd(
    mod: str = "ipykernel_launcher",
    executable: str | None = None,
    extra_arguments: list[str] | None = None,
    python_arguments: list[str] | None = None,
) -> list[str]:
    """Build Popen command list for launching an IPython kernel.

    Parameters
    ----------
    mod : str, optional (default 'ipykernel')
        A string of an IPython module whose __main__ starts an IPython kernel
    executable : str, optional (default sys.executable)
        The Python executable to use for the kernel process.
    extra_arguments : list, optional
        A list of extra arguments to pass when executing the launch code.

    Returns
    -------
    A Popen command list
    """
    if executable is None:
        executable = sys.executable
    extra_arguments = extra_arguments or []
    python_arguments = python_arguments or []
    return [executable, *python_arguments, "-m", mod, "-f", "{connection_file}", *extra_arguments]


def get_kernel_dict(
    extra_arguments: list[str] | None = None, python_arguments: list[str] | None = None
) -> dict[str, Any]:
    """Construct dict for kernel.json"""
    return {
        "argv": make_ipkernel_cmd(
            extra_arguments=extra_arguments, python_arguments=python_arguments
        ),
        "display_name": "Python %i (ipykernel)" % sys.version_info[0],
        "language": "python",
        "metadata": {"debugger": True},
        "kernel_protocol_version": "5.5",
    }


def write_kernel_spec(
    path: Path | str | None = None,
    overrides: dict[str, Any] | None = None,
    extra_arguments: list[str] | None = None,
    python_arguments: list[str] | None = None,
) -> str:
    """Write a kernel spec directory to `path`

    If `path` is not specified, a temporary directory is created.
    If `overrides` is given, the kernelspec JSON is updated before writing.

    The path to the kernelspec is always returned.
    """
    if path is None:
        path = Path(tempfile.mkdtemp(suffix="_kernels")) / KERNEL_NAME

    # stage resources
    shutil.copytree(RESOURCES, path)

    # ensure path is writable
    mask = Path(path).stat().st_mode
    if not mask & stat.S_IWUSR:
        Path(path).chmod(mask | stat.S_IWUSR)

    # write kernel.json
    kernel_dict = get_kernel_dict(extra_arguments, python_arguments)

    if overrides:
        kernel_dict.update(overrides)
    with open(pjoin(path, "kernel.json"), "w") as f:
        json.dump(kernel_dict, f, indent=1)

    return str(path)


def install(
    kernel_spec_manager: KernelSpecManager | None = None,
    user: bool = False,
    kernel_name: str = KERNEL_NAME,
    display_name: str | None = None,
    prefix: str | None = None,
    profile: str | None = None,
    env: dict[str, str] | None = None,
    frozen_modules: bool = False,
) -> str:
    """Install the IPython kernelspec for Jupyter

    Parameters
    ----------
    kernel_spec_manager : KernelSpecManager [optional]
        A KernelSpecManager to use for installation.
        If none provided, a default instance will be created.
    user : bool [default: False]
        Whether to do a user-only install, or system-wide.
    kernel_name : str, optional
        Specify a name for the kernelspec.
        This is needed for having multiple IPython kernels for different environments.
    display_name : str, optional
        Specify the display name for the kernelspec
    profile : str, optional
        Specify a custom profile to be loaded by the kernel.
    prefix : str, optional
        Specify an install prefix for the kernelspec.
        This is needed to install into a non-default location, such as a conda/virtual-env.
    env : dict, optional
        A dictionary of extra environment variables for the kernel.
        These will be added to the current environment variables before the
        kernel is started
    frozen_modules : bool, optional
        Whether to use frozen modules for potentially faster kernel startup.
        Using frozen modules prevents debugging inside of some built-in
        Python modules, such as io, abc, posixpath, ntpath, or stat.
        The frozen modules are used in CPython for faster interpreter startup.
        Ignored for cPython <3.11 and for other Python implementations.

    Returns
    -------
    The path where the kernelspec was installed.
    """
    if kernel_spec_manager is None:
        kernel_spec_manager = KernelSpecManager()

    if env is None:
        env = {}

    if (kernel_name != KERNEL_NAME) and (display_name is None):
        # kernel_name is specified and display_name is not
        # default display_name to kernel_name
        display_name = kernel_name
    overrides: dict[str, Any] = {}
    if display_name:
        overrides["display_name"] = display_name
    if profile:
        extra_arguments = ["--profile", profile]
        if not display_name:
            # add the profile to the default display name
            overrides["display_name"] = "Python %i [profile=%s]" % (sys.version_info[0], profile)
    else:
        extra_arguments = None

    python_arguments = None

    # addresses the debugger warning from debugpy about frozen modules
    if sys.version_info >= (3, 11) and platform.python_implementation() == "CPython":
        if not frozen_modules:
            # disable frozen modules
            python_arguments = ["-Xfrozen_modules=off"]
        elif "PYDEVD_DISABLE_FILE_VALIDATION" not in env:
            # user opted-in to have frozen modules, and we warned them about
            # consequences for the - disable the debugger warning
            env["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"

    if env:
        overrides["env"] = env
    path = write_kernel_spec(
        overrides=overrides, extra_arguments=extra_arguments, python_arguments=python_arguments
    )
    dest = kernel_spec_manager.install_kernel_spec(
        path, kernel_name=kernel_name, user=user, prefix=prefix
    )
    # cleanup afterward
    shutil.rmtree(path)
    return dest


# Entrypoint


class InstallIPythonKernelSpecApp(Application):
    """Dummy app wrapping argparse"""

    name = Unicode("ipython-kernel-install")

    def initialize(self, argv: list[str] | None = None) -> None:
        """Initialize the app."""
        if argv is None:
            argv = sys.argv[1:]
        self.argv = argv

    def start(self) -> None:
        """Start the app."""
        import argparse

        parser = argparse.ArgumentParser(
            prog=self.name, description="Install the IPython kernel spec."
        )
        parser.add_argument(
            "--user",
            action="store_true",
            help="Install for the current user instead of system-wide",
        )
        parser.add_argument(
            "--name",
            type=str,
            default=KERNEL_NAME,
            help="Specify a name for the kernelspec."
            " This is needed to have multiple IPython kernels at the same time.",
        )
        parser.add_argument(
            "--display-name",
            type=str,
            help="Specify the display name for the kernelspec."
            " This is helpful when you have multiple IPython kernels.",
        )
        parser.add_argument(
            "--profile",
            type=str,
            help="Specify an IPython profile to load. "
            "This can be used to create custom versions of the kernel.",
        )
        parser.add_argument(
            "--prefix",
            type=str,
            help="Specify an install prefix for the kernelspec."
            " This is needed to install into a non-default location, such as a conda/virtual-env.",
        )
        parser.add_argument(
            "--sys-prefix",
            action="store_const",
            const=sys.prefix,
            dest="prefix",
            help="Install to Python's sys.prefix."
            " Shorthand for --prefix='%s'. For use in conda/virtual-envs." % sys.prefix,
        )
        parser.add_argument(
            "--env",
            action="append",
            nargs=2,
            metavar=("ENV", "VALUE"),
            help="Set environment variables for the kernel.",
        )
        parser.add_argument(
            "--frozen_modules",
            action="store_true",
            help="Enable frozen modules for potentially faster startup."
            " This has a downside of preventing the debugger from navigating to certain built-in modules.",
        )
        opts = parser.parse_args(self.argv)
        if opts.env:
            opts.env = dict(opts.env)
        try:
            dest = install(
                user=opts.user,
                kernel_name=opts.name,
                profile=opts.profile,
                prefix=opts.prefix,
                display_name=opts.display_name,
                env=opts.env,
            )
        except OSError as e:
            if e.errno == errno.EACCES:
                print(e, file=sys.stderr)
                if opts.user:
                    print("Perhaps you want `sudo` or `--user`?", file=sys.stderr)
                self.exit(1)
            raise
        print(f"Installed kernelspec {opts.name} in {dest}")


if __name__ == "__main__":
    InstallIPythonKernelSpecApp.launch_instance()
