// 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.debugger;

import com.intellij.codeWithMe.ClientId;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionManager;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.*;
import com.intellij.execution.console.LanguageConsoleBuilder;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.impl.ConsoleViewImpl;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.target.HostPort;
import com.intellij.execution.target.ResolvedPortBinding;
import com.intellij.execution.target.TargetEnvironment;
import com.intellij.execution.target.TargetEnvironmentRequest;
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest;
import com.intellij.execution.target.value.TargetEnvironmentFunctions;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.execution.ui.RunnerLayoutUi;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.AppUIExecutor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import com.intellij.util.net.NetUtils;
import com.intellij.xdebugger.*;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PythonHelper;
import com.jetbrains.python.console.*;
import com.jetbrains.python.console.pydev.ConsoleCommunicationListener;
import com.jetbrains.python.debugger.settings.PyDebuggerSettings;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.run.*;
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest;
import com.jetbrains.python.sdk.PySdkExtKt;
import com.jetbrains.python.sdk.flavors.CPythonSdkFlavor;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import com.jetbrains.python.sdk.legacy.PythonSdkUtil;
import kotlin.Unit;
import org.jetbrains.annotations.*;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.jetbrains.python.actions.PyExecuteInConsole.requestFocus;
import static com.jetbrains.python.actions.PyExecuteInConsole.selectConsoleTab;


public class PyDebugRunner implements ProgramRunner<RunnerSettings> {
  public static final @NonNls String PY_DEBUG_RUNNER = "PyDebugRunner";

  public static final @NonNls String DEBUGGER_MAIN = "pydev/pydevd.py";
  public static final @NonNls String CLIENT_PARAM = "--client";
  public static final @NonNls String PORT_PARAM = "--port";
  public static final @NonNls String FILE_PARAM = "--file";
  public static final @NonNls String MODULE_PARAM = "--module";
  public static final @NonNls String MULTIPROCESS_PARAM = "--multiprocess";
  public static final @NonNls String IDE_PROJECT_ROOTS = "IDE_PROJECT_ROOTS";
  public static final @NonNls String LIBRARY_ROOTS = "LIBRARY_ROOTS";
  public static final @NonNls String PYTHON_ASYNCIO_DEBUG = "PYTHONASYNCIODEBUG";
  public static final @NonNls String GEVENT_SUPPORT = "GEVENT_SUPPORT";
  public static final @NonNls String PYDEVD_FILTERS = "PYDEVD_FILTERS";
  public static final @NonNls String PYDEVD_FILTER_LIBRARIES = "PYDEVD_FILTER_LIBRARIES";
  public static final @NonNls String CYTHON_EXTENSIONS_DIR = Path.of(PathManager.getSystemPath(), "cythonExtensions").toString();

  protected static final @NonNls String PYDEVD_USE_CYTHON = "PYDEVD_USE_CYTHON";
  protected static final @NonNls String PYCHARM_DEBUG = "PYCHARM_DEBUG";
  protected static final @NonNls String USE_LOW_IMPACT_MONITORING = "USE_LOW_IMPACT_MONITORING";
  protected static final @NonNls String HALT_VARIABLE_RESOLVE_THREADS_ON_STEP_RESUME = "HALT_VARIABLE_RESOLVE_THREADS_ON_STEP_RESUME";

  private static final @NonNls String PYTHONPATH_ENV_NAME = "PYTHONPATH";

  private static final @NonNls String PYTHON_DONT_WRITE_PYC_FLAG = "-B";

  private static final @NonNls String PYTHON3_PYCACHE_PREFIX_OPTION = "pycache_prefix=";

  private static final Logger LOG = Logger.getInstance(PyDebugRunner.class);

  private PyDebugProcess pyDebugProcess = null;

  @Override
  public @NotNull String getRunnerId() {
    return PY_DEBUG_RUNNER;
  }

  @Override
  public boolean canRun(final @NotNull String executorId, final @NotNull RunProfile profile) {
    if (!DefaultDebugExecutor.EXECUTOR_ID.equals(executorId)) {
      // If not debug at all
      return false;
    }
    /*
     * Any python configuration is debuggable unless it explicitly declares itself as DebugAwareConfiguration and denies it
     * with canRunUnderDebug == false
     */

    if (profile instanceof WrappingRunConfiguration) {
      // If the configuration is wrapper -- unwrap it and check
      return isDebuggable(((WrappingRunConfiguration<?>)profile).getPeer());
    }
    return isDebuggable(profile);
  }

  private static boolean isDebuggable(final @NotNull RunProfile profile) {
    if (profile instanceof DebugAwareConfiguration) {
      // if configuration knows whether debug is allowed
      return ((DebugAwareConfiguration)profile).canRunUnderDebug();
    }
    if (profile instanceof AbstractPythonRunConfiguration) {
      // Any python configuration is debuggable
      return true;
    }
    // No even a python configuration
    return false;
  }

  @RequiresEdt
  protected Promise<@NotNull XDebugSession> createSession(@NotNull RunProfileState state, final @NotNull ExecutionEnvironment environment) {
    FileDocumentManager.getInstance().saveAllDocuments();
    return createSessionUsingTargetsApi(state, environment);
  }

  private @NotNull Promise<XDebugSession> createSessionUsingTargetsApi(@NotNull RunProfileState state,
                                                                       final @NotNull ExecutionEnvironment environment) {
    PythonCommandLineState pyState = (PythonCommandLineState)state;
    RunProfile profile = environment.getRunProfile();

    if (PyDebuggerOptionsProvider.getInstance(environment.getProject()).isRunDebuggerInServerMode() &&
        Registry.is("python.debug.use.single.port")) {
      int port = PyDebuggerOptionsProvider.getInstance(environment.getProject()).getDebuggerPort();
      TargetEnvironment.TargetPortBinding targetPortBinding =
        new TargetEnvironment.TargetPortBinding(port, port);
      return Promises
        .runAsync(() -> {
          try {
            var debuggerScriptCommandLineBuilder = new PythonDebuggerServerModeTargetedCommandLineBuilder(
              environment.getProject(), pyState, profile, targetPortBinding);
            return pyState.execute(environment.getExecutor(), debuggerScriptCommandLineBuilder);
          }
          catch (ExecutionException err) {
            throw new RuntimeException(err);
          }
        })
        .thenAsync(result -> AppUIExecutor.onUiThread().submit(() -> {
          return createXDebugSession(environment, port, result);
        }));
    }
    else {
      var clientId = ClientId.getCurrentOrNull();
      return Promises
        .runAsync(() -> {
          int serverLocalPort = findAvailableSocketPort();
          try {
            TargetEnvironment.LocalPortBinding localPortBinding =
              new TargetEnvironment.LocalPortBinding(serverLocalPort, null);
            var debuggerScriptCommandLineBuilder = new PythonDebuggerClientModeTargetedCommandLineBuilder(
              environment.getProject(), pyState, profile, localPortBinding);
            ExecutionResult result = pyState.execute(environment.getExecutor(), debuggerScriptCommandLineBuilder);
            ServerSocket serverSocket = debuggerScriptCommandLineBuilder.getServerSocketForDebugging();
            if (serverSocket == null) {
              LOG.error("The server socket has not been created after the target environment preparation" +
                        ", trying to fallback and create the server socket on the loopback address");
              serverSocket = createServerSocketOnLoopbackAddress(serverLocalPort);
            }
            return Pair.create(serverSocket, result);
          }
          catch (Exception err) {
            throw new RuntimeException(err.getMessage(), err);
          }
        })
        .thenAsync(pair -> AppUIExecutor.onUiThread().submit(() -> {
          try (AccessToken ignored = ClientId.withClientId(clientId)) {
            ServerSocket serverSocket = pair.getFirst();
            ExecutionResult result = pair.getSecond();
            return createXDebugSession(environment, pyState, serverSocket, result);
          }
        }));
    }
  }

  private static @NotNull ServerSocket createServerSocketOnLoopbackAddress(int serverLocalPort) {
    try {
      return new ServerSocket(serverLocalPort, 0, InetAddress.getLoopbackAddress());
    }
    catch (IOException e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }

  public static int findAvailableSocketPort() {
    try {
      return NetUtils.findAvailableSocketPort();
    }
    catch (IOException e) {
      throw new RuntimeException(PyBundle.message("runcfg.error.message.failed.to.find.free.socket.port"), e);
    }
  }

  private @NotNull XDebugSession createXDebugSession(@NotNull ExecutionEnvironment environment,
                                                     PythonCommandLineState pyState,
                                                     ServerSocket serverSocket, ExecutionResult result) throws ExecutionException {
    XDebugProcessStarter starter = new XDebugProcessStarter() {
      @Override
      public @NotNull XDebugProcess start(final @NotNull XDebugSession session) {
        pyDebugProcess = createDebugProcess(session, serverSocket, result, pyState);

        createConsoleCommunication(environment.getProject(), result, pyDebugProcess, session);
        return pyDebugProcess;
      }
    };
    return XDebuggerManager.getInstance(environment.getProject()).newSessionBuilder(starter)
      .environment(environment)
      .startSession().getSession();
  }

  private @NotNull XDebugSession createXDebugSession(@NotNull ExecutionEnvironment environment,
                                                     int serverPort, ExecutionResult result) throws ExecutionException {
    XDebugProcessStarter starter = new XDebugProcessStarter() {
      @Override
      public @NotNull XDebugProcess start(@NotNull XDebugSession session) {
        PyDebugProcess pyDebugProcess = createDebugProcess(session, serverPort, result);
        createConsoleCommunication(environment.getProject(), result, pyDebugProcess, session);
        return pyDebugProcess;
      }
    };
    return XDebuggerManager.getInstance(environment.getProject()).newSessionBuilder(starter)
      .environment(environment)
      .startSession().getSession();
  }

  protected @NotNull PyDebugProcess createDebugProcess(@NotNull XDebugSession session,
                                                       ServerSocket serverSocket,
                                                       ExecutionResult result,
                                                       PythonCommandLineState pyState) {
    return new PyDebugProcess(session, serverSocket, result.getExecutionConsole(), result.getProcessHandler(),
                              pyState.isMultiprocessDebug());
  }

  protected @NotNull PyDebugProcess createDebugProcess(@NotNull XDebugSession session, int serverPort, ExecutionResult result) {
    return new PyDebugProcess(session, result.getExecutionConsole(), result.getProcessHandler(), "localhost", serverPort);
  }

  /**
   * Calling this method with an SSH interpreter would lead to an error because this method is expected to be executed from EDT,
   * and therefore it would try to create an SSH connection in EDT.
   *
   * @deprecated Override {@link #execute(ExecutionEnvironment, RunProfileState)} instead.
   */
  @Deprecated(forRemoval = true)
  protected @NotNull RunContentDescriptor doExecute(@NotNull RunProfileState state, final @NotNull ExecutionEnvironment environment)
    throws ExecutionException {
    // The PyDebugRunner class might inherit AsyncProgramRunner, but some magic is added for backward compatibility
    // with GenericProgramRunner. Consider changing the base class when doExecute method is deleted.
    // Now the logic here is the same as in execute() except the fact that everything is called in EDT.
    FileDocumentManager.getInstance().saveAllDocuments();

    final PythonCommandLineState pyState = (PythonCommandLineState)state;
    Sdk sdk = pyState.getSdk();
    PyDebugSessionFactory sessionCreator = PyDebugSessionFactory.findExtension(sdk);
    final XDebugSession session;
    if (sessionCreator != null) {
      session = sessionCreator.createSession(pyState, environment);
    }
    else {
      final ServerSocket serverSocket = PythonCommandLineState.createServerSocket();
      final int serverLocalPort = serverSocket.getLocalPort();
      RunProfile profile = environment.getRunProfile();
      final ExecutionResult result =
        pyState.execute(environment.getExecutor(), createCommandLinePatchers(environment.getProject(), pyState, profile, serverLocalPort));

      session = createXDebugSession(environment, pyState, serverSocket, result);
    }
    initSession(session, state, environment.getExecutor());
    return session.getRunContentDescriptor();
  }

  /**
   * The same signature and the same meaning as in
   * {@link com.intellij.execution.runners.AsyncProgramRunner#execute(ExecutionEnvironment, RunProfileState)}.
   */
  protected @NotNull Promise<@Nullable RunContentDescriptor> execute(@NotNull ExecutionEnvironment environment,
                                                                     @NotNull RunProfileState state)
    throws ExecutionException {
    return execute(environment, state, null);
  }

  private @NotNull Promise<@Nullable RunContentDescriptor> execute(@NotNull ExecutionEnvironment environment,
                                                                   @NotNull RunProfileState state,
                                                                   @Nullable XDebugSessionListener sessionListener) {
    // aborts the execution of the run configuration if `.canRun` returns false
    // this is used for cases in which a user action prevents the execution; for example,
    // a warning dialog could be displayed to the user asking them if they wish to proceed with
    // running the configuration
    if (state instanceof PythonCommandLineState pythonState && !pythonState.canRun()) {
      return Promises.resolvedPromise(null);
    }

    return createSession(state, environment)
      .thenAsync(session -> AppUIExecutor.onUiThread().submit(() -> {
        if (sessionListener != null) {
          session.addSessionListener(sessionListener);
        }
        initSession(session, state, environment.getExecutor());
        return session.getRunContentDescriptor();
      }));
  }

  @Override
  public void execute(@NotNull ExecutionEnvironment environment) throws ExecutionException {
    exec(environment, null);
  }

  private @NotNull Promise<@Nullable RunContentDescriptor> executeWithLegacyWorkaround(
    @NotNull ExecutionEnvironment environment,
    @NotNull RunProfileState state,
    @Nullable XDebugSessionListener sessionListener
  ) throws ExecutionException {
    boolean callExecute = true;
    try {
      callExecute = getClass()
        .getDeclaredMethod("doExecute", RunProfileState.class, ExecutionEnvironment.class)
        .getDeclaringClass()
        .equals(PyDebugRunner.class);
    }
    catch (NoSuchMethodException e) {
      // It's not supposed to happen, but if it happens, asynchronous execute is called.
    }
    if (callExecute) {
      return execute(environment, state, sessionListener);
    }
    return AppUIExecutor.onUiThread().submit(() -> doExecute(state, environment));
  }

  protected void initSession(XDebugSession session, RunProfileState state, Executor executor) {
  }

  public static int findIndex(List<String> paramList, String paramName) {
    for (int i = 0; i < paramList.size(); i++) {
      if (paramName.equals(paramList.get(i))) {
        return i + 1;
      }
    }
    return -1;
  }

  /**
   * @deprecated Use {@link #createConsoleCommunication(Project, ExecutionResult, XDebugProcess, XDebugSession)} instead.
   */
  @Deprecated(since = "2021.3.0", forRemoval = true)
  public static void createConsoleCommunicationAndSetupActions(final @NotNull Project project,
                                                               final @NotNull ExecutionResult result,
                                                               @NotNull PyDebugProcess debugProcess, @NotNull XDebugSession session) {
    createConsoleCommunication(project, result, debugProcess, session);
  }

  @ApiStatus.Internal
  public static <T extends XDebugProcess & PyDebugProcessWithConsole> void createConsoleCommunication(final @NotNull Project project,
                                                                                                      final @NotNull ExecutionResult result,
                                                                                                      @NotNull T debugProcess,
                                                                                                      @NotNull XDebugSession session) {
    ExecutionConsole console = result.getExecutionConsole();
    if (console instanceof PythonDebugLanguageConsoleView) {
      ProcessHandler processHandler = result.getProcessHandler();

      initDebugConsole(project, debugProcess, (PythonDebugLanguageConsoleView)console, processHandler, session);
    }
  }

  /**
   * @deprecated Use {@link #initDebugConsole(Project, XDebugProcess, PythonDebugLanguageConsoleView, ProcessHandler, XDebugSession)} instead.
   */
  @Deprecated(since = "2021.3.0", forRemoval = true)
  public static PythonDebugConsoleCommunication<PyDebugProcess> initDebugConsoleView(Project project,
                                                                                     PyDebugProcess debugProcess,
                                                                                     PythonDebugLanguageConsoleView console,
                                                                                     ProcessHandler processHandler,
                                                                                     final XDebugSession session) {
    return initDebugConsole(project, debugProcess, console, processHandler, session);
  }

  @ApiStatus.Internal
  public static <T extends XDebugProcess & PyDebugProcessWithConsole> PythonDebugConsoleCommunication<T> initDebugConsole(Project project,
                                                                                                                          T debugProcess,
                                                                                                                          PythonDebugLanguageConsoleView console,
                                                                                                                          ProcessHandler processHandler,
                                                                                                                          final XDebugSession session) {
    PythonConsoleView pythonConsoleView = console.getPydevConsoleView();
    PythonDebugConsoleCommunication<T> debugConsoleCommunication =
      new PythonDebugConsoleCommunication<>(project, debugProcess, pythonConsoleView);

    pythonConsoleView.setConsoleCommunication(debugConsoleCommunication);


    PydevDebugConsoleExecuteActionHandler consoleExecuteActionHandler = new PydevDebugConsoleExecuteActionHandler(console,
                                                                                                                  processHandler,
                                                                                                                  debugConsoleCommunication);

    PythonDebugConsoleCommunication<T> pythonDebugConsoleCommunication =
      initDebugConsole(pythonConsoleView, consoleExecuteActionHandler, debugProcess, processHandler, debugConsoleCommunication,
                       session);

    // We need to enable Debug Console one more time after adding the consoleExecuteActionHandler to the pythonConsoleView
    if (console.isEnabled()) {
      console.enableConsole(false);
    }

    return pythonDebugConsoleCommunication;
  }

  @ApiStatus.Internal
  protected static <T extends XDebugProcess & PyDebugProcessWithConsole> PythonDebugConsoleCommunication<T> initDebugConsole(
    @NotNull PythonConsoleView pythonConsoleView,
    @NotNull PydevDebugConsoleExecuteActionHandler consoleExecuteActionHandler,
    @NotNull T debugProcess,
    @NotNull ProcessHandler processHandler,
    @NotNull PythonDebugConsoleCommunication<T> debugConsoleCommunication,
    final XDebugSession session) {
    pythonConsoleView.setExecutionHandler(consoleExecuteActionHandler);

    debugProcess.getSession().addSessionListener(consoleExecuteActionHandler);
    new LanguageConsoleBuilder(pythonConsoleView).processHandler(processHandler).initActions(consoleExecuteActionHandler, "py", true);


    debugConsoleCommunication.addCommunicationListener(new ConsoleCommunicationListener() {
      @Override
      public void commandExecuted(boolean more) {
        session.rebuildViews();
      }

      @Override
      public void inputRequested() {
        ApplicationManager.getApplication().invokeLater(() -> {
          if (session.getConsoleView() instanceof PythonDebugLanguageConsoleView debugConsoleView) {
            RunnerLayoutUi sessionUi = session.getUI();
            if (sessionUi != null) {
              selectConsoleTab(session.getRunContentDescriptor(), sessionUi.getContentManager(), true);
            }
            else {
              // TODO [Debugger.RunnerLayoutUi]
            }

            if (pythonConsoleView.isVisible()) {
              requestFocus(true, null, pythonConsoleView, true);
            }
            else if (debugConsoleView.getPrimaryConsoleView() instanceof ConsoleViewImpl consoleView) {
              requestFocus(false, consoleView.getEditor(), null, true);
            }
          }
        });
      }
    });

    return debugConsoleCommunication;
  }

  /**
   * To be deprecated.
   * <p>
   * The part of the legacy implementation based on {@link GeneralCommandLine}.
   */
  public static @Nullable CommandLinePatcher createRunConfigPatcher(RunProfileState state, RunProfile profile) {
    CommandLinePatcher runConfigPatcher = null;
    if (state instanceof PythonCommandLineState && profile instanceof AbstractPythonRunConfiguration) {
      runConfigPatcher = (AbstractPythonRunConfiguration<?>)profile;
    }
    return runConfigPatcher;
  }

  public CommandLinePatcher[] createCommandLinePatchers(final Project project, final PythonCommandLineState state,
                                                        RunProfile profile,
                                                        final int serverLocalPort) {
    return new CommandLinePatcher[]{createDebugServerPatcher(project, state, serverLocalPort, profile),
      createRunConfigPatcher(state, profile)};
  }

  /**
   * To be deprecated.
   * <p>
   * The part of the legacy implementation based on {@link GeneralCommandLine}.
   */
  public static boolean patchExeParams(ParametersList parametersList) {
    // we should remove '-m' parameter, but notify debugger of it,
    // but we can't remove one parameter from a group, so we create a new parameters group
    int moduleParamsIndex =
      parametersList.getParamsGroups().indexOf(parametersList.getParamsGroup(PythonCommandLineState.GROUP_MODULE));
    ParamsGroup oldModuleParams = parametersList.removeParamsGroup(moduleParamsIndex);
    if (oldModuleParams == null) {
      return false;
    }
    boolean isModule = false;

    ParamsGroup newModuleParams = new ParamsGroup(PythonCommandLineState.GROUP_MODULE);
    for (String param : oldModuleParams.getParameters()) {
      if (!param.equals("-m")) {
        newModuleParams.addParameter(param);
      }
      else {
        isModule = true;
      }
    }

    parametersList.addParamsGroupAt(moduleParamsIndex, newModuleParams);
    return isModule;
  }

  /**
   * To be deprecated.
   * <p>
   * The part of the legacy implementation based on {@link GeneralCommandLine}.
   */
  private CommandLinePatcher createDebugServerPatcher(final Project project,
                                                      final PythonCommandLineState pyState,
                                                      final int serverLocalPort,
                                                      final RunProfile profile) {
    return new CommandLinePatcher() {
      @Override
      public void patchCommandLine(GeneralCommandLine commandLine) {
        // script name is the last parameter; all other params are for python interpreter; insert just before the name
        ParametersList parametersList = commandLine.getParametersList();

        @SuppressWarnings("ConstantConditions") @NotNull
        ParamsGroup debugParams = parametersList.getParamsGroup(PythonCommandLineState.GROUP_DEBUGGER);
        assert debugParams != null;

        boolean isModule = patchExeParams(parametersList);

        fillDebugParameters(project, debugParams, serverLocalPort, pyState, commandLine, profile, isModule);

        @SuppressWarnings("ConstantConditions") @NotNull
        ParamsGroup exeParams = parametersList.getParamsGroup(PythonCommandLineState.GROUP_EXE_OPTIONS);

        final PythonSdkFlavor<?> flavor = pyState.getSdkFlavor();
        if (flavor != null) {
          assert exeParams != null;
          for (String option : flavor.getExtraDebugOptions()) {
            exeParams.addParameter(option);
          }
        }
      }
    };
  }

  /**
   * To be deprecated.
   * <p>
   * The part of the legacy implementation based on {@link GeneralCommandLine}.
   */
  private void fillDebugParameters(@NotNull Project project,
                                   @NotNull ParamsGroup debugParams,
                                   int serverLocalPort,
                                   @NotNull PythonCommandLineState pyState,
                                   @NotNull GeneralCommandLine cmd,
                                   @Nullable RunProfile runProfile,
                                   boolean isModule) {
    PythonHelper.DEBUGGER.addToGroup(debugParams, cmd);

    if (isModule) {
      // add a module flag only after command line parameters
      debugParams.addParameter(MODULE_PARAM);
    }

    configureDebugParameters(project, debugParams, pyState, cmd);

    configureDebugEnvironment(project, cmd.getEnvironment(), runProfile);

    configureDebugConnectionParameters(debugParams, serverLocalPort);
  }

  private @NotNull PythonExecution prepareDebuggerScriptExecution(@NotNull Project project,
                                                                  @NotNull Function<TargetEnvironment, HostPort> serverPortOnTarget,
                                                                  @NotNull PythonCommandLineState pyState,
                                                                  @NotNull PythonExecution originalExecution,
                                                                  @Nullable RunProfile runProfile,
                                                                  @NotNull HelpersAwareTargetEnvironmentRequest request) {
    PythonExecution debuggerScript;

    if (originalExecution instanceof PythonToolExecution pythonToolExecution) {
      debuggerScript = PythonScripts.prepareHelperScriptViaToolExecution(
        PythonHelper.DEBUGGER,
        request,
        pythonToolExecution.getToolPath(),
        pythonToolExecution.getToolParams()
      );
    }
    else {
      debuggerScript = PythonScripts.prepareHelperScriptExecution(PythonHelper.DEBUGGER, request);
    }

    TargetEnvironmentRequest targetEnvironmentRequest = request.getTargetEnvironmentRequest();
    PythonScripts.extendEnvs(debuggerScript, originalExecution.getEnvs(), targetEnvironmentRequest.getTargetPlatform());

    debuggerScript.setWorkingDir(originalExecution.getWorkingDir());

    originalExecution.accept(new PythonExecution.Visitor() {
      @Override
      public void visit(@NotNull PythonScriptExecution pythonScriptExecution) {
        // do nothing
      }

      @Override
      public void visit(@NotNull PythonModuleExecution pythonModuleExecution) {
        // add a module flag only after command line parameters
        debuggerScript.addParameter(MODULE_PARAM);
      }

      @Override
      public void visit(@NotNull PythonToolScriptExecution pythonToolScriptExecution) {
        // do nothing
      }

      @Override
      public void visit(@NotNull PythonToolModuleExecution pythonToolModuleExecution) {
        // add a module flag only after command line parameters
        var moduleFlag = pythonToolModuleExecution.getModuleFlag();
        debuggerScript.addParameter(moduleFlag);
      }
    });

    configureDebugParameters(project, pyState, debuggerScript, false);

    // TODO [Targets API] This workaround is required until Cython extensions are uploaded using Targets API
    boolean isLocalTarget = targetEnvironmentRequest instanceof LocalTargetEnvironmentRequest;
    configureDebugEnvironment(project, new TargetEnvironmentController(debuggerScript.getEnvs(), request), runProfile,
                              isLocalTarget);

    if (PyDebuggerOptionsProvider.getInstance(project).isRunDebuggerInServerMode() && Registry.is("python.debug.use.single.port")) {
      configureServerModeDebugConnectionParameters(debuggerScript, serverPortOnTarget);
    }
    else {
      configureClientModeDebugConnectionParameters(debuggerScript, serverPortOnTarget);
    }

    originalExecution.accept(new PythonExecution.Visitor() {
      @Override
      public void visit(@NotNull PythonScriptExecution pythonScriptExecution) {
        Function<TargetEnvironment, String> scriptPath = pythonScriptExecution.getPythonScriptPath();
        if (scriptPath != null) {
          debuggerScript.addParameter(scriptPath);
        }
        else {
          throw new IllegalArgumentException("Python script path must be set");
        }
      }

      @Override
      public void visit(@NotNull PythonModuleExecution pythonModuleExecution) {
        String moduleName = pythonModuleExecution.getModuleName();
        if (moduleName != null) {
          debuggerScript.addParameter(moduleName);
        }
        else {
          throw new IllegalArgumentException("Python module name must be set");
        }
      }

      @Override
      public void visit(@NotNull PythonToolScriptExecution pythonToolScriptExecution) {
        Function<TargetEnvironment, Path> scriptPath = pythonToolScriptExecution.getPythonScriptPath();
        debuggerScript.addParameter(scriptPath.andThen((it) -> it.toString()));
      }

      @Override
      public void visit(@NotNull PythonToolModuleExecution pythonToolModuleExecution) {
        String moduleName = pythonToolModuleExecution.getModuleName();
        debuggerScript.addParameter(moduleName);
      }
    });

    debuggerScript.getParameters().addAll(originalExecution.getParameters());

    return debuggerScript;
  }

  public static void configureDebugEnvironment(@NotNull Project project, Map<String, String> environment,
