from __future__ import annotations

from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING, Literal, overload

from utilities.errors import ImpossibleCaseError
from utilities.subprocess import (
    MKTEMP_DIR_CMD,
    maybe_sudo_cmd,
    mkdir,
    mkdir_cmd,
    rm_cmd,
    run,
)

if TYPE_CHECKING:
    from collections.abc import Iterator

    from utilities.types import PathLike, StrStrMapping


@overload
def docker_cp(
    src: tuple[str, PathLike], dest: PathLike, /, *, sudo: bool = False
) -> None: ...
@overload
def docker_cp(
    src: PathLike, dest: tuple[str, PathLike], /, *, sudo: bool = False
) -> None: ...
def docker_cp(
    src: PathLike | tuple[str, PathLike],
    dest: PathLike | tuple[str, PathLike],
    /,
    *,
    sudo: bool = False,
) -> None:
    match src, dest:
        case Path() | str(), (str() as cont, Path() | str() as dest_path):
            docker_exec(
                cont, *maybe_sudo_cmd(*mkdir_cmd(dest_path, parent=True), sudo=sudo)
            )
            run(*docker_cp_cmd(src, dest, sudo=sudo))
        case (str(), Path() | str()), Path() | str():
            mkdir(dest, parent=True, sudo=sudo)
            run(*docker_cp_cmd(src, dest, sudo=sudo))
        case _:  # pragma: no cover
            raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])


@overload
def docker_cp_cmd(
    src: tuple[str, PathLike], dest: PathLike, /, *, sudo: bool = False
) -> list[str]: ...
@overload
def docker_cp_cmd(
    src: PathLike, dest: tuple[str, PathLike], /, *, sudo: bool = False
) -> list[str]: ...
def docker_cp_cmd(
    src: PathLike | tuple[str, PathLike],
    dest: PathLike | tuple[str, PathLike],
    /,
    *,
    sudo: bool = False,
) -> list[str]:
    match src, dest:
        case (Path() | str()) as src_use, (
            str() as dest_cont,
            Path() | str() as dest_path,
        ):
            dest_use = f"{dest_cont}:{dest_path}"
        case (str() as src_cont, (Path() | str()) as src_path), (
            Path() | str() as dest_use
        ):
            src_use = f"{src_cont}:{src_path}"
        case _:  # pragma: no cover
            raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
    parts: list[str] = ["docker", "cp", str(src_use), str(dest_use)]
    return maybe_sudo_cmd(*parts, sudo=sudo)


@overload
def docker_exec(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    shell: bool = False,
    print: bool = False,
    print_stdout: bool = False,
    print_stderr: bool = False,
    return_: Literal[True],
    return_stdout: bool = False,
    return_stderr: bool = False,
    **env_kwargs: str,
) -> str: ...
@overload
def docker_exec(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    shell: bool = False,
    print: bool = False,
    print_stdout: bool = False,
    print_stderr: bool = False,
    return_: bool = False,
    return_stdout: Literal[True],
    return_stderr: bool = False,
    **env_kwargs: str,
) -> str: ...
@overload
def docker_exec(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    shell: bool = False,
    print: bool = False,
    print_stdout: bool = False,
    print_stderr: bool = False,
    return_: bool = False,
    return_stdout: bool = False,
    return_stderr: Literal[True],
    **env_kwargs: str,
) -> str: ...
@overload
def docker_exec(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    shell: bool = False,
    print: bool = False,
    print_stdout: bool = False,
    print_stderr: bool = False,
    return_: Literal[False] = False,
    return_stdout: Literal[False] = False,
    return_stderr: Literal[False] = False,
    **env_kwargs: str,
) -> None: ...
@overload
def docker_exec(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    shell: bool = False,
    print: bool = False,
    print_stdout: bool = False,
    print_stderr: bool = False,
    return_: bool = False,
    return_stdout: bool = False,
    return_stderr: bool = False,
    **env_kwargs: str,
) -> str | None: ...
def docker_exec(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    shell: bool = False,
    print: bool = False,  # noqa: A002
    print_stdout: bool = False,
    print_stderr: bool = False,
    return_: bool = False,
    return_stdout: bool = False,
    return_stderr: bool = False,
    **env_kwargs: str,
) -> str | None:
    cmd_use = docker_exec_cmd(  # skipif-ci
        container, cmd, *args, env=env, user=user, workdir=workdir, **env_kwargs
    )
    if shell:  # skipif-ci
        return run(
            " ".join(cmd_use),
            shell=shell,
            print=print,
            print_stdout=print_stdout,
            print_stderr=print_stderr,
            return_=return_,
            return_stdout=return_stdout,
            return_stderr=return_stderr,
        )
    return run(  # skipif-ci
        *cmd_use,
        shell=shell,
        print=print,
        print_stdout=print_stdout,
        print_stderr=print_stderr,
        return_=return_,
        return_stdout=return_stdout,
        return_stderr=return_stderr,
    )


def docker_exec_cmd(
    container: str,
    cmd: str,
    /,
    *args: str,
    env: StrStrMapping | None = None,
    user: str | None = None,
    workdir: PathLike | None = None,
    **env_kwargs: str,
) -> list[str]:
    """Build a command for `docker exec`."""
    parts: list[str] = ["docker", "exec"]
    mapping: dict[str, str] = ({} if env is None else dict(env)) | env_kwargs
    for key, value in mapping.items():
        parts.extend(["--env", f"{key}={value}"])
    if user is not None:
        parts.extend(["--user", user])
    if workdir is not None:
        parts.extend(["--workdir", str(workdir)])
    return [*parts, container, cmd, *args]


@contextmanager
def yield_docker_temp_dir(
    container: str, /, *, user: str | None = None
) -> Iterator[Path]:
    path = Path(  # skipif-ci
        docker_exec(container, *MKTEMP_DIR_CMD, user=user, return_=True).rstrip("\n")
    )
    try:  # skipif-ci
        yield path
    finally:  # skipif-ci
        docker_exec(container, *rm_cmd(path), user=user)


__all__ = ["docker_cp_cmd", "docker_exec", "docker_exec_cmd", "yield_docker_temp_dir"]
