<?php

declare(strict_types=1);

namespace Drupal\lrs_xapi;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\StatementInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

/**
 * LRS reference storage class.
 */
final class XapiStatementStorage {

  public const ACTIVITY_STATE_TABLE = 'lrs_xapi_activity_state';
  public const STATEMENT_TABLE = 'lrs_xapi_statements';

  /**
   * Used verbs. All else will not be inserted to the database.
   */
  private const USED_VERBS = [
    'passed' => TRUE,
    'failed' => TRUE,
  ];

  /**
   * Supported conditions when retrieving statements, based on the storage.
   */
  private const SUPPORTED_CONDITIONS = [
    'registration' => TRUE,
    'object_id' => TRUE,
    'object_type' => TRUE,
    'verb' => TRUE,
    'data' => TRUE,
    'last_mod' => TRUE,
  ];

  /**
   * The constructor.
   */
  public function __construct(
    private readonly Connection $db,
    #[AutowireCallable(service: 'datetime.time', method: 'getRequestTime', lazy: TRUE)]
    private \Closure $getRequestTime,
  ) {}

  /**
   * Get Xapi statements.
   */
  public function getStatements(array $conditions, int $limit): array {
    return $this->getData(self::STATEMENT_TABLE, $conditions, $limit);
  }

  /**
   * Set Xapi statement.
   */
  public function setStatement(string $data): void {
    $statement_data = Json::decode($data);
    if (!\is_array($statement_data)) {
      throw new \InvalidArgumentException('Invalid statement JSON.');
    }

    // Data validation.
    $errors = [];
    if (!\array_key_exists('context', $statement_data)) {
      $errors['context'] = 'context missing in the given statement.';
    }
    elseif (!\array_key_exists('registration', $statement_data['context'])) {
      $errors['registration'] = 'registration missing in context in the given statement.';
    }
    if (!\array_key_exists('object', $statement_data)) {
      $errors['object'] = 'object missing in the given statement.';
    }
    else {
      if (!\array_key_exists('id', $statement_data['object'])) {
        $errors['object_id'] = 'id missing in object in the given statement.';
      }
      if (!\array_key_exists('objectType', $statement_data['object'])) {
        $errors['object_type'] = 'objectType missing in object in the given statement.';
      }
    }
    if (!\array_key_exists('verb', $statement_data)) {
      $errors['verb'] = 'verb missing in the given statement.';
    }
    elseif (!\array_key_exists('id', $statement_data['verb'])) {
      $errors['verb_id'] = 'id missing in verb in the given statement.';
    }

    if (\count($errors) !== 0) {
      throw new \InvalidArgumentException('Invalid statement JSON: ' . PHP_EOL . ' - ' . implode(PHP_EOL . ' - ', $errors));
    }

    // Simplify verb.
    $verb = $statement_data['verb']['id'];
    $verb = \substr($verb, \strrpos($verb, '/') + 1);

    // Filter.
    if (!\array_key_exists($verb, self::USED_VERBS)) {
      return;
    }

    // Add timestamp.
    $statement_data['timestamp'] = (string) ($this->getRequestTime)();

    $this->db->merge(self::STATEMENT_TABLE)
      ->key('registration', $statement_data['context']['registration'])
      ->key('verb', $verb)
      ->fields([
        'object_id' => $statement_data['object']['id'],
        'object_type' => $statement_data['object']['objectType'],
        'data' => Json::encode($statement_data),
        'last_mod' => $statement_data['timestamp'],
      ])
      ->execute();
  }

  /**
   * Delete statements.
   */
  public function deleteStatements(array $conditions): void {
    if (\count($conditions) === 0) {
      throw new \InvalidArgumentException('Conditions must not be empty.');
    }

    $db_statement = $this->db->delete(self::STATEMENT_TABLE);
    foreach ($conditions as $column => $value) {
      if (\is_array($value)) {
        $db_statement->condition($value[0], $value[1], $value[2]);
      }
      else {
        $db_statement->condition($column, $value);
      }
    }
    $db_statement->execute();
  }

  /**
   * Get activity state data.
   */
  public function getActivityStateData(array $conditions): array {
    return $this->getData(self::ACTIVITY_STATE_TABLE, $conditions);
  }

  /**
   * Set Xapi activity state item.
   */
  public function setActivityState(string $registration, string $state_id, string $data): void {

    $this->db->merge(self::ACTIVITY_STATE_TABLE)
      ->key('registration', $registration)
      ->key('state_id', $state_id)
      ->fields([
        'data' => $data,
        'last_mod' => ($this->getRequestTime)(),
      ])
      ->execute();
  }

  /**
   * Delete entries by registration IDs.
   */
  public function deleteByRegistrationIds(array $registration_ids): void {
    foreach ([self::STATEMENT_TABLE, self::ACTIVITY_STATE_TABLE] as $table) {
      $this->db->delete($table)
        ->condition('registration', $registration_ids, 'IN');
    }
  }

  /**
   * Code saver.
   */
  private function getData(string $table, array $conditions, int $limit = 0): array {
    if (\count($conditions) === 0) {
      throw new \InvalidArgumentException('Conditions must not be empty.');
    }

    // Simplify verb.
    if (\array_key_exists('verb', $conditions)) {
      $conditions['verb'] = \substr($conditions['verb'], \strrpos($conditions['verb'], '/') + 1);
    }

    $select = $this->db->select($table)
      ->fields($table, ['data'])
      ->orderBy('last_mod', 'DESC');
    foreach ($conditions as $column => $value) {
      if (!\array_key_exists($column, static::SUPPORTED_CONDITIONS)) {
        // Silently skip unsupported columns.
        continue;
      }
      elseif (\is_array($value)) {
        $select->condition($value[0], $value[1], $value[2]);
      }
      else {
        $select->condition($column, $value);
      }
    }

    if ($limit !== 0) {
      $select->range(0, $limit);
    }

    $db_statement = $select->execute();
    \assert($db_statement instanceof StatementInterface);
    $statements = $db_statement->fetchCol();

    return $statements;
  }

}
