<?php

namespace Drupal\acquia_contenthub\Commands;

use Consolidation\AnnotatedCommand\CommandError;
use Consolidation\AnnotatedCommand\Input\StdinHandler;
use Drupal\acquia_contenthub\Client\ClientFactory;
use Drupal\acquia_contenthub\Commands\Traits\ColorizedOutputTrait;
use Drupal\acquia_contenthub\Libs\Traits\ResponseCheckerTrait;
use Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface;
use Drush\Commands\DrushCommands;
use Drush\Exceptions\UserAbortException;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Console\Input\InputInterface;

/**
 * Provides low level commands for direct interaction with Content Hub API.
 */
class AcquiaContentHubApiCommands extends DrushCommands {

  use ColorizedOutputTrait;
  use ResponseCheckerTrait;

  /**
   * The contenthub client object.
   *
   * @var \Acquia\ContentHubClient\ContentHubClient|bool
   */
  protected $client;

  /**
   * Factory for instantiating ContentHubClient.
   *
   * @var \Drupal\acquia_contenthub\Client\ClientFactory
   */
  protected ClientFactory $clientFactory;

  /**
   * Content Hub configurations.
   *
   * @var \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface
   */
  protected ContentHubConfigurationInterface $achConfigurations;

  /**
   * Constructs this class.
   *
   * @param \Drupal\acquia_contenthub\Client\ClientFactory $client_factory
   *   Factory for instantiating ContentHubClient.
   * @param \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface $achConfigurations
   *   Content Hub configurations.
   */
  public function __construct(ClientFactory $client_factory, ContentHubConfigurationInterface $achConfigurations) {
    $this->clientFactory = $client_factory;
    $this->achConfigurations = $achConfigurations;
  }

  /**
   * Check if Content Hub Client is available.
   *
   * @hook validate acquia:contenthub:api
   *
   * @throws \Exception
   */
  public function checkClient(): ?CommandError {
    $this->client = $this->clientFactory->getClient();
    if ($this->client) {
      return NULL;
    }
    return new CommandError('Client is not available. Please check configuration or register the client!', self::EXIT_FAILURE);
  }

  /**
   * Validates method argument.
   *
   * @hook validate acquia:contenthub:api
   *
   * @throws \Exception
   */
  public function validateHttpMethod(): ?CommandError {
    $method = $this->input()->getArgument('method');
    switch ($method) {
      case 'get':
      case 'post':
      case 'put':
      case 'delete':
        return NULL;

      default:
        return new CommandError('Invalid method specified, valid methods: get, post, put, delete', self::EXIT_FAILURE);
    }
  }

  /**
   * Formats query params to make it digestible for the commands.
   *
   * @hook init acquia:contenthub:api
   */
  public function formatQueryParams(): void {
    $option = $this->input()->getOption('query-params');
    if (empty($option)) {
      $this->input()->setOption('query-params', []);
      return;
    }

    $query = [];
    $segments = explode('&', $option);
    foreach ($segments as $segment) {
      [$key, $value] = explode('=', $segment);
      $query[$key] = $value;
    }

    $this->input()->setOption('query-params', $query);
  }

  /**
   * Parses the body option.
   *
   * If it is a file, reads its content. If not it uses it as is.
   *
   * @hook init acquia:contenthub:api
   */
  public function parseBodyOption(InputInterface $input): void {
    $option = $this->input()->getOption('body');
    if (is_string($option)) {
      return;
    }
    $data = StdinHandler::selectStream($input, 'body')->contents();
    $this->input()->setOption('body', $data);
  }

  /**
   * Request Content Hub API directly.
   *
   * This command provides a low level interface to interact with the Content
   * Hub API. It is a powerful tool, and must be used with caution!
   * The output is in json format.
   *
   * @param string $method
   *   The http method.
   * @param string $endpoint
   *   The endpoint to call.
   *
   * @option query-params
   *   Query parameters, e.g. --query-params
   *   "type=client&filter=entity_label=My%20Node"
   * @default query-params ''
   * @option body
   *   The request body
   * @default body ''
   * @option more
   *   Increases verbosity, returns more data about the response
   *
   * @command acquia:contenthub:api
   * @aliases ach-api
   *
   * @usage acquia:contenthub:api get entities/e84aa78a-873d-4a4a-b9d0-cde24c0020a6
   *   Returns data about the specified resource in json format.
   * @usage acquia:contenthub:api post entities --body "$(cat post_body.json)" | jq
   *   Reads data from json file and pipes it through jq for readability.
   * @usage acquia:contenthub:api post entities --body < post_body.json --more | jq
   *   Sends the stream to stdin from file for body and prints additional
   *   details from the response.
   * @usage acquia:contenthub:api put entities/e84aa78a-873d-4a4a-b9d0-cde24c0020a6 --body '{"label": "modified label"}'
   *   Modifies a resource.
   * @usage acquia:contenthub:api delete entities/e84aa78a-873d-4a4a-b9d0-cde24c0020a6 --query-params "propagate=false"
   *   Deletes a resource while also preventing propagation of the event by
   *   passing the query parameter.
   * @usage acquia:contenthub:api delete entities --body '["uuid1","uuid2"]'
   *    Deletes all the resources mentioned in the array at once.
   *
   * @throws \Exception
   */
  public function api(string $method, string $endpoint): int {
    try {
      /** @var \Psr\Http\Message\ResponseInterface $result */
      $result = $this->{$method}($endpoint);
    }
    catch (\Exception $e) {
      $this->output()->writeln($this->error(sprintf('Error: %s', $e->getMessage())));
      return self::EXIT_FAILURE;
    }

    if (!$this->input()->getOption('more')) {
      $this->output()->write($result->getBody());
      return self::EXIT_SUCCESS;
    }

    $output = [
      'method' => $method,
      'endpoint' => $endpoint,
      'query_params' => $this->input()->getOption('query-params'),
      'request_body' => $this->input()->getOption('body'),
      'client_configuration' => $this->client->getConfig(),
      'response_headers' => $result->getHeaders(),
      'status_code' => $result->getStatusCode(),
      'response_body' => json_decode($result->getBody(), TRUE),
    ];

    $this->output()->write(json_encode($output));
    return self::EXIT_SUCCESS;
  }

  /**
   * Sends a GET request to the service.
   *
   * @param string $endpoint
   *   The endpoint to send the request to.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The server response.
   */
  public function get(string $endpoint): ResponseInterface {
    $query_params = $this->input()->getOption('query-params');
    $options = ['query' => $query_params];

    return $this->client->get($endpoint, $options);
  }

  /**
   * Sends a POST request to the service.
   *
   * @param string $endpoint
   *   The endpoint to send the request to.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The server response.
   */
  public function post(string $endpoint): ResponseInterface {
    $options = [
      'query' => $this->input()->getOption('query-params'),
      'body' => $this->input()->getOption('body'),
    ];

    return $this->client->post($endpoint, $options);
  }

  /**
   * Sends a PUT request to the service.
   *
   * @param string $endpoint
   *   The endpoint to send the request to.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The server response.
   */
  public function put(string $endpoint): ResponseInterface {
    $options = [
      'query' => $this->input()->getOption('query-params'),
      'body' => $this->input()->getOption('body'),
    ];

    return $this->client->put($endpoint, $options);
  }

  /**
   * Sends a DELETE request to the service.
   *
   * @param string $endpoint
   *   The endpoint to send the request to.
   *
   * @return \Psr\Http\Message\ResponseInterface
   *   The server response.
   *
   * @throws \Drush\Exceptions\UserAbortException
   */
  public function delete(string $endpoint): ResponseInterface {
    $yes = $this->confirm(sprintf('Are you sure you want to delete the resource: %s?', $endpoint));
    if (!$yes) {
      throw new UserAbortException('The deletion process has been aborted.');
    }
    $options = [
      'query' => $this->input()->getOption('query-params'),
      'body' => $this->input()->getOption('body'),
    ];
    return $this->client->delete($endpoint, $options);
  }

}
