<?php

namespace Drupal\dkan_metastore\Plugin\DkanApiDocs;

use Drupal\dkan_common\Plugin\DkanApiDocsBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\dkan_metastore\MetastoreService;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Docs plugin.
 *
 * @DkanApiDocs(
 *  id = "metastore_api_docs",
 *  description = "Metastore docs"
 * )
 */
class MetastoreApiDocs extends DkanApiDocsBase {

  const DOC_SCHEMAS = ['dataset'];

  /**
   * The DKAN metastore service.
   *
   * @var Drupal\dkan_metastore\Service
   */
  private $metastore;

  /**
   * Constructs a \Drupal\Component\Plugin\PluginBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $pluginId
   *   The plugin_id for the plugin instance.
   * @param mixed $pluginDefinition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler service.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
   *   The module handler service.
   * @param \Drupal\dkan_metastore\MetastoreService $metastore
   *   The metastore service.
   */
  public function __construct(
    array $configuration,
    $pluginId,
    $pluginDefinition,
    ModuleHandlerInterface $moduleHandler,
    TranslationInterface $stringTranslation,
    MetastoreService $metastore,
  ) {
    parent::__construct($configuration, $pluginId, $pluginDefinition, $moduleHandler, $stringTranslation);
    $this->metastore = $metastore;
  }

  /**
   * Container injection.
   *
   * @param \Drupal\dkan_common\Plugin\ContainerInterface $container
   *   The service container.
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $pluginId
   *   The plugin_id for the plugin instance.
   * @param mixed $pluginDefinition
   *   The plugin implementation definition.
   *
   * @return static
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $pluginId,
    $pluginDefinition,
  ) {
    return new static(
      $configuration,
      $pluginId,
      $pluginDefinition,
      $container->get('module_handler'),
      $container->get('string_translation'),
      $container->get('dkan.metastore.service')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function spec() {
    $spec = $this->getDoc('dkan_metastore');

    $exampleSchemaIds = array_values(array_filter(
      array_keys($this->metastore->getSchemas()),
      [$this, 'filterSchemaIds']
    ));
    foreach ($exampleSchemaIds as $schemaId) {
      $spec["components"]["parameters"]["schemaId"]["examples"]["$schemaId"] = ['value' => $schemaId];
    }

    $schemaIds = self::DOC_SCHEMAS;
    foreach ($schemaIds as $schemaId) {
      $spec["components"]["schemas"] += $this->schemaComponent($schemaId);
      $spec["components"]["parameters"] += $this->schemaParameters($schemaId);
      $spec['paths'] += $this->schemaPaths($schemaId);

      $tSchema = [':schemaId' => $schemaId];
      $spec['tags'][] = [
        'name' => $this->t("Metastore: :schemaId", $tSchema),
        'description' => $this->t(
          'CRUD operations for :schemaId metastore items. Substitute any other schema name for ":schemaId" to modify other items.',
          $tSchema
        ),
      ];
    }

    // Copy one of the UUID parameters to a generic one for
    // non-schema-specific URLs.
    $spec["components"]["parameters"]["exampleUuid"] = $spec["components"]["parameters"]["{$schemaIds[0]}Uuid"];

    return $spec;
  }

  /**
   * Determine whether a schemaID should be used in the docs.
   *
   * @param string $schemaId
   *   A schema ID.
   *
   * @return bool
   *   TRUE if it should be included.
   */
  private function filterSchemaIds($schemaId) {
    if (in_array($schemaId, ["legacy", "catalog"])) {
      return FALSE;
    }
    if (str_ends_with($schemaId, ".ui")) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Alter the metastore schema to make identifiers optional for posts.
   *
   * @param array $schema
   *   An openapi schema array.
   * @param string $identifierProperty
   *   The property in the schema that holds the required identifier.
   *
   * @return array
   *   Altered schema.
   */
  private function makeIdentifierOptional(array $schema, $identifierProperty = "identifier") {
    $required = $schema["required"];
    $key = array_search($identifierProperty, $required);
    if (!empty($required) && ($key !== FALSE)) {
      unset($required[$key]);
      $schema["required"] = array_values($required);
    }
    return $schema;
  }

  /**
   * Make all properties optional (for patch).
   *
   * @param array $schema
   *   Schema array.
   *
   * @return array
   *   Altered schema.
   */
  private function makeAllOptional(array $schema) {
    return self::nestedFilterKeys($schema, function ($prop) {
      if ($prop === 'required') {
        return FALSE;
      }
      return TRUE;
    });
  }

  /**
   * Add filtered schema to components from schemaId.
   *
   * @param string $schemaId
   *   Schema ID.
   *
   * @return array
   *   An array with one key, the schemaID.
   */
  private function schemaComponent($schemaId) {
    $schema = json_decode(json_encode($this->metastore->getSchema($schemaId)), TRUE);

    return [
      "{$schemaId}" => self::filterJsonSchemaUnsupported($schema),
    ];
  }

  /**
   * Create parameters with examples for each schema ID.
   *
   * @param string $schemaId
   *   The schema ID (e.g. "dataset).
   *
   * @return array
   *   Array with single key, value is full parameter array.
   */
  private function schemaParameters($schemaId) {
    return [
      "{$schemaId}Uuid" => [
        "name" => "identifier",
        "in" => "path",
        "description" => $this->t("A :schemaId identifier", [":schemaId" => $schemaId]),
        "required" => TRUE,
        "schema" => ["type" => "string"],
        "example" => $this->getExampleIdentifier($schemaId) ?: "00000000-0000-0000-0000-000000000000",
      ],
    ];
  }

  /**
   * Get a working example identifier for the schema ID.
   *
   * @param string $schemaId
   *   A schema ID.
   *
   * @return string
   *   An identifier.
   */
  private function getExampleIdentifier($schemaId) {
    if ($first = $this->metastore->getAll($schemaId, 0, 1)) {
      return $first[0]->{"$.identifier"};
    }
    return FALSE;
  }

  /**
   * Create set of paths for a specific schema.
   *
   * @param string $schemaId
   *   A schema ID.
   *
   * @return array
   *   An array of paths for openapi.
   */
  private function schemaPaths($schemaId) {
    $schema = json_decode(json_encode($this->metastore->getSchema($schemaId)), TRUE);
    $doc = [];

    $doc["/api/1/metastore/schemas/$schemaId/items"] = [
      "post" => $this->schemaItemPost($schemaId, $schema),
    ];

    $doc["/api/1/metastore/schemas/$schemaId/items/{identifier}"] = [
      "get" => $this->schemaItemGet($schemaId),
      "put" => $this->schemaItemPut($schemaId),
      "patch" => $this->schemaItemPatch($schemaId, $schema),
    ];

    return $doc;
  }

  /**
   * Create the openapi post method for a metastore item.
   *
   * @param string $schemaId
   *   A schema ID, e.g. "dataset".
   * @param array $schema
   *   The full metastore schema.
   *
   * @return array
   *   Full request array to be added to spec.
   */
  private function schemaItemPost($schemaId, array $schema) {
    $tSchema = [':schemaId' => $schemaId];
    return [
      "operationId" => "$schemaId-post",
      "summary" => $this->t("Create a new :schemaId.", $tSchema),
      "tags" => [$this->t("Metastore: :schemaId", $tSchema)],
      "security" => [['basic_auth' => []]],
      "requestBody" => [
        "required" => TRUE,
        "description" => $this->t("Takes the standard :schemaId schema, but does not require identifier.\n\nAutomatic example not yet available; try retrieving a :schemaId via GET, removing the identifier property, and pasting to test.", $tSchema),
        "content" => [
          "application/json" => [
            "schema" => self::filterJsonSchemaUnsupported($this->makeIdentifierOptional($schema)),
          ],
        ],
      ],
      "responses" => [
        "201" => [
          "description" => "Metadata creation successful.",
          "content" => [
            "application/json" => ["schema" => ['$ref' => '#/components/schemas/metastoreWriteResponse']],
          ],
        ],
        '400' => ['$ref' => '#/components/responses/400BadJson'],
        '409' => ['$ref' => '#/components/responses/409MetadataAlreadyExists'],
      ],
    ];
  }

  /**
   * Create the openapi get method for a metastore item.
   *
   * @param string $schemaId
   *   A schema ID, e.g. "dataset".
   *
   * @return array
   *   Full request array to be added to spec.
   */
  private function schemaItemGet($schemaId) {
    $tSchema = [':schemaId' => $schemaId];
    return [
      "operationId" => "$schemaId-get-item",
      "summary" => $this->t("Get a single :schemaId.", $tSchema),
      "tags" => [$this->t("Metastore: :schemaId", $tSchema)],
      "parameters" => [
        ['$ref' => "#/components/parameters/{$schemaId}Uuid"],
        ['$ref' => "#/components/parameters/showReferenceIds"],
      ],
      "responses" => [
        "200" => [
          "description" => $this->t("Full :schemaId item.", $tSchema),
          "content" => [
            "application/json" => [
              "schema" => ['$ref' => "#/components/schemas/$schemaId"],
            ],
          ],
        ],
        "404" => ['$ref' => '#/components/responses/404IdNotFound'],
      ],
    ];
  }

  /**
   * Create the openapi put method for a metastore item.
   *
   * @param string $schemaId
   *   A schema ID, e.g. "dataset".
   *
   * @return array
   *   Full request array to be added to spec.
   */
  private function schemaItemPut($schemaId) {
    $tSchema = [':schemaId' => $schemaId];
    return [
      "operationId" => "$schemaId-put",
      "summary" => $this->t("Replace a :schemaId", $tSchema),
      "description" => $this->t("Object will be completely replaced; optional properties not included in the request will be deleted.\n\nAutomatic example not yet available; try retrieving a :schemaId via GET, changing values, and pasting to test. If no item exists with the provided identifier, it will be created.", $tSchema),
      "tags" => [$this->t("Metastore: :schemaId", $tSchema)],
      "security" => [
        ['basic_auth' => []],
      ],
      "parameters" => [['$ref' => "#/components/parameters/{$schemaId}Uuid"]],
      "requestBody" => [
        "required" => TRUE,
        "content" => [
          "application/json" => [
            "schema" => ['$ref' => "#/components/schemas/$schemaId"],
          ],
        ],
      ],
      "responses" => [
        "200" => ['$ref' => '#/components/responses/200MetadataUpdated'],
        "201" => ['$ref' => '#/components/responses/201MetadataCreated'],
        '400' => ['$ref' => '#/components/responses/400BadJson'],
        '409' => ['$ref' => '#/components/responses/409MetadataAlreadyExists'],
        "404" => ['$ref' => '#/components/responses/404MetadataObjectNotFound'],
      ],
    ];
  }

  /**
   * Create the openapi patch method for a metastore item.
   *
   * @param string $schemaId
   *   A schema ID, e.g. "dataset".
   * @param array $schema
   *   The full metastore schema.
   *
   * @return array
   *   Full request array to be added to spec.
   */
  private function schemaItemPatch($schemaId, array $schema) {
    $tSchema = [':schemaId' => $schemaId];
    return [
      "operationId" => "$schemaId-patch",
      "summary" => $this->t("Modify an existing :schemaId", $tSchema),
      "description" => $this->t("Values provided will replace existing values, but required values may be omitted.\n\nAutomatic example not yet available; try retrieving a :schemaId via GET, changing values, removing unchanged properties, and pasting to test.", $tSchema),
      "tags" => [$this->t("Metastore: :schemaId", $tSchema)],
      "security" => [
        ['basic_auth' => []],
      ],
      "parameters" => [['$ref' => "#/components/parameters/{$schemaId}Uuid"]],
      "requestBody" => [
        "required" => TRUE,
        "content" => [
          "application/json" => [
            "schema" => self::filterJsonSchemaUnsupported($this->makeAllOptional($schema)),
          ],
        ],
      ],
      "responses" => [
        "200" => ['$ref' => '#/components/responses/200MetadataUpdated'],
        '400' => ['$ref' => '#/components/responses/400BadJson'],
        "404" => ['$ref' => '#/components/responses/404MetadataObjectNotFound'],
      ],
    ];
  }

}
