"""Functions to run commands and capture output."""

import os
import shutil
import sys
import textwrap
import traceback
from contextlib import contextmanager
from io import StringIO
from typing import Callable, Optional, Tuple, Union

import colorama
from ansimarkup import ansiprint
from jinja2 import Environment

from failprint import WINDOWS
from failprint.capture import Capture, cast_capture
from failprint.formats import DEFAULT_FORMAT, accept_custom_format, formats, printable_command
from failprint.process import run_pty_subprocess, run_subprocess
from failprint.types import CmdFuncType, CmdType

if WINDOWS:
    colorama.init()


class StdBuffer:
    """A simple placeholder for three memory buffers."""

    def __init__(self, stdinput=None, stdout=None, stderr=None):
        """
        Initialize the object.

        Arguments:
            stdinput: String to use as standard input.
            stdout: A buffer for standard output.
            stderr: A buffer for standard error.
        """
        self.stdin = StringIO(stdinput) if stdinput is not None else sys.stdin
        self.stdout = stdout or StringIO()
        self.stderr = stderr or StringIO()


@contextmanager
def stdbuffer(stdinput=None):
    """
    Capture output in a `with` statement.

    Arguments:
        stdinput: String to use as standard input.

    Yields:
        An instance of `StdBuffer`.
    """
    old_stdin = sys.stdin
    old_stdout = sys.stdout
    old_stderr = sys.stderr

    buffer = StdBuffer(stdinput)

    sys.stdin = buffer.stdin
    sys.stdout = buffer.stdout
    sys.stderr = buffer.stderr

    yield buffer

    sys.stdin = old_stdin
    sys.stdout = old_stdout
    sys.stderr = old_stderr

    buffer.stdin.close()
    buffer.stdout.close()
    buffer.stderr.close()


class RunResult:
    """Placeholder for a run result."""

    def __init__(self, code: int, output: str) -> None:
        """
        Initialize the object.

        Arguments:
            code: The exit code of the command.
            output: The output of the command.
        """
        self.code = code
        self.output = output


def run(  # noqa: WPS231 (high complexity)
    cmd: CmdFuncType,
    args=None,
    kwargs=None,
    number: int = 1,
    capture: Optional[Union[str, bool, Capture]] = None,
    title: Optional[str] = None,
    fmt: Optional[str] = None,
    pty: bool = False,
    progress: bool = True,
    nofail: bool = False,
    quiet: bool = False,
    silent: bool = False,
    stdin: Optional[str] = None,
) -> RunResult:
    """
    Run a command in a subprocess or a Python function, and print its output if it fails.

    Arguments:
        cmd: The command to run.
        args: Arguments to pass to the callable.
        kwargs: Keyword arguments to pass to the callable.
        number: The command number.
        capture: The output to capture.
        title: The command title.
        fmt: The output format.
        pty: Whether to run in a PTY.
        progress: Whether to show progress.
        nofail: Whether to always succeed.
        quiet: Whether to not print the command output.
        silent: Don't print anything.
        stdin: String to use as standard input.

    Returns:
        The command exit code, or 0 if `nofail` is True.
    """
    format_name: str = fmt or os.environ.get("FAILPRINT_FORMAT", DEFAULT_FORMAT)  # type: ignore
    format_name = accept_custom_format(format_name)
    format_obj = formats.get(format_name, formats[DEFAULT_FORMAT])

    env = Environment(autoescape=False)  # noqa: S701 (no HTML: no need to escape)
    env.filters["indent"] = textwrap.indent

    command = printable_command(cmd, args, kwargs)

    if not silent and progress and format_obj.progress_template:
        progress_template = env.from_string(format_obj.progress_template)
        ansiprint(progress_template.render({"title": title, "command": command}), end="\r")

    capture = cast_capture(capture)

    if callable(cmd):
        code, output = run_function(cmd, args, kwargs, capture, stdin)
    else:
        code, output = run_command(cmd, capture, format_obj.accept_ansi, pty, stdin)

    if not silent:
        template = env.from_string(format_obj.template)
        ansiprint(
            template.render(
                {
                    "title": title,
                    "command": command,
                    "code": code,
                    "success": code == 0,
                    "failure": code != 0,
                    "number": number,
                    "output": output,
                    "nofail": nofail,
                    "quiet": quiet,
                    "silent": silent,
                },
            ),
        )

    return RunResult(0 if nofail else code, output)


def run_command(
    cmd: CmdType,
    capture: Capture = Capture.BOTH,
    ansi: bool = False,
    pty: bool = False,
    stdin: Optional[str] = None,
) -> Tuple[int, str]:
    """
    Run a command.

    Arguments:
        cmd: The command to run.
        capture: The output to capture.
        ansi: Whether to accept ANSI sequences.
        pty: Whether to run in a PTY.
        stdin: String to use as standard input.

    Returns:
        The exit code and the command output.
    """
    shell = isinstance(cmd, str)

    # if chosen format doesn't accept ansi, or on Windows, don't use pty
    if pty and (not ansi or WINDOWS):
        pty = False

    # pty can only combine, so only use pty when combining
    if pty and capture in {Capture.BOTH, Capture.NONE}:
        if shell:
            cmd = ["sh", "-c", cmd]  # type: ignore  # we know cmd is str
        return run_pty_subprocess(cmd, capture, stdin)  # type: ignore  # we made sure cmd is a list

    # we are on Windows
    if WINDOWS:
        # make sure the process can find the executable
        if not shell:
            cmd[0] = shutil.which(cmd[0]) or cmd[0]  # type: ignore  # we know cmd is a list
        return run_subprocess(cmd, capture, shell=shell, stdin=stdin)  # noqa: S604 (shell=True)

    return run_subprocess(cmd, capture, shell=shell, stdin=stdin)  # noqa: S604 (shell=True)


def run_function(
    func,
    args=None,
    kwargs=None,
    capture: Capture = Capture.BOTH,
    stdin: Optional[str] = None,
) -> Tuple[int, str]:
    """
    Run a function.

    Arguments:
        func: The function to run.
        args: Positional arguments passed to the function.
        kwargs: Keyword arguments passed to the function.
        capture: The output to capture.
        stdin: String to use as standard input.

    Returns:
        The exit code and the function output.
    """
    args = args or []
    kwargs = kwargs or {}

    if capture == Capture.NONE:
        return run_function_get_code(func, sys.stderr, args, kwargs), ""

    with stdbuffer(stdin) as buffer:
        if capture == Capture.BOTH:
            # combining stdout and stderr
            # -> redirect stderr to stdout
            buffer.stderr = buffer.stdout
            sys.stderr = buffer.stdout

        code = run_function_get_code(func, buffer.stderr, args, kwargs)

        if capture == Capture.STDERR:
            output = buffer.stderr.getvalue()
        else:
            output = buffer.stdout.getvalue()

    return code, output


def run_function_get_code(func: Callable, stderr, args=None, kwargs=None) -> int:  # noqa: WPS212 (return statements)
    """
    Run a function and return a exit code.

    Arguments:
        func: The function to run.
        stderr: A file descriptor to write potential tracebacks.
        args: Positional arguments passed to the function.
        kwargs: Keyword arguments passed to the function.

    Returns:
        An exit code.
    """
    try:
        result = func(*args, **kwargs)
    except Exception:  # noqa: W0703 (catching Exception on purpose)
        stderr.write(traceback.format_exc() + "\n")
        return 1

    # first check True and False
    # because int(True) == 1 and int(False) == 0
    if result is True:
        return 0
    if result is False:
        return 1
    try:
        return int(result)
    except (ValueError, TypeError):
        if result is None or bool(result):
            return 0
        return 1

"""Functions to run commands and capture output."""

import os
import shutil
import sys
import textwrap
import traceback
from contextlib import contextmanager
from io import StringIO
from typing import Callable, Optional, Tuple, Union

import colorama
from ansimarkup import ansiprint
from jinja2 import Environment

from failprint import WINDOWS
from failprint.capture import Capture, cast_capture
from failprint.formats import DEFAULT_FORMAT, accept_custom_format, formats, printable_command
from failprint.process import run_pty_subprocess, run_subprocess
from failprint.types import CmdFuncType, CmdType

if WINDOWS:
    colorama.init()


class StdBuffer:
    """A simple placeholder for three memory buffers."""

    def __init__(self, stdinput=None, stdout=None, stderr=None):
        """
        Initialize the object.

        Arguments:
            stdinput: String to use as standard input.
            stdout: A buffer for standard output.
            stderr: A buffer for standard error.
        """
        self.stdin = StringIO(stdinput) if stdinput is not None else sys.stdin
        self.stdout = stdout or StringIO()
        self.stderr = stderr or StringIO()


@contextmanager
def stdbuffer(stdinput=None):
    """
    Capture output in a `with` statement.

    Arguments:
        stdinput: String to use as standard input.

    Yields:
        An instance of `StdBuffer`.
    """
    old_stdin = sys.stdin
    old_stdout = sys.stdout
    old_stderr = sys.stderr

    buffer = StdBuffer(stdinput)

    sys.stdin = buffer.stdin
    sys.stdout = buffer.stdout
    sys.stderr = buffer.stderr

    yield buffer

    sys.stdin = old_stdin
    sys.stdout = old_stdout
    sys.stderr = old_stderr

    buffer.stdin.close()
    buffer.stdout.close()
    buffer.stderr.close()


class RunResult:
    """Placeholder for a run result."""

    def __init__(self, code: int, output: str) -> None:
        """
        Initialize the object.

        Arguments:
            code: The exit code of the command.
            output: The output of the command.
        """
        self.code = code
        self.output = output


def run(  # noqa: WPS231 (high complexity)
    cmd: CmdFuncType,
    args=None,
    kwargs=None,
    number: int = 1,
    capture: Optional[Union[str, bool, Capture]] = None,
    title: Optional[str] = None,
    fmt: Optional[str] = None,
    pty: bool = False,
    progress: bool = True,
    nofail: bool = False,
    quiet: bool = False,
    silent: bool = False,
    stdin: Optional[str] = None,
) -> RunResult:
    """
    Run a command in a subprocess or a Python function, and print its output if it fails.

    Arguments:
        cmd: The command to run.
        args: Arguments to pass to the callable.
        kwargs: Keyword arguments to pass to the callable.
        number: The command number.
        capture: The output to capture.
        title: The command title.
        fmt: The output format.
        pty: Whether to run in a PTY.
        progress: Whether to show progress.
        nofail: Whether to always succeed.
        quiet: Whether to not print the command output.
        silent: Don't print anything.
        stdin: String to use as standard input.

    Returns:
        The command exit code, or 0 if `nofail` is True.
    """
    format_name: str = fmt or os.environ.get("FAILPRINT_FORMAT", DEFAULT_FORMAT)  # type: ignore
    format_name = accept_custom_format(format_name)
    format_obj = formats.get(format_name, formats[DEFAULT_FORMAT])

    env = Environment(autoescape=False)  # noqa: S701 (no HTML: no need to escape)
    env.filters["indent"] = textwrap.indent

    command = printable_command(cmd, args, kwargs)

    if not silent and progress and format_obj.progress_template:
        progress_template = env.from_string(format_obj.progress_template)
        ansiprint(progress_template.render({"title": title, "command": command}), end="\r")

    capture = cast_capture(capture)

    if callable(cmd):
        code, output = run_function(cmd, args, kwargs, capture, stdin)
    else:
        code, output = run_command(cmd, capture, format_obj.accept_ansi, pty, stdin)

    if not silent:
        template = env.from_string(format_obj.template)
        ansiprint(
            template.render(
                {
                    "title": title,
                    "command": command,
                    "code": code,
                    "success": code == 0,
                    "failure": code != 0,
                    "number": number,
                    "output": output,
                    "nofail": nofail,
                    "quiet": quiet,
                    "silent": silent,
                },
            ),
        )

    return RunResult(0 if nofail else code, output)


def run_command(
    cmd: CmdType,
    capture: Capture = Capture.BOTH,
    ansi: bool = False,
    pty: bool = False,
    stdin: Optional[str] = None,
) -> Tuple[int, str]:
    """
    Run a command.

    Arguments:
        cmd: The command to run.
        capture: The output to capture.
        ansi: Whether to accept ANSI sequences.
        pty: Whether to run in a PTY.
        stdin: String to use as standard input.

    Returns:
        The exit code and the command output.
    """
    shell = isinstance(cmd, str)

    # if chosen format doesn't accept ansi, or on Windows, don't use pty
    if pty and (not ansi or WINDOWS):
        pty = False

    # pty can only combine, so only use pty when combining
    if pty and capture in {Capture.BOTH, Capture.NONE}:
        if shell:
            cmd = ["sh", "-c", cmd]  # type: ignore  # we know cmd is str
        return run_pty_subprocess(cmd, capture, stdin)  # type: ignore  # we made sure cmd is a list

    # we are on Windows
    if WINDOWS:
        # make sure the process can find the executable
        if not shell:
            cmd[0] = shutil.which(cmd[0]) or cmd[0]  # type: ignore  # we know cmd is a list
        return run_subprocess(cmd, capture, shell=shell, stdin=stdin)  # noqa: S604 (shell=True)

    return run_subprocess(cmd, capture, shell=shell, stdin=stdin)  # noqa: S604 (shell=True)


def run_function(
    func,
    args=None,
    kwargs=None,
    capture: Capture = Capture.BOTH,
    stdin: Optional[str] = None,
) -> Tuple[int, str]:
    """
    Run a function.

    Arguments:
        func: The function to run.
        args: Positional arguments passed to the function.
        kwargs: Keyword arguments passed to the function.
        capture: The output to capture.
        stdin: String to use as standard input.

    Returns:
        The exit code and the function output.
    """
    args = args or []
    kwargs = kwargs or {}

    if capture == Capture.NONE:
        return run_function_get_code(func, sys.stderr, args, kwargs), ""

    with stdbuffer(stdin) as buffer:
        if capture == Capture.BOTH:
            # combining stdout and stderr
            # -> redirect stderr to stdout
            buffer.stderr = buffer.stdout
            sys.stderr = buffer.stdout

        code = run_function_get_code(func, buffer.stderr, args, kwargs)

        if capture == Capture.STDERR:
            output = buffer.stderr.getvalue()
        else:
            output = buffer.stdout.getvalue()

    return code, output


def run_function_get_code(func: Callable, stderr, args=None, kwargs=None) -> int:  # noqa: WPS212 (return statements)
    """
    Run a function and return a exit code.

    Arguments:
        func: The function to run.
        stderr: A file descriptor to write potential tracebacks.
        args: Positional arguments passed to the function.
        kwargs: Keyword arguments passed to the function.

    Returns:
        An exit code.
    """
    try:
        result = func(*args, **kwargs)
    except Exception:  # noqa: W0703 (catching Exception on purpose)
        stderr.write(traceback.format_exc() + "\n")
        return 1

    # first check True and False
    # because int(True) == 1 and int(False) == 0
    if result is True:
        return 0
    if result is False:
        return 1
    try:
        return int(result)
    except (ValueError, TypeError):
        if result is None or bool(result):
            return 0
        return 1

Functions to run commands and capture output. A simple placeholder for three memory buffers. Initialize the object.          Arguments:             stdinput: String to use as standard input.             stdout: A buffer for standard output.             stderr: A buffer for standard error. Capture output in a `with` statement.      Arguments:         stdinput: String to use as standard input.      Yields:         An instance of `StdBuffer`. Placeholder for a run result. Initialize the object.          Arguments:             code: The exit code of the command.             output: The output of the command. # noqa: WPS231 (high complexity) Run a command in a subprocess or a Python function, and print its output if it fails.      Arguments:         cmd: The command to run.         args: Arguments to pass to the callable.         kwargs: Keyword arguments to pass to the callable.         number: The command number.         capture: The output to capture.         title: The command title.         fmt: The output format.         pty: Whether to run in a PTY.         progress: Whether to show progress.         nofail: Whether to always succeed.         quiet: Whether to not print the command output.         silent: Don't print anything.         stdin: String to use as standard input.      Returns:         The command exit code, or 0 if `nofail` is True. # type: ignore # noqa: S701 (no HTML: no need to escape) Run a command.      Arguments:         cmd: The command to run.         capture: The output to capture.         ansi: Whether to accept ANSI sequences.         pty: Whether to run in a PTY.         stdin: String to use as standard input.      Returns:         The exit code and the command output. # if chosen format doesn't accept ansi, or on Windows, don't use pty # pty can only combine, so only use pty when combining # type: ignore  # we know cmd is str # type: ignore  # we made sure cmd is a list # we are on Windows # make sure the process can find the executable # type: ignore  # we know cmd is a list # noqa: S604 (shell=True) # noqa: S604 (shell=True) Run a function.      Arguments:         func: The function to run.         args: Positional arguments passed to the function.         kwargs: Keyword arguments passed to the function.         capture: The output to capture.         stdin: String to use as standard input.      Returns:         The exit code and the function output. # combining stdout and stderr # -> redirect stderr to stdout # noqa: WPS212 (return statements) Run a function and return a exit code.      Arguments:         func: The function to run.         stderr: A file descriptor to write potential tracebacks.         args: Positional arguments passed to the function.         kwargs: Keyword arguments passed to the function.      Returns:         An exit code. # noqa: W0703 (catching Exception on purpose) # first check True and False # because int(True) == 1 and int(False) == 0