<?php

namespace Drupal\dkan_metastore_search;

use Drupal\dkan_common\Events\Event;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\dkan_metastore\MetastoreService;
use Drupal\search_api\Query\ResultSet;
use Drupal\search_api\Utility\QueryHelperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Search.
 *
 * Provides search results and facets information from a SearchAPI index.
 *
 * @package Drupal\dkan_metastore_search
 */
class Search implements ContainerInjectionInterface {
  use QueryBuilderTrait;
  use FacetsFromIndexTrait;
  // @todo Use real classes to get proper encapsulation.
  use FacetsFromContentTrait;

  const EVENT_SEARCH = 'dkan_metastore_search_search';
  const EVENT_SEARCH_PARAMS = 'dkan_metastore_search_search_params';

  // @todo this constant is used by QueryBuilder, maybe we need a class.
  const EVENT_SEARCH_QUERY_BUILDER_CONDITION = 'dkan_metastore_search_query_builder_condition';

  /**
   * The search index.
   *
   * @var \Drupal\search_api\IndexInterface
   */
  private $index;

  /**
   * Metastore service.
   *
   * @var \Drupal\dkan_metastore\MetastoreService
   */
  private $metastoreService;

  /**
   * Entity Type Manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  private $entityTypeManager;

  /**
   * Query helper.
   *
   * @var \Drupal\search_api\Utility\QueryHelperInterface
   */
  private $queryHelper;

  /**
   * Event dispatcher service.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  private $eventDispatcher;

  /**
   * Constructor.
   *
   * @param \Drupal\dkan_metastore\MetastoreService $metastoreService
   *   Metastore service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\search_api\Utility\QueryHelperInterface $queryHelper
   *   Query helper.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   Event dispatcher.
   */
  public function __construct(
    MetastoreService $metastoreService,
    EntityTypeManagerInterface $entityTypeManager,
    QueryHelperInterface $queryHelper,
    EventDispatcherInterface $eventDispatcher,
  ) {
    $this->metastoreService = $metastoreService;
    $this->entityTypeManager = $entityTypeManager;
    $this->queryHelper = $queryHelper;
    $this->eventDispatcher = $eventDispatcher;

    $this->setSearchIndex('dkan');
  }

  /**
   * Create.
   *
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('dkan.metastore.service'),
      $container->get('entity_type.manager'),
      $container->get('search_api.query_helper'),
      $container->get('event_dispatcher')
    );
  }

  /**
   * Search.
   *
   * Returns an object with 2 properties: total (the total number of records
   * without limits), and results (An array with the result objects).
   *
   * @param array $params
   *   Search parameters.
   */
  public function search(array $params = []) {
    $event = new Event($params);
    $this->eventDispatcher->dispatch($event, self::EVENT_SEARCH_PARAMS);
    $params = $event->getData();
    $query = $this->getQuery($params, $this->index, $this->queryHelper)[0];
    $result = $query->execute();

    $count = $result->getResultCount();
    $data = $this->getData($result);

    $event = new Event($data);
    $this->eventDispatcher->dispatch($event, self::EVENT_SEARCH);
    $data = $event->getData();

    return (object) [
      'total' => $count,
      'results' => $data,
    ];
  }

  /**
   * Facets.
   *
   * @param array $params
   *   Array of search parameters.
   *
   * @return array
   *   Array of facets, each containing type, name, total.
   */
  public function facets(array $params = []) : array {
    $params['page-size'] = PHP_INT_MAX;
    $params['page'] = 1;

    [$query, $activeConditions] = $this->getQuery($params, $this->index, $this->queryHelper);

    if ($activeConditions) {
      $facets = $this->getFacetsFromContent($params, $query);
    }
    else {
      $facets = $this->getFacetsFromIndex($params, $this->index, $query);
    }

    static::orderFacets($facets);
    return $facets;
  }

  /**
   * Order the facet array by total (desc) then name (asc)
   *
   * @param array $facets
   *   Array of facet objects with properties "total", "name" and "type".
   */
  public static function orderFacets(array &$facets): void {
    usort($facets, function ($a, $b) {
      if (!isset($a->name, $b->name, $a->total, $b->total)) {
        throw new \InvalidArgumentException("Facets much name and total properties.");
      }
      if ($a->total == $b->total) {
        return strcmp($a->name, $b->name);
      }
      else {
        return $b->total - $a->total;
      }
    });
  }

  /**
   * Private.
   *
   * @param \Drupal\search_api\Query\ResultSet $result
   *   Result set.
   *
   * @return null|array
   *   Filtered results.
   */
  private function getData(ResultSet $result) {
    $metastore = $this->metastoreService;

    return array_filter(array_map(
      function ($item) use ($metastore) {
        $id = $item->getId();
        $id = str_replace('dkan_dataset/', '', $id);
        try {
          return json_decode((string) $metastore->get('dataset', $id));
        }
        catch (\Exception) {
          return NULL;
        }
      },
      $result->getResultItems()
    ));
  }

  /**
   * Set the dkan search index.
   *
   * @param string $id
   *   Search index identifier.
   */
  private function setSearchIndex(string $id) {
    $storage = $this->entityTypeManager
      ->getStorage('search_api_index');
    $this->index = $storage->load($id);

    if (!$this->index) {
      throw new \Exception("An index named [{$id}] does not exist.");
    }
  }

  /**
   * Get the current search index.
   *
   * @return \Drupal\search_api\IndexInterface|null
   *   If available, return the search index.
   */
  public function getSearchIndex() {
    return $this->index ?? NULL;
  }

}
