import os
import platform
import shlex
import shutil
import subprocess

from io import StringIO

import mslex

from wox.logger.filters import WoxContext
from wox.logger.logger_setup import wox_logger
from wox.wox_exceptions import (
    CommandNotFoundError,
    PythonInterpreterCallError,
    WoxException
)


# Only used by pytest summoning subprocessor directly without passing by main entrypoint.
wox_logger.addFilter(WoxContext({'task': 'pytest', 'env': 'subprocess'}))

def sanitize_command(command, python_interpreter_path = None):
    """
        Sanitizes a command before execution by resolving its executable path.

        Parameters
        ----------
        command : str
            The command line string to execute.
        python_interpreter_path : pathlib.Path or None
            Path to the Python binary of a virtual environment.
            If None, uses the system PATH.

        Raises
        ------
        PythonInterpreterCallError
            Command tries to launch an interactive Python session.
        CommandNotFoundError
            Executable for the command not found.

        Returns
        -------
        list[str]
            A list representing the sanitized command ready for subprocess.
    """

    match platform.system():
        case 'Linux':
            command = shlex.split(command)
        case 'Windows':
            command = mslex.split(command, like_cmd = False)

    # --- Special case: user launched "python" alone ---
    if (command[0] == 'py' or command[0] == 'python') and len(command) == 1:
        raise PythonInterpreterCallError(command[0])

    cmd, options = command[0], command[1:]

    # --- PATH resolution ---
    if python_interpreter_path:
        # Add venv's bin folder first in PATH
        path = str(python_interpreter_path.parent) + os.pathsep + os.environ['PATH']
        # Replace `python` by explicit path to the venv interpreter
        if cmd == 'python':
            cmd = str(python_interpreter_path)
    else:
        path = os.environ['PATH']

    # --- Resolve executable ---
    executable = shutil.which(cmd = cmd, path = path)
    if not executable:
        raise CommandNotFoundError(cmd)

    return [executable] + options

def run_command(command,
        unsafe_command = False, shell = False,
        return_output = False,
        python_interpreter = None,
        timeout = 2
    ):
    """
        Executes the command in subprocess.

        Parameters
        ----------
        command : str
            Command passed to subprocess.
        unsafe_command : bool, default False
            If True, the command is not sanitized and use subprocess as shell.
        shell : bool, default False
            Use subprocess as shell, which implies security issues.
        return_output : bool, default False
            If True, the subprocess returns the output from its stderr.
        PythonInterpreter
            Python interpreter with its version and path.
        timeout : int, default 10
            Delay in seconds before subprocess throws a TimeoutError.

        Returns
        -------
        str
            The output of subprocess' stderr. If return_output is True.

        Raises
        ------
        DependencyConflictError
            Dependency conflict occurs during package installation.
        DiskFullError
            Disk is full and command cannot write data.
        PipTimeoutError
            Pip cannot reach package server.
        PackageNotFoundError
            Package is missing or wrongly spelled.
        PermissionDeniedError
            Permissions prevent writing data.
        PythonInterpreterNotFoundError
            The specified Python interpreter cannot be found.
        PythonInterpreterUnstableError
            The Python interpreter crashes at the C-level.
        subprocess.CalledProcessError
            Fallback for unhandled errors.
    """

    if unsafe_command:
        shell = True
    else:
        command = sanitize_command(
            command, python_interpreter.path if python_interpreter else None
        )

    env = os.environ.copy()
    if python_interpreter is not None:
        env['PATH'] = str(python_interpreter.path.parent) + os.pathsep + os.environ['PATH']
    if any(part.endswith('pytest') or part == 'pytest' for part in command):
        env['WOX_SUBPROCESSOR_NO_LOG'] = '1'

    wox_logger.info(f"Subprocess launched with command '{command}'.")
    wox_logger.info('Subprocess output - start')
    wox_logger.info('-'*25)

    with StringIO() as buffer:
        with subprocess.Popen(
            args = command,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE,
            shell = shell,
            env = env,
            text = True,
            encoding = 'utf-8',
            errors = 'replace'
        ) as process:

            for line in process.stdout:
                wox_logger.info(line.rstrip('\r\n'))
                if return_output:
                    buffer.write(line)

            for line in process.stderr:
                wox_logger.info(line.rstrip('\r\n'))
                buffer.write(line)

            process.wait(timeout = timeout)

            wox_logger.info('-'*23)
            wox_logger.info('Subprocess output - end')

            if process.returncode != 0:

                stderr_output = buffer.getvalue()
                if not stderr_output:
                    subprocess_error = subprocess.CalledProcessError(
                        process.returncode, command
                    )
                    stderr_output = str(subprocess_error).lower()

                try:
                    if unsafe_command is True and 'python' in command:
                        import wox.subprocess_error_detection.naive_python as error_detector

                        error_detector.detect_python_discovery_errors(command)
                    elif (
                        (len(command) > 3 and command[3] == 'pip') or \
                        (len(command) > 2 and command[2] == 'venv')
                    ):
                        import wox.subprocess_error_detection.pip_install as error_detector

                        error_detector.detect_pip_errors(
                            stderr_output,
                            command,
                            python_interpreter if python_interpreter else None
                        )

                    elif os.path.basename(command[0]) == 'uv':
                        import wox.subprocess_error_detection.uv_install as error_detector

                        error_detector.detect_uv_errors(
                            stderr_output,
                            command,
                            python_interpreter
                        )
                except WoxException:
                    raise
                else:
                    raise subprocess.CalledProcessError(process.returncode, command)

            if return_output:
                return buffer.getvalue().strip()
