<?php

declare(strict_types=1);

namespace Drupal\Tests\netforum\xWeb;

/**
 * Manages a SoapServer process using the PHP built-in webserver.
 */
class SoapServerManager {

  /**
   * Seconds to wait for the built-in PHP web server to start.
   */
  protected const PROCESS_START_TIMEOUT = 5;

  /**
   * The PHP process running the SoapServer.
   *
   * @var resource|null
   */
  protected $soapServerProcess = NULL;

  protected array $soapServerPipes = [];

  protected int $soapServerPort = 0;

  /**
   * JSON file containing serialized settings to be passed to the SoapServer
   * process.
   */
  protected string $netForumSettingsFile = '';

  /**
   * JSON file containing serialized state for SoapServer process.
   */
  protected string $netForumStateFile = '';

  private readonly NetForumXmlSettings $netForumXmlSettings;

  public function __construct(NetForumXmlSettings $settings) {
    $this->netForumXmlSettings = $settings;
  }

  public function __destruct() {
    $this->stopSoapServer();
  }

  /**
   * Stop a previously started server and clean up temporary files.
   */
  public function stopSoapServer(): void {
    if ($this->soapServerProcess !== NULL) {
      proc_terminate($this->soapServerProcess);
      proc_close($this->soapServerProcess);

      $this->soapServerProcess = NULL;
      $this->soapServerPipes = [];
      $this->soapServerPort = 0;

      if (is_file($this->netForumSettingsFile)) {
        unlink($this->netForumSettingsFile);
      }

      if (is_file($this->netForumStateFile)) {
        unlink($this->netForumStateFile);
      }
    }
  }

  /**
   * Start a new PHP process running the SoapServer. Make sure to call
   * stopSoapServer() when done to end the process and clean up temporary
   * files.
   *
   * @throws \RuntimeException
   */
  public function startSoapServer(): void {
    $this->writeSettingsAndState();

    $command = "settings={$this->netForumSettingsFile} state={$this->netForumStateFile} php -S 127.0.0.1:0 " . dirname(__DIR__, 1) . '/xWeb/SoapServer.php';
    // @phpcs:disable Drupal.Commenting.InlineComment.InvalidEndChar,Drupal.Commenting.InlineComment.NotCapital
    // fwrite(STDERR, 'Launching: ' . $command . PHP_EOL);
    $process = proc_open($command, [
      0 => ['pipe', 'r'],
      1 => ['pipe', 'w'],
      2 => ['pipe', 'w'],
    ], $this->soapServerPipes);
    if ($process === FALSE) {
      throw new \RuntimeException('Failed to create xWeb SOAP server process.');
    }
    $this->soapServerProcess = $process;

    // Set stderr to non-blocking.
    stream_set_blocking($this->soapServerPipes[2], FALSE);

    $serverStartTime = microtime(TRUE);
    $serverStarted = FALSE;
    while ((microtime(TRUE) - $serverStartTime) < self::PROCESS_START_TIMEOUT) {
      $stderr = stream_get_contents($this->soapServerPipes[2]);
      if (!empty($stderr) && preg_match('#https?://[^:]+:(\d+)#', $stderr, $matches)) {
        $this->soapServerPort = (int) $matches[1];

        $serverStarted = TRUE;
      }

      sleep(1);
    }

    if (!$serverStarted) {
      throw new \RuntimeException('Failed to start xWeb SOAP server process.');
    }
  }

  /**
   * Writes temporary files for settings and state.
   */
  private function writeSettingsAndState(): void {
    $tempSettingsFile = tempnam(sys_get_temp_dir(), 'nf_settings_');
    if ($tempSettingsFile === FALSE) {
      throw new \RuntimeException('Failed to create temporary file for Soap Server settings.');
    }
    $this->netForumSettingsFile = $tempSettingsFile;

    $fpcSettings = file_put_contents($this->netForumSettingsFile, $this->netForumXmlSettings->toJson());
    if ($fpcSettings === FALSE) {
      throw new \RuntimeException('Failed to write to temporary file for Soap Server settings.');
    }

    $tempStateFile = tempnam(sys_get_temp_dir(), 'nf_state_');
    if ($tempStateFile === FALSE) {
      throw new \RuntimeException('Failed to create temporary file for Soap Server state.');
    }
    $this->netForumStateFile = $tempStateFile;
  }

  public function getSoapServerPort(): int {
    return $this->soapServerPort;
  }

  public function isSoapServerRunning(): bool {
    if ($this->soapServerProcess !== NULL) {
      $procInfo = proc_get_status($this->soapServerProcess);

      return $procInfo['running'];
    }

    return FALSE;
  }

}
