<?php

declare(strict_types=1);

namespace Drupal\languagewire_translation_provider\api\Core;

use Drupal\languagewire_translation_provider\api\Core\ContentType\ContentJson;
use Drupal\languagewire_translation_provider\api\Core\ContentType\ContentTypeInterface;
use Drupal\languagewire_translation_provider\api\Core\DataType\DataArrayObject;
use Drupal\languagewire_translation_provider\api\Core\DataType\DataTypeInterface;
use Drupal\languagewire_translation_provider\api\Core\Exception\Error;
use Drupal\languagewire_translation_provider\api\Core\Exception\NonRecoverableError;
use Drupal\languagewire_translation_provider\api\Core\Exception\RecoverableError;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException;
use Psr\Http\Message\ResponseInterface;

/**
 * Project API library.
 */
abstract class ProjectAPILibraryAbstract implements ProjectAPILibraryInterface {
  private const IDENTIFIER = 'project-api';
  private const VERSION = '1.0.0';
  protected const ENDPOINT = NULL;
  protected const METHOD = NULL;
  protected const REQUEST_STRUCTURE = NULL;
  protected const RESPONSE_STRUCTURE = NULL;
  protected const RESPONSE_SUCCESS_CODES = [200];
  protected const RESPONSE_RECOVERABLE_CODES = [
    // 'Request Timeout'
    408,
    // 'Too Many Requests'
    429,
    // 'Internal Server Error'
    500,
    // 'Bad Gateway'
    502,
    // 'Service Unavailable'
    503,
    // 'Gateway Timeout'
    504,
    // 'Insufficient Storage'
    507,
    // 'Loop Detected'
    508,
    // 'Not Extended'
    510,
    // 'Network Authentication Required'
    511,
  ];
  protected const RESPONSE_NON_RECOVERABLE_CODES = [
    // 'Bad Request',
    400,
    // 'Unauthorized',
    401,
    // 'Forbidden',
    403,
    // 'Not Found',
    404,
    // 'Method Not Allowed',
    405,
    // 'Not Acceptable',
    406,
    // 'Proxy Authentication Required',
    407,
    // 'Conflict',
    409,
    // 'Gone',
    410,
    // 'Length Required',
    411,
    // 'Precondition Failed',
    412,
    // 'Payload Too Large',
    413,
    // 'URI Too Long',
    414,
    // 'Unsupported Media Type',
    415,
    // 'Range Not Satisfiable',
    416,
    // 'Expectation Failed',
    417,
    // 'Unprocessable Entity',
    422,
    // 'Locked',
    423,
    // 'Failed Dependency',
    424,
    // 'Upgrade Required',
    426,
    // 'Precondition Required',
    428,
    // 'Request Header Fields Too Large',
    431,
    // 'Unavailable For Legal Reasons',
    451,
    // 'Not Implemented',
    501,
    // 'HTTP Version Not Supported',
    505,
  ];

  /**
   * API Settings.
   *
   * @var Settings
   */
  private Settings $settings;

  /**
   * Request Content Type.
   *
   * @var \Drupal\languagewire_translation_provider\api\Core\ContentType\ContentTypeInterface
   */
  private ContentTypeInterface $requestContentType;

  /**
   * Response Content Type.
   *
   * @var \Drupal\languagewire_translation_provider\api\Core\ContentType\ContentTypeInterface
   */
  private ContentTypeInterface $responseContentType;

  /**
   * Request Data Type.
   *
   * @var \Drupal\languagewire_translation_provider\api\Core\DataType\DataTypeInterface
   */
  private DataTypeInterface $requestDataType;

  /**
   * Response Data Type.
   *
   * @var \Drupal\languagewire_translation_provider\api\Core\DataType\DataTypeInterface
   */
  private DataTypeInterface $responseDataType;

  /**
   * Headers.
   *
   * @var array
   */
  private array $headers = [];

  /**
   * Params.
   *
   * @var array
   */
  private array $params = [];

  /**
   * Exception Error.
   *
   * @var \Drupal\languagewire_translation_provider\api\Core\Exception\Error|RecoverableError|NonRecoverableError|GuzzleException|RequestException|ConnectException
   */
  private Error|RecoverableError|NonRecoverableError|GuzzleException|RequestException|ConnectException $error;

  public function __construct(Settings $settings) {
    $this->settings = $settings;

    $contentJson = new ContentJson();

    $dataArrayRequest = new DataArrayObject();
    $dataArrayRequest->setStructure(static::REQUEST_STRUCTURE ?? []);

    $this->setRequestContentType($contentJson);
    $this->setRequestDataType($dataArrayRequest);

    $dataArrayResponse = new DataArrayObject();
    $dataArrayResponse->setStructure(static::RESPONSE_STRUCTURE ?? []);

    $this->setResponseContentType($contentJson);
    $this->setResponseDataType($dataArrayResponse);
    $this->setHeaders();
  }

  /**
   * Execute.
   *
   * @return array|NULL
   *   Response or null if error occurred.
   */
  public function execute(): ?array {
    try {
      $response = $this->makeCall();

      $contents = $response->getBody()->getContents();

      $data = !empty($contents) ? $this->getResponseContentType()->decode($contents) : [];

      $statusCode = $response->getStatusCode();

      $success = in_array($statusCode, static::RESPONSE_SUCCESS_CODES);
      $recoverable = in_array($statusCode, static::RESPONSE_RECOVERABLE_CODES);
      $nonRecoverable = in_array($statusCode, static::RESPONSE_NON_RECOVERABLE_CODES);

      if ($success && $this->getResponseDataType()->validate($data)) {
        return $data;
      }
      elseif ($recoverable) {
        throw new RecoverableError('Recoverable status code:' . $contents, $statusCode);
      }

      throw new NonRecoverableError($nonRecoverable ? 'Unrecoverable status code:' . $contents : 'Invalid response data', $statusCode);
    }
    catch (GuzzleException | RequestException | RecoverableError | NonRecoverableError | Exception $e) {
      $this->error = $e;

      return NULL;
    }
  }

  /**
   * Get error object.
   */
  public function getError(): Error|RecoverableError|NonRecoverableError|NULL {
    return $this->error ?? NULL;
  }

  /**
   * Get error message.
   */
  public function getErrorMessage(): ?string {
    return isset($this->error) ? $this->error->getMessage() : NULL;
  }

  /**
   * Get additional headers.
   */
  protected function getAdditionalHeaders(): array {
    return $this->headers;
  }

  /**
   * Set additional headers.
   */
  protected function setAdditionalHeaders(array $headers): void {
    $this->headers = $headers;
  }

  /**
   * Get request content type.
   */
  protected function getRequestContentType(): ContentTypeInterface {
    return $this->requestContentType;
  }

  /**
   * Set request content type.
   */
  protected function setRequestContentType(ContentTypeInterface $requestContentType): void {
    $this->requestContentType = $requestContentType;
  }

  /**
   * Get response content type.
   */
  protected function getResponseContentType(): ContentTypeInterface {
    return $this->responseContentType;
  }

  /**
   * Set response content type.
   */
  protected function setResponseContentType(ContentTypeInterface $responseContentType): void {
    $this->responseContentType = $responseContentType;
  }

  /**
   * Get request data type.
   */
  protected function getRequestDataType(): DataTypeInterface {
    return $this->requestDataType;
  }

  /**
   * Set request data type.
   */
  protected function setRequestDataType(DataTypeInterface $requestDataType): void {
    $this->requestDataType = $requestDataType;
  }

  /**
   * Get response data type.
   */
  protected function getResponseDataType(): DataTypeInterface {
    return $this->responseDataType;
  }

  /**
   * Set response data type.
   */
  protected function setResponseDataType(DataTypeInterface $responseDataType): void {
    $this->responseDataType = $responseDataType;
  }

  /**
   * Set params.
   */
  protected function setParams(array $params): void {
    $this->params = $params;
  }

  /**
   * Set headers.
   */
  private function setHeaders() : void {
    $this->headers = [
      'x-connector'             => $this->settings->getIdentifierName(),
      'x-connector-version'     => $this->settings->getIdentifierVersion(),
      'x-cms-version'           => $this->settings->getCmsVersion(),
      'x-client-library'        => self::IDENTIFIER,
      'x-client-library-version' => self::VERSION,
    ];
  }

  /**
   * Get params.
   */
  private function getParams(?string $dataPosition = NULL): mixed {
    $defaultDataPosition = $this->getMethod() === Method::GET ? DataPosition::HEAD : DataPosition::BODY;

    $params = $this->params;

    $hasPosition = $dataPosition !== NULL;
    if ($hasPosition && isset($params[$dataPosition])) {
      return $params[$dataPosition];
    }
    elseif ($hasPosition && $dataPosition !== $defaultDataPosition) {
      return NULL;
    }

    return $params;
  }

  /**
   * Get method.
   */
  private function getMethod(): string {
    return static::METHOD ?? 'GET';
  }

  /**
   * Make call.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The response object.
   *
   * @throws \InvalidArgumentException
   *   Invalid argument exception.
   *
   * @throws \GuzzleHttp\Exception\GuzzleException
   *   Http execution exception.
   */
  private function makeCall(): ResponseInterface {
    if (static::ENDPOINT === NULL) {
      throw new \InvalidArgumentException('Endpoint cannot be null');
    }
    elseif (!$this->getRequestDataType()->validate($this->getParams())) {
      throw new \InvalidArgumentException('Invalid or missing request data');
    }

    return (new Client(['http_errors' => FALSE]))->request($this->getMethod(), $this->buildUrl(), $this->buildOptions());
  }

  /**
   * Build options.
   */
  private function buildOptions(): array {
    $requestContentType = $this->getRequestContentType();

    $headers = [
      'Accept' => $this->getResponseContentType()->getContentTypeString(),
      'Content-Type' => $requestContentType->getContentTypeString(),
      'Authorization' => 'Bearer ' . $this->settings->getAuthorizationToken(),
    ];

    $options = [
      'headers' => $headers + $this->getAdditionalHeaders(),
    ];

    $params = $this->getParams(DataPosition::BODY);

    if ($this->getMethod() !== Method::GET && !empty($params)) {
      if ($requestContentType instanceof ContentJson) {
        $options['json'] = $params;
      }
      else {
        $options['body'] = is_array($params) ? $requestContentType->encode($params) : $params;
      }
    }

    return $options;
  }

  /**
   * Build url.
   */
  private function buildUrl(): string {
    $endpoint = static::ENDPOINT;

    if (str_contains($endpoint, 'https://') === TRUE) {
      $url = $endpoint;
    }
    else {
      $url = $this->settings->getEndpointUri() . '/' . $endpoint;
    }

    $getParams = [];
    foreach ($this->getParams(DataPosition::HEAD) ?? [] as $key => $value) {
      $keyName = ':' . $key;

      if (str_contains($url, $keyName)) {
        $url = str_replace($keyName, (string) $value, $url);
      }
      else {
        $getParams[$key] = $value;
      }
    }

    if (count($getParams) > 0) {
      $url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . http_build_query($getParams);
    }

    return $url;
  }

}
