from __future__ import annotations

import contextlib
import os
import shutil
from collections.abc import Generator
from collections.abc import Sequence

from pre_commit import lang_base
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import UNSET
from pre_commit.prefix import Prefix
from pre_commit.util import cmd_output_b

ENVIRONMENT_DIR = 'juliaenv'
health_check = lang_base.basic_health_check
get_default_version = lang_base.basic_get_default_version


def run_hook(
        prefix: Prefix,
        entry: str,
        args: Sequence[str],
        file_args: Sequence[str],
        *,
        is_local: bool,
        require_serial: bool,
        color: bool,
) -> tuple[int, bytes]:
    # `entry` is a (hook-repo relative) file followed by (optional) args, e.g.
    # `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we
    # 1) shell parse it and join with args with hook_cmd
    # 2) prepend the hooks prefix path to the first argument (the file), unless
    #    it is a local script
    # 3) prepend `julia` as the interpreter

    cmd = lang_base.hook_cmd(entry, args)
    script = cmd[0] if is_local else prefix.path(cmd[0])
    cmd = ('julia', '--startup-file=no', script, *cmd[1:])
    return lang_base.run_xargs(
        cmd,
        file_args,
        require_serial=require_serial,
        color=color,
    )


def get_env_patch(target_dir: str, version: str) -> PatchesT:
    return (
        ('JULIA_LOAD_PATH', target_dir),
        # May be set, remove it to not interfer with LOAD_PATH
        ('JULIA_PROJECT', UNSET),
        # Keep the package depot inside the hook environment so installed
        # packages and precompile caches persist with the env instead of
        # leaking into a shared depot. The trailing separator leaves an empty
        # entry, which julia expands to its default depots. This keeps the
        # bundled stdlib resources (and their precompile caches) available so
        # hooks don't recompile from scratch on first run.
        ('JULIA_DEPOT_PATH', os.path.join(target_dir, 'depot') + os.pathsep),
    )


@contextlib.contextmanager
def in_env(prefix: Prefix, version: str) -> Generator[None]:
    envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
    with envcontext(get_env_patch(envdir, version)):
        yield


def install_environment(
        prefix: Prefix,
        version: str,
        additional_dependencies: Sequence[str],
) -> None:
    envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
    with in_env(prefix, version):
        # TODO: Support language_version with juliaup similar to rust via
        # rustup
        # if version != 'system':
        #     ...

        # Copy Project.toml to hook env if it exist
        os.makedirs(envdir, exist_ok=True)
        project_names = ('JuliaProject.toml', 'Project.toml')
        project_found = False
        for project_name in project_names:
            project_file = prefix.path(project_name)
            if not os.path.isfile(project_file):
                continue
            shutil.copy(project_file, envdir)
            project_found = True
            break

        # If no project file was found we create an empty one so that the
        # package manager doesn't error
        if not project_found:
            open(os.path.join(envdir, 'Project.toml'), 'a').close()

        # Copy Manifest.toml to hook env if it exists
        manifest_names = ('JuliaManifest.toml', 'Manifest.toml')
        for manifest_name in manifest_names:
            manifest_file = prefix.path(manifest_name)
            if not os.path.isfile(manifest_file):
                continue
            shutil.copy(manifest_file, envdir)
            break

        # Julia code to instantiate the hook environment
        julia_code = """
        @assert length(ARGS) > 0
        hook_env = ARGS[1]
        deps = join(ARGS[2:end], " ")

        # We prepend @stdlib here so that we can load the package manager even
        # though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env.
        pushfirst!(LOAD_PATH, "@stdlib")
        using Pkg
        popfirst!(LOAD_PATH)

        # Instantiate the environment shipped with the hook repo. If we have
        # additional dependencies we disable precompilation in this step to
        # avoid double work.
        precompile = isempty(deps) ? "1" : "0"
        withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do
            Pkg.instantiate()
        end

        # Add additional dependencies (with precompilation)
        if !isempty(deps)
            withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do
                Pkg.REPLMode.pkgstr("add " * deps)
            end
        end
        """
        cmd_output_b(
            'julia', '--startup-file=no', '-e', julia_code, '--', envdir,
            *additional_dependencies,
            cwd=prefix.prefix_dir,
        )
