// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.run;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.intellij.execution.DefaultExecutionResult;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.CommandLineState;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.execution.configurations.GeneralCommandLine.ParentEnvironmentType;
import com.intellij.execution.configurations.ParametersList;
import com.intellij.execution.configurations.PtyCommandLine;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.filters.UrlFilter;
import com.intellij.execution.impl.ProcessStreamsSynchronizer;
import com.intellij.execution.process.*;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.target.*;
import com.intellij.execution.target.local.LocalTargetEnvironment;
import com.intellij.execution.target.value.TargetEnvironmentFunctions;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.facet.Facet;
import com.intellij.facet.FacetManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.PersistentLibraryKind;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.encoding.EncodingProjectManager;
import com.intellij.remote.ProcessControlWithMappings;
import com.intellij.remote.RemoteSdkProperties;
import com.intellij.remote.TargetAwarePathMappingProvider;
import com.intellij.util.PathMappingSettings;
import com.intellij.util.PlatformUtils;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.execution.ParametersListUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.console.PyDebugConsoleBuilder;
import com.jetbrains.python.debugger.PyDebugRunner;
import com.jetbrains.python.debugger.PyDebuggerOptionsProvider;
import com.jetbrains.python.debugger.PyTargetPathMapper;
import com.jetbrains.python.facet.LibraryContributingFacet;
import com.jetbrains.python.facet.PythonPathContributingFacet;
import com.jetbrains.python.library.PythonLibraryType;
import com.jetbrains.python.packaging.PyExecutionException;
import com.jetbrains.python.remote.PyRemotePathMapper;
import com.jetbrains.python.remote.PyRemoteSdkAdditionalData;
import com.jetbrains.python.run.features.PyRunToolIds;
import com.jetbrains.python.run.features.PyRunToolParameters;
import com.jetbrains.python.run.features.PyRunToolProvider;
import com.jetbrains.python.run.features.PyRunToolUsageCollector;
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest;
import com.jetbrains.python.run.target.PySdkTargetPaths;
import com.jetbrains.python.run.target.PythonCommandLineTargetEnvironmentProvider;
import com.jetbrains.python.sdk.*;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import com.jetbrains.python.sdk.flavors.conda.CondaPythonExecKt;
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
import com.jetbrains.python.target.PyTargetAwareAdditionalData;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;

import static com.intellij.execution.util.EnvFilesUtilKt.configureEnvsFromFiles;
import static com.jetbrains.python.run.PythonScriptCommandLineState.getExpandedWorkingDir;
import static com.jetbrains.python.run.features.PyRunToolExtKt.useRunTool;
import static com.jetbrains.python.run.features.PyRunToolProviderKt.getEnableRunTool;

/**
 * Since this state is async, any method could be called on any thread
 *
 * @author traff, Leonid Shalupov
 */
public abstract class PythonCommandLineState extends CommandLineState {
  // command line has a number of fixed groups of parameters; patchers should only operate on them and not the raw list.

  public static final String GROUP_EXE_OPTIONS = "Exe Options";
  public static final String GROUP_DEBUGGER = "Debugger";
  public static final String GROUP_PROFILER = "Profiler";
  public static final String GROUP_COVERAGE = "Coverage";
  /**
   * This group is applied for Python module execution. In this case it
   * contains two parameters: {@code -m} and the module name.
   * <p>
   * For Python script execution this group must be empty.
   * <p>
   * Note that this option <cite>terminates option list</cite> so this group
   * must go <b>after</b> other Python interpreter options. At the same time it
   * must go <b>before</b> <cite>arguments passed to program in
   * sys.argv[1:]</cite>, which are stored in {@link #GROUP_SCRIPT}.
   */
  public static final String GROUP_MODULE = "Module";
  //TODO: DOC ParametersListUtil
  public static final String GROUP_SCRIPT = "Script";
  public static final String MODULE_PARAMETER = "-m";

  /**
   * The port number to use for a server socket. '0' means the port will be automatically allocated.
   */
  private static final int SERVER_SOCKET_PORT = 0;

  /**
   * The length of the backlog to use for a server socket. '0' means the length of queue will be chosen by Java Platform.
   */
  private static final int SERVER_SOCKET_BACKLOG = 0;

  private final AbstractPythonRunConfiguration<?> myConfig;

  private Boolean myMultiprocessDebug = null;
  private boolean myRunWithPty = PtyCommandLine.isEnabled();

  public boolean isDebug() {
    return getEnvironment().getExecutor() instanceof DefaultDebugExecutor &&
           getEnvironment().getRunProfile() instanceof AbstractPythonRunConfiguration;
  }

  public static ServerSocket createServerSocket() throws ExecutionException {
    final ServerSocket serverSocket;
    try {
      serverSocket = new ServerSocket(SERVER_SOCKET_PORT, SERVER_SOCKET_BACKLOG, InetAddress.getLoopbackAddress());
    }
    catch (IOException e) {
      throw new ExecutionException(PyBundle.message("runcfg.error.message.failed.to.find.free.socket.port"), e);
    }
    return serverSocket;
  }

  public PythonCommandLineState(AbstractPythonRunConfiguration<?> runConfiguration, ExecutionEnvironment env) {
    super(env);
    myConfig = runConfiguration;
  }

  public @Nullable PythonSdkFlavor getSdkFlavor() {
    return PythonSdkFlavor.getFlavor(myConfig.getInterpreterPath());
  }

  public @Nullable Sdk getSdk() {
    return myConfig.getSdk();
  }

  public AbstractPythonRunConfiguration<?> getConfig() {
    return myConfig;
  }

  @Override
  public @NotNull ExecutionResult execute(@NotNull Executor executor, @NotNull ProgramRunner<?> runner) throws ExecutionException {
    if (runner instanceof PyDebugRunner || runner instanceof PythonRunner) {
      return execute(executor, new CommandLinePatcher[0]);
    }
    return executeWithoutStartProcess(executor);
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  public ExecutionResult execute(Executor executor, CommandLinePatcher... patchers) throws ExecutionException {
    return execute(executor, getDefaultPythonProcessStarter(), patchers);
  }

  public @Nullable ExecutionResult execute(@NotNull Executor executor) throws ExecutionException {
    return execute(executor, (targetEnvironmentRequest, pythonScript) -> pythonScript);
  }

  public final @NotNull List<String> getConfiguredInterpreterParameters() {
    String interpreterOptions = myConfig.getInterpreterOptions();
    if (!StringUtil.isEmptyOrSpaces(interpreterOptions)) {
      return ParametersListUtil.parse(interpreterOptions);
    }
    else {
      return Collections.emptyList();
    }
  }

  private @NotNull ExecutionResult executeWithoutStartProcess(@NotNull Executor executor) throws ExecutionException {
    ProcessHandler processHandler = new SimpleProcessHandler();
    ConsoleView console = createAndAttachConsoleInEDT(myConfig.getProject(), processHandler, executor);
    return new DefaultExecutionResult(console, processHandler, createActions(console, processHandler));
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  public ExecutionResult execute(Executor executor,
                                 PythonProcessStarter processStarter,
                                 CommandLinePatcher... patchers) throws ExecutionException {
    final ProcessHandler processHandler = startProcess(processStarter, patchers);
    ConsoleView console = createAndAttachConsoleInEDT(myConfig.getProject(), processHandler, executor);
    return new DefaultExecutionResult(console, processHandler, createActions(console, processHandler));
  }

  @ApiStatus.Internal
  public @NotNull ConsoleView createAndAttachConsoleInEDT(@NotNull Project project, ProcessHandler processHandler, Executor executor)
    throws ExecutionException {
    final Ref<Object> consoleRef = Ref.create();
    ApplicationManager.getApplication().invokeAndWait(
      () -> {
        try {
          consoleRef.set(createAndAttachConsole(project, processHandler, executor));
        }
        catch (ExecutionException | RuntimeException e) {
          consoleRef.set(e);
        }
      });

    if (consoleRef.get() instanceof ExecutionException) {
      throw (ExecutionException)consoleRef.get();
    }
    else if (consoleRef.get() instanceof RuntimeException) throw (RuntimeException)consoleRef.get();

    return (ConsoleView)consoleRef.get();
  }

  /**
   * Please do not overuse {@code null} return value. {@code null} value is utilized by {@link PythonScriptCommandLineState} when starting
   * Python run configurations with "Run with Python Console" flag. It is more a workaround, so please annotate the overridden methods with
   * {@link NotNull} where possible.
   */
  public @Nullable ExecutionResult execute(@NotNull Executor executor,
                                           @NotNull PythonScriptTargetedCommandLineBuilder converter) throws ExecutionException {
    final ProcessHandler processHandler = startProcess(converter);
    final ConsoleView console = createAndAttachConsoleInEDT(myConfig.getProject(), processHandler, executor);
    return new DefaultExecutionResult(console, processHandler, createActions(console, processHandler));
  }

  protected @NotNull ConsoleView createAndAttachConsole(Project project, ProcessHandler processHandler, Executor executor)
    throws ExecutionException {
    final ConsoleView consoleView = createConsoleBuilder(project).getConsole();
    consoleView.addMessageFilter(createUrlFilter(processHandler));
    consoleView.addMessageFilter(new PythonImportErrorFilter(project));

    addTracebackFilter(project, consoleView, processHandler);

    consoleView.attachToProcess(processHandler);
    return consoleView;
  }

  protected void addTracebackFilter(@NotNull Project project, @NotNull ConsoleView consoleView, @NotNull ProcessHandler processHandler) {
    // TODO workaround
    if (PythonSdkUtil.isRemote(myConfig.getSdk()) && processHandler instanceof ProcessControlWithMappings) {
      consoleView
        .addMessageFilter(new PyRemoteTracebackFilter(project, getExpandedWorkingDir(myConfig), (ProcessControlWithMappings)processHandler));
    }
    else {
      consoleView.addMessageFilter(new PythonTracebackFilter(project, myConfig.getWorkingDirectorySafe()));
    }
    consoleView.addMessageFilter(createUrlFilter(processHandler)); // Url filter is always nice to have
  }

  private TextConsoleBuilder createConsoleBuilder(Project project) {
    if (isDebug()) {
      return new PyDebugConsoleBuilder(project, PythonSdkUtil.findSdkByPath(myConfig.getInterpreterPath()));
    }
    else {
      return TextConsoleBuilderFactory.getInstance().createBuilder(project);
    }
  }

  @Override
  protected @NotNull ProcessHandler startProcess() throws ExecutionException {
    return startProcess(getDefaultPythonProcessStarter());
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on {@link GeneralCommandLine}.</i>
   * <p>
   * Patches the command line parameters applying patchers from first to last, and then runs it.
   *
   * @param patchers any number of patchers; any patcher may be null, and the whole argument may be null.
   * @return handler of the started process
   */
  protected @NotNull ProcessHandler startProcess(PythonProcessStarter processStarter, CommandLinePatcher... patchers) throws ExecutionException {
    GeneralCommandLine commandLine = generateCommandLine(patchers);

    // Extend command line
    PythonRunConfigurationExtensionsManager.Companion.getInstance()
      .patchCommandLine(myConfig, getRunnerSettings(), commandLine, getEnvironment().getRunner().getRunnerId());

    ProcessHandler processHandler = processStarter.start(myConfig, commandLine);

    // attach extensions
    PythonRunConfigurationExtensionsManager.Companion.getInstance()
      .attachExtensionsToProcess(myConfig, processHandler, getRunnerSettings());

    return processHandler;
  }

  /**
   * Starts the Python script and returns the process handler associated with
   * it.
   * <p>
   * <i>Note that {@code patchCommandLine()} method of
   * {@link PythonRunConfigurationExtensionsManager} cannot be used with
   * {@link TargetedCommandLine} and so it is ignored.</i>
   */
  protected @NotNull ProcessHandler startProcess(@NotNull PythonScriptTargetedCommandLineBuilder builder)
    throws ExecutionException {
    HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest = getPythonTargetInterpreter();

    Sdk sdk = getSdk();
    if (sdk != null) {
      RunConfigurationTargetEnvironmentAdjuster adjuster =
        RunConfigurationTargetEnvironmentAdjuster.Factory.findTargetEnvironmentRequestAdjuster(sdk);
      if (adjuster != null) {
        adjuster.adjust(helpersAwareTargetRequest.getTargetEnvironmentRequest(), myConfig);
      }
    }

    // The original Python script to be executed
    PythonExecution pythonScript = buildPythonExecutionFinal(helpersAwareTargetRequest, getSdk());

    // Python script that may be the debugger script that runs the original script
    PythonExecution realPythonExecution = builder.build(helpersAwareTargetRequest, pythonScript);

    if (myConfig instanceof PythonRunConfiguration pythonConfig) {
      String inputFilePath = pythonConfig.getInputFile();
      if (pythonConfig.isRedirectInput() && !StringUtil.isEmptyOrSpaces(inputFilePath)) {
        realPythonExecution.withInputFile(new File(inputFilePath));
      }
    }

    // TODO [Targets API] [major] Meaningful progress indicator should be taken
    EmptyProgressIndicator progressIndicator = new EmptyProgressIndicator();
    TargetEnvironment targetEnvironment =
      helpersAwareTargetRequest.getTargetEnvironmentRequest().prepareEnvironment(TargetProgressIndicator.EMPTY);

    // TODO Detect and discard existing overrides of configured parameters.
    List<String> allInterpreterParameters =
      Streams.concat(getConfiguredInterpreterParameters().stream(), realPythonExecution.getAdditionalInterpreterParameters().stream())
        .toList();

    PyRunToolParameters runToolParameters = null;
    if (sdk != null && getEnableRunTool()) {
      PyRunToolProvider runToolProvider = PyRunToolProvider.forSdk(sdk);
      if (runToolProvider != null && useRunTool(myConfig, sdk)) {
        runToolParameters = PythonCommandLineStateExKt.getRunToolParametersForJvm(runToolProvider);
        PyRunToolUsageCollector.logRun(myConfig.getProject(), PyRunToolIds.idOf(runToolProvider));
      }
    }

    TargetedCommandLine targetedCommandLine =
      PythonScripts.buildTargetedCommandLine(realPythonExecution, targetEnvironment, sdk, allInterpreterParameters, myRunWithPty,
                                             runToolParameters);

    // TODO [Targets API] `myConfig.isPassParentEnvs` must be handled (at least for the local case)
    ProcessHandler processHandler = doStartProcess(targetEnvironment, targetedCommandLine, progressIndicator);

    // Attach extensions
    PythonRunConfigurationExtensionsManager.Companion.getInstance()
      .attachExtensionsToProcess(myConfig, processHandler, getRunnerSettings());

    processHandler.addProcessListener(new ProcessListener() {
      @Override
      public void processTerminated(@NotNull ProcessEvent event) {
        targetEnvironment.shutdown();
      }
    });

    return processHandler;
  }

  private @NotNull PythonExecution buildPythonExecutionFinal(HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest, @Nullable Sdk sdk) {
    TargetEnvironmentRequest targetEnvironmentRequest = helpersAwareTargetRequest.getTargetEnvironmentRequest();
    PythonExecution pythonExecution = buildPythonExecution(helpersAwareTargetRequest);
    pythonExecution.setWorkingDir(getPythonExecutionWorkingDir(targetEnvironmentRequest));
    initEnvironment(myConfig.getProject(), pythonExecution, myConfig, createRemotePathMapper(), isDebug(), helpersAwareTargetRequest, sdk);
    customizePythonExecutionEnvironmentVars(helpersAwareTargetRequest, pythonExecution.getEnvs(), myConfig.isPassParentEnvs());
    PythonScripts.ensureProjectSdkAndModuleDirsAreOnTarget(targetEnvironmentRequest,
                                                           myConfig.getProject(),
                                                           myConfig.getModule() != null ? new Module[]{myConfig.getModule()} : Module.EMPTY_ARRAY);
    return pythonExecution;
  }

  /**
   * Returns the promise to the working directory path for this
   * {@link PythonCommandLineState}. The working directory is resolved within
   * the uploads that are registered in the provided request.
   *
   * @param targetEnvironmentRequest the environment to explore for the working directory upload
   * @return the promise to the working directory path
   */
  protected @Nullable Function<TargetEnvironment, String> getPythonExecutionWorkingDir(@NotNull TargetEnvironmentRequest targetEnvironmentRequest) {
    // the following working directory is located on the local machine
    String workingDir = myConfig.getWorkingDirectorySafe();
    if (!StringUtil.isEmptyOrSpaces(workingDir)) {
      return getTargetPath(targetEnvironmentRequest, Path.of(workingDir));
    }
    return null;
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  protected final @NotNull PythonProcessStarter getDefaultPythonProcessStarter() {
    return (config, commandLine) -> {
      Sdk sdk = PythonSdkUtil.findSdkByPath(myConfig.getInterpreterPath());
      assert sdk != null : "No SDK For " + myConfig.getInterpreterPath();
      final ProcessHandler processHandler;
      var additionalData = sdk.getSdkAdditionalData();
      if (additionalData instanceof PyRemoteSdkAdditionalDataMarker) {
        assert additionalData instanceof PyRemoteSdkAdditionalData : "additionalData is remote, but not legacy. Is it a target-based? " +
                                                                     additionalData;
        PyRemotePathMapper pathMapper = createRemotePathMapper();
        processHandler = PyRemoteProcessStarter.startLegacyRemoteProcess((PyRemoteSdkAdditionalData)additionalData, commandLine,
                                                                         myConfig.getProject(), pathMapper);
      }
      else {
        processHandler = doCreateProcess(commandLine);
        ProcessTerminatedListener.attach(processHandler);
      }
      return processHandler;
    };
  }

  private @NotNull ProcessHandler doStartProcess(@NotNull TargetEnvironment targetEnvironment,
                                                 @NotNull TargetedCommandLine commandLine,
                                                 @NotNull ProgressIndicator progressIndicator) throws ExecutionException {
    final ProcessHandler processHandler;
    Process process = targetEnvironment.createProcess(commandLine, progressIndicator);
    // TODO [Targets API] [major] The command line should be prefixed with the interpreter identifier (f.e. Docker container id)
    String commandLineString = StringUtil.join(commandLine.getCommandPresentation(targetEnvironment), " ");
    processHandler = createPtyAwaredProcessHandler(process, commandLineString, targetEnvironment, commandLine);
    ProcessTerminatedListener.attach(processHandler);
    return processHandler;
  }

  protected final @Nullable PyRemotePathMapper createRemotePathMapper() {
    if (myConfig.getMappingSettings() == null) {
      return null;
    }
    else {
      return PyRemotePathMapper.fromSettings(myConfig.getMappingSettings(), PyRemotePathMapper.PyPathMappingType.USER_DEFINED);
    }
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   * <p>
   * Generate command line and apply patchers.
   *
   * @param patchers array of patchers
   * @return generated command line changed by patchers
   */
  public final @NotNull GeneralCommandLine generateCommandLine(CommandLinePatcher @Nullable [] patchers) {
    return applyPatchers(generateCommandLine(), patchers);
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   * <p>
   * Apply patchers to the given command line
   *
   * @param commandLine command line to change
   * @param patchers    array of patchers
   * @return command line changed by patchers
   */
  private static @NotNull GeneralCommandLine applyPatchers(@NotNull GeneralCommandLine commandLine, CommandLinePatcher @Nullable [] patchers) {
    if (patchers != null) {
      for (CommandLinePatcher patcher : patchers) {
        if (patcher != null) patcher.patchCommandLine(commandLine);
      }
    }
    return commandLine;
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  protected ProcessHandler doCreateProcess(GeneralCommandLine commandLine) throws ExecutionException {
    return PythonProcessRunner.createProcess(commandLine);
  }

  private @NotNull ProcessHandler createPtyAwaredProcessHandler(@NotNull Process process,
                                                                @NotNull String commandLineString,
                                                                @NotNull TargetEnvironment targetEnvironment,
                                                                @NotNull TargetedCommandLine commandLine) {
    ProcessHandler processHandler = createProcessHandler(process, commandLineString, targetEnvironment, commandLine);
    if (processHandler instanceof OSProcessHandler osProcessHandler) {
      osProcessHandler.setHasPty(myRunWithPty);
    }
    return processHandler;
  }

  protected @NotNull ProcessHandler createProcessHandler(@NotNull Process process,
                                                         @NotNull String commandLineString,
                                                         @NotNull TargetEnvironment targetEnvironment,
                                                         @NotNull TargetedCommandLine commandLine) {
    if (targetEnvironment instanceof LocalTargetEnvironment) {
      // TODO This special treatment of local target must be replaced with a generalized approach
      //  (f.e. with an ability of a target environment to match arbitrary local path to a target one)
      if (isDebug()) {
        return new PyDebugProcessHandler(process, commandLineString, commandLine.getCharset());
      }
      return new PythonProcessHandler(process, commandLineString, commandLine.getCharset());
    }
    PathMappingSettings pathMappingSettings = new PathMappingSettings();
    // add mappings from run configuration on top
    PathMappingSettings runConfigurationPathMappings = myConfig.myMappingSettings;
    if (runConfigurationPathMappings != null) {
      pathMappingSettings.addAll(runConfigurationPathMappings);
    }
    // add path mappings configured in SDK, they will be handled in second place
    PathMappingSettings sdkPathMappings = getSdkPathMappings();
    if (sdkPathMappings != null) {
      // filter out any deployment paths, as we want to resolve sources to their local counterparts when possible rather
      // than the files copied from the remote end
      var deploymentPaths = getDeploymentPaths();
      pathMappingSettings.addAll(
        sdkPathMappings.getPathMappings()
          .stream()
          .filter(mapping -> !deploymentPaths.contains(mapping.getRemoteRoot()))
          .toList()
      );
    }
    final boolean isMostlySilentProcess = false;
    PyTargetPathMapper consolidatedPathMappings = new PyTargetPathMapper(targetEnvironment, pathMappingSettings);
    return PyCustomProcessHandlerProvider.createProcessHandler(process, targetEnvironment, commandLineString, commandLine.getCharset(),
                                                               consolidatedPathMappings, isMostlySilentProcess, myRunWithPty);
  }

  /**
   * Collects deployment paths from suitable mapping providers.
   * <p>
   * If the current SDK additional data is not a {@code PyTargetAwareAdditionalData}, then an empty set is returned.
   *
   * @return a set of paths on remote file systems
   */
  private @NotNull Set<String> getDeploymentPaths() {
    Sdk sdk = myConfig.getSdk();
    Set<String> deploymentPaths = new HashSet<>();
    if (sdk != null) {
      SdkAdditionalData sdkAdditionalData = sdk.getSdkAdditionalData();
      if (sdkAdditionalData instanceof PyTargetAwareAdditionalData data) {
        var providers = TargetAwarePathMappingProvider.Companion.getSuitableMappingProviders(data);
        for (TargetAwarePathMappingProvider provider : providers) {
          var pathMappings = provider.getPathMappingSettings(myConfig.getProject(), data).getPathMappings();
          for (PathMappingSettings.PathMapping mapping : pathMappings) {
            deploymentPaths.add(mapping.getRemoteRoot());
          }
        }
      }
    }
    return deploymentPaths;
  }

  private @Nullable PathMappingSettings getSdkPathMappings() {
    Sdk sdk = myConfig.getSdk();
    if (sdk != null) {
      SdkAdditionalData sdkAdditionalData = sdk.getSdkAdditionalData();
      if (sdkAdditionalData instanceof RemoteSdkProperties) {
        return ((RemoteSdkProperties)sdkAdditionalData).getPathMappings();
      }
    }
    return null;
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   * <p>
   * Generate command line from run configuration.
   * It can be overridden if commandline shouldn't be based on the run configuration or when it requires some additional changes
   * before patchers applying.
   *
   * @return generated command line
   */
  public @NotNull GeneralCommandLine generateCommandLine() {
    SdkAdditionalData data = null;
    if (myConfig.getSdk() != null) {
      data = myConfig.getSdk().getSdkAdditionalData();
    }
    GeneralCommandLine commandLine = createPythonCommandLine(myConfig.getProject(), data, myConfig, isDebug(), myRunWithPty);

    buildCommandLineParameters(commandLine);

    customizeEnvironmentVars(commandLine.getEnvironment(), myConfig.isPassParentEnvs());

    ProcessStreamsSynchronizer.redirectErrorStreamIfNeeded(commandLine);

    return commandLine;
  }

  /**
   * Builds {@link PythonExecution}.
   * <p>
   * User volumes (including the volumes for project files) are expected to be
   * already requested.
   *
   * @param helpersAwareRequest the request
   * @return the representation of Python script or module execution
   */
  @RequiresBackgroundThread
  protected @NotNull PythonExecution buildPythonExecution(@NotNull HelpersAwareTargetEnvironmentRequest helpersAwareRequest) {
    throw new UnsupportedOperationException("The implementation of Run Configuration based on Targets API is absent");
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  public static @NotNull GeneralCommandLine createPythonCommandLine(Project project,
                                                           @Nullable SdkAdditionalData data,
                                                           PythonRunParams config,
                                                           boolean isDebug,
                                                           boolean runWithPty) {
    GeneralCommandLine commandLine = generalCommandLine(runWithPty);

    commandLine.withCharset(EncodingProjectManager.getInstance(project).getDefaultCharset());

    createStandardGroups(commandLine);

    initEnvironment(project, data, commandLine, config, isDebug);

    setRunnerPath(project, commandLine, config);

    return commandLine;
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  private static GeneralCommandLine generalCommandLine(boolean runWithPty) {
    return runWithPty ? new PtyCommandLine().withConsoleMode(false) : new GeneralCommandLine();
  }

  /**
   * Creates a number of parameter groups in the command line:
   * GROUP_EXE_OPTIONS, GROUP_DEBUGGER, GROUP_SCRIPT.
   * These are necessary for command line patchers to work properly.
   */
  public static void createStandardGroups(GeneralCommandLine commandLine) {
    ParametersList params = commandLine.getParametersList();
    params.addParamsGroup(GROUP_EXE_OPTIONS);
    params.addParamsGroup(GROUP_DEBUGGER);
    params.addParamsGroup(GROUP_PROFILER);
    params.addParamsGroup(GROUP_COVERAGE);
    params.addParamsGroup(GROUP_MODULE);
    params.addParamsGroup(GROUP_SCRIPT);
  }

  /**
   * <i>To be deprecated. The part of the legacy implementation based on
   * {@link GeneralCommandLine}.</i>
   */
  protected static void initEnvironment(Project project,
                                        SdkAdditionalData data,
                                        @NotNull GeneralCommandLine commandLine,
                                        @NotNull PythonRunParams runParams,
                                        boolean isDebug) {
    final Map<String, String> env = prepareEnv(project, runParams, true);
    setupEncodingEnvs(env, commandLine.getCharset());
    commandLine.getEnvironment().clear();
    commandLine.getEnvironment().putAll(env);
    commandLine.withParentEnvironmentType(runParams.isPassParentEnvs() ? ParentEnvironmentType.CONSOLE : ParentEnvironmentType.NONE);

    buildPythonPath(project, commandLine, runParams, isDebug);

    for (PythonCommandLineEnvironmentProvider envProvider : PythonCommandLineEnvironmentProvider.EP_NAME.getExtensionList()) {
      envProvider.extendEnvironment(project, data, commandLine, runParams);
    }
  }

  public static void initEnvironment(@NotNull Project project,
                                     @NotNull PythonExecution commandLine,
                                     @NotNull PythonRunParams runParams,
                                     @NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest,
