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

import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.PtyCommandLine;
import com.intellij.execution.console.ConsoleExecuteAction;
import com.intellij.execution.console.ConsoleHistoryController;
import com.intellij.execution.console.LanguageConsoleView;
import com.intellij.execution.process.*;
import com.intellij.execution.runners.ConsoleTitleGen;
import com.intellij.execution.target.*;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.icons.AllIcons;
import com.intellij.ide.errorTreeView.NewErrorTreeViewPanel;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorAction;
import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler;
import com.intellij.openapi.editor.actions.SplitLineAction;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.openapi.util.registry.RegistryManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.JBColor;
import com.intellij.ui.SideBorder;
import com.intellij.ui.content.Content;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.execution.ParametersListUtil;
import com.intellij.util.net.NetUtils;
import com.intellij.util.ui.JBEmptyBorder;
import com.intellij.util.ui.MessageCategory;
import com.intellij.util.ui.UIUtil;
import com.intellij.xdebugger.XDebugProcess;
import com.intellij.xdebugger.XDebugProcessStarter;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XDebuggerManager;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PythonPluginDisposable;
import com.jetbrains.python.console.actions.ConsoleCopyOutputAction;
import com.jetbrains.python.console.actions.ScrollToTheEndAction;
import com.jetbrains.python.console.actions.ShowCommandQueueAction;
import com.jetbrains.python.console.actions.ShowVarsAction;
import com.jetbrains.python.console.pydev.ConsoleCommunicationListener;
import com.jetbrains.python.debugger.PyDebugRunner;
import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.PyVariableViewSettings;
import com.jetbrains.python.debugger.ValuesPolicy;
import com.jetbrains.python.debugger.settings.PyDebuggerSettings;
import com.jetbrains.python.parser.icons.PythonParserIcons;
import com.jetbrains.python.remote.PyRemotePathMapper;
import com.jetbrains.python.remote.PyRemoteSdkAdditionalDataBase;
import com.jetbrains.python.run.*;
import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest;
import com.jetbrains.python.sdk.PythonEnvUtil;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import org.jetbrains.annotations.*;

import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.*;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.intellij.execution.runners.AbstractConsoleRunnerWithHistory.registerActionShortcuts;
import static com.jetbrains.python.console.PyConsoleUtil.ASYNCIO_REPL_ENV;

/**
 * @author traff, oleg
 */
@ApiStatus.Internal
public class PydevConsoleRunnerImpl implements PydevConsoleRunner {
  /**
   * The address that IDE uses to listen for incoming connections from Python
   * Console script started in the "client mode".
   */
  private static final @NonNls String LOCALHOST = "localhost";

  public static final @NonNls String WORKING_DIR_AND_PYTHON_PATHS = "WORKING_DIR_AND_PYTHON_PATHS";
  public static final @NonNls String PROJECT_ROOT = "PROJECT_ROOT";
  public static final @NonNls String CONSOLE_START_COMMAND = "import sys; print('Python %s on %s' % (sys.version, sys.platform))\n" +
                                                             "sys.path.extend([" + WORKING_DIR_AND_PYTHON_PATHS + "])\n";
  public static final @NonNls String STARTED_BY_RUNNER = "startedByRunner";
  public static final @NonNls String INLINE_OUTPUT_SUPPORTED = "INLINE_OUTPUT_SUPPORTED";
  private static final @NonNls String ASYNCIO_REPL_COMMAND = "-m asyncio";

  private static final Long WAIT_BEFORE_FORCED_CLOSE_MILLIS = 2000L;
  private static final Logger LOG = Logger.getInstance(PydevConsoleRunnerImpl.class);
  public static final int PORTS_WAITING_TIMEOUT = 20000;
  /**
   * Everything in console is UTF-8 only, just like regular script execution
   */
  public static final Charset CONSOLE_CHARSET = StandardCharsets.UTF_8;
  private final Project myProject;
  private final @NlsContexts.TabTitle String myTitle;
  private final @Nullable String myWorkingDir;
  private final @Nullable Function<TargetEnvironment, String> myWorkingDirFunction;
  private @Nullable Sdk mySdk;
  private PydevConsoleCommunication myPydevConsoleCommunication;
  private ProcessHandler myProcessHandler;
  protected PythonConsoleExecuteActionHandler myConsoleExecuteActionHandler;
  private final List<ConsoleListener> myConsoleListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private final PyConsoleType myConsoleType;
  private final @NotNull Map<String, String> myEnvironmentVariables;
  protected final @NotNull PyConsoleOptions.PyConsoleSettings myConsoleSettings;
  private final String @Nullable [] myStatementsToExecute;

  /*
   Console title used during initialization, it can be changed with Rename action
   */
  private @Nullable @NlsContexts.TabTitle String myConsoleInitTitle = null;
  private PythonConsoleView myConsoleView;

  /**
   * The function is resolved to {@link #myResolvedStatementsToExecute} against the target environment associated with this console.
   */
  private final @Nullable Function<TargetEnvironment, @NotNull String> myStatementsToExecuteFunction;

  private @Nullable String myResolvedStatementsToExecute;

  public PydevConsoleRunnerImpl(final @NotNull Project project,
                                @Nullable Sdk sdk,
                                final @NotNull PyConsoleType consoleType,
                                final @NotNull @NlsContexts.TabTitle String title,
                                @Nullable Function<TargetEnvironment, String> workingDirFunction,
                                @NotNull Map<String, String> environmentVariables,
                                @NotNull PyConsoleOptions.PyConsoleSettings settingsProvider,
                                @NotNull Function<TargetEnvironment, @NotNull String> statementsToExecuteFunction) {
    myProject = project;
    mySdk = sdk;
    myTitle = title;
    myWorkingDir = null;
    myWorkingDirFunction = workingDirFunction;
    myConsoleType = consoleType;
    myEnvironmentVariables = environmentVariables;
    myConsoleSettings = settingsProvider;
    myStatementsToExecute = null;
    myStatementsToExecuteFunction = statementsToExecuteFunction;
  }

  public PydevConsoleRunnerImpl(final @NotNull Project project,
                                @Nullable Sdk sdk,
                                final @NotNull PyConsoleType consoleType,
                                final @NotNull @NlsContexts.TabTitle String title,
                                final @Nullable String workingDir,
                                @NotNull Map<String, String> environmentVariables,
                                @NotNull PyConsoleOptions.PyConsoleSettings settingsProvider,
                                String... statementsToExecute) {
    myProject = project;
    mySdk = sdk;
    myTitle = title;
    myWorkingDir = workingDir;
    myWorkingDirFunction = null;
    myConsoleType = consoleType;
    myEnvironmentVariables = environmentVariables;
    myConsoleSettings = settingsProvider;
    myStatementsToExecute = statementsToExecute;
    myStatementsToExecuteFunction = environment -> StringUtil.join(statementsToExecute, "\n");
  }

  public PydevConsoleRunnerImpl(final @NotNull Project project,
                                @Nullable Sdk sdk,
                                final @NotNull PyConsoleType consoleType,
                                final @Nullable String workingDir,
                                @NotNull Map<String, String> environmentVariables,
                                @NotNull PyConsoleOptions.PyConsoleSettings settingsProvider,
                                String... statementsToExecute) {
    this(project, sdk, consoleType, consoleType.getTitle(), workingDir, environmentVariables, settingsProvider, statementsToExecute);
  }

  public void setConsoleTitle(@NlsContexts.TabTitle String consoleTitle) {
    myConsoleInitTitle = consoleTitle;
  }

  private List<AnAction> fillRunActionsToolbar(final DefaultActionGroup toolbarActions) {
    List<AnAction> actions = new ArrayList<>();
    // Rerun
    actions.add(createRerunAction());
    // Stop
    actions.add(createStopAction());
    // Execute
    actions.add(
      new ConsoleExecuteAction(myConsoleView, myConsoleExecuteActionHandler, myConsoleExecuteActionHandler.getEmptyExecuteAction(),
                               myConsoleExecuteActionHandler, AllIcons.Debugger.ExecuteCurrentStatement));
    toolbarActions.addAll(actions);
    // Attach Debugger
    toolbarActions.add(new ConnectDebuggerAction());
    // Show Queue
    toolbarActions.add(new ShowCommandQueueAction(myConsoleView));
    // Scroll to the end
    toolbarActions.add(new ScrollToTheEndAction(myConsoleView.getEditor()));
    // Separator
    toolbarActions.add(Separator.create());
    // Settings
    DefaultActionGroup settings = DefaultActionGroup.createPopupGroup(() -> PyBundle.message("pydev.console.runner.settings"));
    settings.getTemplatePresentation().setIcon(AllIcons.General.GearPlain);
    settings.add(new PyVariableViewSettings.SimplifiedView(null));
    settings.add(new PyVariableViewSettings.VariablesPolicyGroup());
    settings.add(new PyVariableViewSettings.QuotingPolicyGroup());
    toolbarActions.add(settings);
    // Output actions
    DefaultActionGroup outputActions = DefaultActionGroup.createPopupGroup(() -> PyBundle.message("pydev.console.runner.output.actions"));
    outputActions.getTemplatePresentation().setIcon(AllIcons.General.InspectionsEye);
    outputActions.add(new ShowVarsAction(myConsoleView, myPydevConsoleCommunication)); // Show Variables
    outputActions.add(new SoftWrapAction()); // Use soft wraps
    outputActions.add(Separator.create());
    outputActions.add(ConsoleHistoryController.getController(myConsoleView).getBrowseHistory()); // Console History
    outputActions.add(new ConsoleCopyOutputAction(myConsoleView)); // Copy Console Output
    toolbarActions.add(outputActions);
    // Actions without icons
    actions.add(PyConsoleUtil.createInterruptAction(myConsoleView));
    actions.add(PyConsoleUtil.createTabCompletionAction(myConsoleView));
    actions.add(createSplitLineAction());

    return actions;
  }


  @Override
  public void open() {
    PythonConsoleToolWindow toolWindow = PythonConsoleToolWindow.getInstance(myProject);
    if (toolWindow != null && toolWindow.isInitialized()) {
      toolWindow.getToolWindow().activate(() -> {
      }, true);
    }
    else {
      runSync(true);
    }
  }


  @Override
  public void runSync(boolean requestEditorFocus) {
    ProgressManager.getInstance().run(new Task.Backgroundable(myProject, PyBundle.message("connecting.to.console.title"), false) {
      @Override
      public void run(final @NotNull ProgressIndicator indicator) {
        try {
          Sdk sdk = mySdk;
          if (sdk == null) {
            throw new ExecutionException(PyBundle.message("pydev.console.python.interpreter.is.not.selected"));
          }
          initAndRun(sdk);
          indicator.setText(PyBundle.message("connecting.to.console.progress"));
          connect(getStatementsToExecute());
          if (requestEditorFocus) {
            myConsoleView.requestFocus();
          }
        }
        catch (ExecutionException e) {
          LOG.warn("Error running console", e);
          ApplicationManager.getApplication().invokeLater(() -> showErrorsInConsole(e));
        }
      }
    });
  }

  private String @NotNull [] getStatementsToExecute() {
    if (myResolvedStatementsToExecute != null) {
      return new String[]{myResolvedStatementsToExecute};
    }
    else {
      if (myStatementsToExecute == null) {
        return ArrayUtil.EMPTY_STRING_ARRAY;
      }
      else {
        return myStatementsToExecute;
      }
    }
  }

  @Override
  public void run(boolean requestEditorFocus) {
    TransactionGuard.submitTransaction(PythonPluginDisposable.getInstance(myProject),
                                       () -> FileDocumentManager.getInstance().saveAllDocuments());

    ApplicationManager.getApplication().executeOnPooledThread(
      () -> ProgressManager.getInstance().run(new Task.Backgroundable(myProject, PyBundle.message("connecting.to.console.title"), false) {
        @Override
        public void run(final @NotNull ProgressIndicator indicator) {
          indicator.setText(PyBundle.message("connecting.to.console.progress"));
          try {
            if (mySdk == null) {
              throw new ExecutionException(PyBundle.message("pydev.console.python.interpreter.is.not.selected"));
            }
            initAndRun(mySdk);
            connect(getStatementsToExecute());
            if (requestEditorFocus) {
              myConsoleView.requestFocus();
            }
          }
          catch (final Exception e) {
            LOG.warn("Error running console", e);
            UIUtil.invokeAndWaitIfNeeded(() -> showErrorsInConsole(e));
          }
        }
      })
    );
  }

  private void showErrorsInConsole(Exception e) {
    fireConsoleInitializationErrorEvent(e);
    DefaultActionGroup actionGroup = new DefaultActionGroup(createRerunAction());

    final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar("PydevConsoleRunnerErrors",
                                                                                        actionGroup, false);

    // Runner creating
    final JPanel panel = new JPanel(new BorderLayout());
    panel.add(actionToolbar.getComponent(), BorderLayout.WEST);

    NewErrorTreeViewPanel errorViewPanel = new NewErrorTreeViewPanel(myProject, null, false, false, null);

    String[] messages = StringUtil.isNotEmpty(e.getMessage()) ? StringUtil.splitByLines(e.getMessage()) : ArrayUtilRt.EMPTY_STRING_ARRAY;
    if (messages.length == 0) {
      messages = new String[]{PyBundle.message("pydev.console.runner.unknown.error")};
    }

    errorViewPanel.addMessage(MessageCategory.ERROR, messages, null, -1, -1, null);
    panel.add(errorViewPanel, BorderLayout.CENTER);

    //noinspection DialogTitleCapitalization
    final RunContentDescriptor contentDescriptor = new RunContentDescriptor(
      null, myProcessHandler, panel, PyBundle.message("pydev.console.runner.error.running.console"));

    showContentDescriptor(contentDescriptor);
  }


  protected void showContentDescriptor(RunContentDescriptor contentDescriptor) {
    ToolWindow toolwindow = PythonConsoleToolWindow.getToolWindow(myProject);
    toolwindow.getComponent().putClientProperty(STARTED_BY_RUNNER, "true");
    PythonConsoleToolWindow.getInstance(myProject).init(toolwindow, contentDescriptor);
  }

  private @NotNull PythonExecution createPythonConsoleExecution(@NotNull Function<TargetEnvironment, HostPort> ideServerPort,
                                                                @NotNull PythonConsoleRunParams runParams,
                                                                @NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest) {
    return doCreatePythonConsoleExecution(ideServerPort, runParams, helpersAwareTargetRequest);
  }

  protected @NotNull PythonConsoleRunParams createConsoleRunParams(@Nullable String workingDir,
                                                                   @NotNull Sdk sdk,
                                                                   @NotNull Map<String, String> environmentVariables) {
    return new PythonConsoleRunParams(myConsoleSettings, workingDir, sdk, environmentVariables);
  }

  private @NotNull PythonExecution doCreatePythonConsoleExecution(@NotNull Function<TargetEnvironment, HostPort> ideServerPort,
                                                                  @NotNull PythonConsoleRunParams runParams,
                                                                  @NotNull HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest) {
    PythonExecution pythonConsoleScriptExecution =
      PydevConsoleCli.createPythonConsoleScriptInClientMode(ideServerPort, helpersAwareTargetRequest);

    PyRemoteSdkAdditionalDataBase remoteSdkAdditionalData = getRemoteAdditionalData(mySdk);
    PyRemotePathMapper pathMapper = remoteSdkAdditionalData != null
                                    ? PydevConsoleRunnerUtil.getPathMapper(myProject, myConsoleSettings, remoteSdkAdditionalData)
                                    : null;
    PythonCommandLineState.initEnvironment(myProject, pythonConsoleScriptExecution, runParams, helpersAwareTargetRequest, pathMapper,
                                           mySdk);

    if (myWorkingDirFunction != null) {
      pythonConsoleScriptExecution.setWorkingDir(myWorkingDirFunction);
    }

    return pythonConsoleScriptExecution;
  }

  private @NotNull PythonConsoleView createConsoleView(@NotNull Sdk sdk) {
    PythonConsoleView consoleView = new PythonConsoleView(myProject, myTitle, sdk, false);
    myPydevConsoleCommunication.setConsoleFile(consoleView.getVirtualFile());
    consoleView.addMessageFilter(new PythonTracebackFilter(myProject));
    consoleView.addMessageFilter(new PythonImportErrorFilter(myProject));
    return consoleView;
  }

  /**
   * This method performs several actions at once:
   * <ol>
   *   <li>Prepares the target environment determined by the provided SDK.</li>
   *   <li>Creates the server on IDE side and makes it start listening for the
   *   incoming connections.</li>
   *   <li>Starts the Python Console backend process on the target in the
   *   client mode.</li>
   * </ol>
   */
  private @NotNull ConsoleProcessCreationResult createProcessUsingTargetsAPI(@NotNull Sdk sdk) throws ExecutionException {
    int ideServerPort;
    try {
      ideServerPort = NetUtils.findAvailableSocketPort();
    }
    catch (IOException e) {
      throw new ExecutionException(e);
    }

    HelpersAwareTargetEnvironmentRequest helpersAwareRequest = PythonCommandLineState.getPythonTargetInterpreter(myProject, sdk);
    TargetEnvironmentRequest targetEnvironmentRequest = helpersAwareRequest.getTargetEnvironmentRequest();
    TargetEnvironment.LocalPortBinding ideServerPortBinding = new TargetEnvironment.LocalPortBinding(ideServerPort, null);
    targetEnvironmentRequest.getLocalPortBindings().add(ideServerPortBinding);
    Function<TargetEnvironment, HostPort> ideServerHostPortOnTarget = targetEnvironment -> {
      ResolvedPortBinding resolvedPortBinding = targetEnvironment.getLocalPortBindings().get(ideServerPortBinding);
      if (resolvedPortBinding == null) {
        throw new IllegalStateException(MessageFormat.format("Local port binding \"{0}\" must be registered", ideServerPortBinding));
      }
      return resolvedPortBinding.getTargetEndpoint();
    };

    addProjectAndModuleToRequest(targetEnvironmentRequest);

    PythonConsoleRunParams runParams = createConsoleRunParams(myWorkingDir, sdk, myEnvironmentVariables);
    PythonExecution pythonConsoleExecution = createPythonConsoleExecution(ideServerHostPortOnTarget, runParams, helpersAwareRequest);
    pythonConsoleExecution.setCharset(CONSOLE_CHARSET);
    pythonConsoleExecution.getEnvs().put(PythonEnvUtil.PYTHONIOENCODING, environment -> CONSOLE_CHARSET.name());
    List<String> interpreterOptions;
    if (!StringUtil.isEmptyOrSpaces(runParams.getInterpreterOptions())) {
      interpreterOptions = ParametersListUtil.parse(runParams.getInterpreterOptions());
    }
    else {
      interpreterOptions = Collections.emptyList();
    }

    // extra debug options are defined at least for IronPython
    PythonSdkFlavor flavor = PythonSdkFlavor.getFlavor(sdk);
    if (flavor != null) {
      interpreterOptions.addAll(flavor.getExtraDebugOptions());
    }

    // TODO [Targets API] We should pass the proper progress indicator here
    TargetEnvironment targetEnvironment = targetEnvironmentRequest.prepareEnvironment(TargetProgressIndicator.EMPTY);

    myResolvedStatementsToExecute = myStatementsToExecuteFunction.apply(targetEnvironment);

    // TODO [Targets API] [regression] We should create PTY process when `PtyCommandLine.isEnabled()`
    //  (see the legacy method `doCreateConsoleCmdLine()`)

    // TODO [Targets API] Path mappings of SDK's remote additional data is not applied here
    boolean usePty = PtyCommandLine.isEnabled() && !SystemInfo.isWindows;
    TargetedCommandLine targetedCommandLine = PythonScripts.buildTargetedCommandLine(pythonConsoleExecution,
                                                                                     targetEnvironment,
                                                                                     sdk,
                                                                                     interpreterOptions
    );

    // The environment is now prepared and ide server port should be resolved, so let's start the server
    ResolvedPortBinding resolvedServerPortBinding = targetEnvironment.getLocalPortBindings().get(ideServerPortBinding);
    String ideServerHost;
    if (resolvedServerPortBinding != null) {
      ideServerHost = resolvedServerPortBinding.getLocalEndpoint().getHost();
    }
    else {
      LOG.error("The resolution of the local port binding for \"" + ideServerPort + "\" port cannot be found in the prepared environment" +
                ", falling back to \"localhost\" for the server socket binding on the local machine");
      ideServerHost = "localhost";
    }
    PydevConsoleCommunicationServer communicationServer = new PydevConsoleCommunicationServer(myProject, ideServerHost, ideServerPort);
    myPydevConsoleCommunication = communicationServer;
    try {
      communicationServer.serve();
    }
    catch (Exception e) {
      communicationServer.close();
      throw new ExecutionException(e.getMessage(), e);
    }
    Process process = targetEnvironment.createProcess(targetedCommandLine, new EmptyProgressIndicator());
    communicationServer.setPythonConsoleProcess(process);
    String commandLineString = StringUtil.join(targetedCommandLine.getCommandPresentation(targetEnvironment), " ");
    PyRemotePathMapper pathMapper =
      PydevConsoleRunnerUtil.createTargetEnvironmentPathMapper(myProject, sdk, myConsoleSettings, targetEnvironment);
    boolean isMostlySilentProcess = true;
    ProcessHandler processHandler = PyCustomProcessHandlerProvider.createProcessHandler(process, targetEnvironment, commandLineString,
                                                                                        targetedCommandLine.getCharset(), pathMapper,
                                                                                        isMostlySilentProcess);
    return new TargetProcessHandlerFactory(processHandler, process, commandLineString, communicationServer, targetEnvironment);
  }


  /**
   * @return if console is module-specific then provide it.
   */
  protected @Nullable Module getModule() {
    return null;
  }

  /**
   * Adds upload volumes to request, so console will have access to required files
   */
  private void addProjectAndModuleToRequest(@NotNull TargetEnvironmentRequest targetEnvironmentRequest) {
    var module = getModule();
    Module[] modules;
    if (module != null) {
      // Use module if provided
      modules = new Module[1];
      modules[0] = module;
    }
    else {
      // Use all modules otherwise
      modules = ModuleManager.getInstance(myProject).getModules();
    }
    PythonScripts.ensureProjectSdkAndModuleDirsAreOnTarget(targetEnvironmentRequest, myProject, modules);
  }

  @Contract("null -> null")
  private static @Nullable PyRemoteSdkAdditionalDataBase getRemoteAdditionalData(@Nullable Sdk sdk) {
    if (sdk == null) {
      return null;
    }
    else {
      SdkAdditionalData sdkAdditionalData = sdk.getSdkAdditionalData();
      if (sdkAdditionalData instanceof PyRemoteSdkAdditionalDataBase) {
        return (PyRemoteSdkAdditionalDataBase)sdkAdditionalData;
      }
      else {
        return null;
      }
    }
  }

  public static int getRemotePortFromProcess(@NotNull Process process) throws ExecutionException {
    Scanner s = new Scanner(process.getInputStream());
    return readInt(s, process);
  }

  private static int readInt(Scanner s, Process process) throws ExecutionException {
    long started = System.currentTimeMillis();

    StringBuilder sb = new StringBuilder();
    boolean flag = false;

    while (System.currentTimeMillis() - started < PORTS_WAITING_TIMEOUT) {
      if (s.hasNextLine()) {
        String line = s.nextLine();
        sb.append(line).append("\n");
        try {
          int i = Integer.parseInt(line);
          if (flag) {
            LOG.warn("Unexpected strings in output:\n" + sb);
          }
          return i;
        }
        catch (NumberFormatException ignored) {
          flag = true;
          continue;
        }
      }

      TimeoutUtil.sleep(200);

      if (process.exitValue() != 0) {
        String error;
        try {
          error = PyBundle.message("pydev.console.console.process.terminated.with.error",
                                   StreamUtil.readText(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)),
                                   sb.toString());
        }
        catch (Exception ignored) {
          error = PyBundle.message("pydev.console.console.process.terminated.with.exit.code", process.exitValue(), sb.toString());
        }
        throw new ExecutionException(error);
      }
      else {
        break;
      }
    }

    throw new ExecutionException(PyBundle.message("pydev.console.couldnt.read.integer.value.from.stream"));
  }

  /**
   * Encapsulates the logic of creating {@link ProcessHandler} depending on API used to execute Python Console processes (controlled by
   * {@code python.use.targets.api} registry flag) and the type of Python interpreter.
   */
  private abstract static class ConsoleProcessCreationResult {
    protected final @NotNull Process myProcess;
    protected final @NotNull String myCommandLineString;
    protected final @NotNull PydevConsoleCommunication myConsoleCommunication;
    protected final @Nullable TargetEnvironment myTargetEnvironment;

    private ConsoleProcessCreationResult(@NotNull Process process,
                                         @NotNull String commandLineString,
                                         @NotNull PydevConsoleCommunication consoleCommunication,
                                         @Nullable TargetEnvironment environment) {
      myProcess = process;
      myCommandLineString = commandLineString;
      myConsoleCommunication = consoleCommunication;
      myTargetEnvironment = environment;
    }

    public final @Nullable TargetEnvironment getTargetEnvironment() {
      return myTargetEnvironment;
    }

    public abstract @NotNull ProcessHandler createPythonConsoleProcessHandler(@NotNull PythonConsoleView consoleView);
  }

  /**
   * Corresponds to the enabled {@code python.use.targets.api} registry flag (which is the default now) and a local or remote Python
   * interpreter.
   */
  private static final class TargetProcessHandlerFactory extends ConsoleProcessCreationResult {
    private final @NotNull ProcessHandler myProcessHandler;

    private TargetProcessHandlerFactory(@NotNull ProcessHandler handler,
                                        @NotNull Process process,
                                        @NotNull String commandLineString,
                                        @NotNull PydevConsoleCommunication consoleCommunication,
                                        @NotNull TargetEnvironment targetEnvironment) {
      super(process, commandLineString, consoleCommunication, targetEnvironment);
      myProcessHandler = handler;
    }

    @Override
    public @NotNull ProcessHandler createPythonConsoleProcessHandler(@NotNull PythonConsoleView consoleView) {
      PyConsoleProcessHandlers.configureProcessHandlerForPythonConsole(myProcessHandler, consoleView, myConsoleCommunication);
      return myProcessHandler;
    }
  }

  private void initAndRun(@NotNull Sdk sdk) throws ExecutionException {
    // Create Server process
    ConsoleProcessCreationResult processCreationResult;
    processCreationResult = createProcessUsingTargetsAPI(sdk);
    TargetEnvironment targetEnvironment = processCreationResult.getTargetEnvironment();
    UIUtil.invokeAndWaitIfNeeded(() -> {
      // Init console view
      ApplicationManager.getApplication().runWriteIntentReadAction(() -> {
        myConsoleView = createConsoleView(sdk);
        myConsoleView.setRunner(this);
        myConsoleView.setBorder(new SideBorder(JBColor.border(), SideBorder.LEFT));
        myPydevConsoleCommunication.setConsoleView(myConsoleView);
        myProcessHandler = processCreationResult.createPythonConsoleProcessHandler(myConsoleView);

        myConsoleExecuteActionHandler = createExecuteActionHandler();

        ProcessTerminatedListener.attach(myProcessHandler);

        PythonConsoleView consoleView = myConsoleView;
        myProcessHandler.addProcessListener(new ProcessListener() {
          @Override
          public void processTerminated(@NotNull ProcessEvent event) {
            consoleView.setEditable(false);
            // PY-53068: When we send to python side command `exit` we don't get notifyFinished,
            // so we should clear Console Command Queue
            consoleView.restoreQueueWindow(true);
          }
        });

        // Attach to process
        if (targetEnvironment != null) {
          myConsoleView.setTargetEnvironment(targetEnvironment);
        }
        myConsoleView.attachToProcess(myProcessHandler);
        createContentDescriptorAndActions();

        // Run
        myProcessHandler.startNotify();
        return null;
      });
    });
  }

  protected void createContentDescriptorAndActions() {
    boolean isHorizontalAndUnitedToolbar = PyExecuteConsoleCustomizer.Companion.getInstance().isHorizontalAndUnitedToolbar();
    final DefaultActionGroup runToolbarActions = new DefaultActionGroup();
    final ActionToolbar runActionsToolbar =
      ActionManager.getInstance().createActionToolbar("PydevConsoleRunner", runToolbarActions, isHorizontalAndUnitedToolbar);
    final JPanel actionsPanel = new JPanel(new BorderLayout());
    // Left toolbar panel
    actionsPanel.add(runActionsToolbar.getComponent(), BorderLayout.WEST);

    final JPanel mainPanel = new JPanel(new BorderLayout());
    mainPanel.add(myConsoleView.getComponent(), BorderLayout.CENTER);
