<?php

declare(strict_types=1);

namespace Drupal\netforum\xWeb;

use Psr\Log\LoggerInterface;

/**
 * Custom SoapClient that handles xWeb specifics.
 *
 * @phpcs:disable Drupal.NamingConventions.ValidFunctionName.MethodDoubleUnderscore
 */
class SoapClient extends \SoapClient {

  /**
   * The maximum number of times to retry a SOAP call if it fails and requires
   * Authentication.
   */
  protected const MAX_SOAP_CALL_ATTEMPTS = 2;

  protected ?AuthTokenHandlerInterface $tokenHandler = NULL;

  protected ?LoggerInterface $logHandler = NULL;

  protected bool $logAllRequestsResponses = FALSE;

  /**
   * Store a copy of the SOAP Headers being set on the parent SoapClient. This
   * allows for retrieval since the __default_headers property is private.
   */
  protected ?array $defaultHeaders = NULL;

  public function __doRequest(string $request, string $location, string $action, int $version, bool $oneWay = FALSE): ?string {
    $correlationId = NULL;
    if ($this->logAllRequestsResponses && $this->logHandler instanceof LoggerInterface) {
      $correlationId = bin2hex(random_bytes(4));

      $this->logHandler->debug('{action} - {correlationId} - Request: {request}', [
        // Remove the XML namespace from the action.
        'action' => substr($action, strrpos($action, '/') + 1),
        'correlationId' => $correlationId,
        'request' => $request,
      ]);
    }

    $response = parent::__doRequest($request, $location, $action, $version, $oneWay);

    if ($this->logAllRequestsResponses && $this->logHandler instanceof LoggerInterface) {
      $this->logHandler->debug('{action} - {correlationId} - Response: {response}', [
        // Remove the XML namespace from the action.
        'action' => substr($action, strrpos($action, '/') + 1),
        'correlationId' => $correlationId,
        'response' => $response,
      ]);
    }

    return $response;
  }

  public function __getLogHandler(): ?LoggerInterface {
    return $this->logHandler;
  }

  public function __setLogHandler(LoggerInterface $logHandler): void {
    $this->logHandler = $logHandler;
  }

  public function __getTokenHandler(): ?AuthTokenHandlerInterface {
    return $this->tokenHandler;
  }

  public function __setTokenHandler(AuthTokenHandlerInterface $authHandler): void {
    $this->tokenHandler = $authHandler;
  }

  public function __getLogAllRequestsResponses(): bool {
    return $this->logAllRequestsResponses;
  }

  public function __setLogAllRequestsResponses(bool $logAll): void {
    $this->logAllRequestsResponses = $logAll;
  }

  public function __setSoapHeaders($headers = NULL): bool {
    $setHeaders = parent::__setSoapHeaders($headers);
    if ($setHeaders) {
      $this->defaultHeaders = $headers;
    }

    return $setHeaders;
  }

  /**
   * Gets the current SOAP headers that are being used for all calls using this
   * instance.
   */
  public function __getSoapHeaders(): ?array {
    return $this->defaultHeaders;
  }

  public function __soapCall(string $name, array $args, ?array $options = NULL, $inputHeaders = NULL, &$outputHeaders = NULL): mixed {
    if ($this->tokenHandler instanceof AuthTokenHandlerInterface) {
      $this->tokenHandler->preSoapCall($name, $this, $this->defaultHeaders);
    }

    // A SOAP function call can fail if the token has expired.
    // Attempt to Authenticate() to get a new token and then call the same
    // function again.
    $response = NULL;
    $faultCount = 0;
    while ($faultCount < static::MAX_SOAP_CALL_ATTEMPTS) {
      try {
        $response = parent::__soapCall($name, $args, $options, $inputHeaders, $outputHeaders);

        break;
      }
      catch (\SoapFault $fault) {
        $faultCount++;

        if ($faultCount < static::MAX_SOAP_CALL_ATTEMPTS &&
          $this->tokenHandler instanceof AuthTokenHandlerInterface && $this->tokenHandler->handlesTokenExpiry() &&
          str_starts_with($fault->getMessage(), 'System.Web.Services.Protocols.SoapException: Failed')) {
          $this->logHandler?->info('SOAP Failed fault when calling {name}. Attempting to Authenticate and retrying...', ['name' => $name]);

          $this->tokenHandler->authenticate();

          continue;
        }

        $this->logHandler?->error('SOAP fault limit reached when calling {name}, giving up.', ['name' => $name]);

        throw $fault;
      }
    }

    if ($this->tokenHandler instanceof AuthTokenHandlerInterface) {
      $this->tokenHandler->postSoapCall($name, $this, $outputHeaders);
    }

    return $response;
  }

}
