<?php

declare(strict_types=1);

namespace Drupal\lms_xapi;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use TinCan\RemoteLRS;
use TinCan\Statement;
use TinCan\Util;
use TinCan\Verb;

/**
 * Tincan API integration utilities.
 */
final class XapiService {

  public const CONFIG_NAME = 'lms_xapi.settings';

  public function __construct(
    private readonly LRSReferenceStorage $storage,
    #[AutowireCallable(service: 'config.factory', method: 'get', lazy: TRUE)]
    private \Closure $getConfig,
    #[AutowireIterator('xapi_id_generator')]
    private iterable $xapiIdGenerators,
    private readonly FileUrlGeneratorInterface $fileUrlGenerator,
    #[Autowire(service: 'logger.channel.lms_xapi', lazy: TRUE)]
    private readonly LoggerChannelInterface $logger,
  ) {}

  /**
   * Generate an ID for a Xapi submission.
   *
   * NOTE: LMS ID must always start with user ID for easy search.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that the XAPI export is attached to.
   * @param \Drupal\Core\Session\AccountInterface $student
   *   The student assigned to the generated LRS reference.
   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
   *   A cacheable metadata object.
   */
  public function getLmsId(
    EntityInterface $entity,
    AccountInterface $student,
    CacheableMetadata $cacheable_metadata,
  ): string {
    $parts = [];
    foreach ($this->xapiIdGenerators as $generator) {
      if ($generator->applies($entity, $student, $cacheable_metadata)) {
        if (\count($parts) !== 0) {
          throw new \Exception('More than one Xapi ID generator applies in the same case.');
        }
        $parts = [$student->id()];
        foreach ($generator->generate($entity, $student, $cacheable_metadata) as $part) {
          $parts[] = $part;
        }
      }
    }

    // Default ID fallback.
    if (\count($parts) === 0) {
      $parts = [
        $student->id(),
        $entity->getEntityTypeId(),
        $entity->id(),
      ];
    }
    // Add a trailing separator ":" so we're sure the entire part can be
    // unique when searching.
    return \implode(':', $parts) . ':';
  }

  /**
   * Get LRS - LMS user - activity UUID.
   */
  public function getLrsUuid(string $lms_id, bool $create = TRUE): string {
    $uuid = $this->storage->getStoredLrsUuid($lms_id);
    if ($uuid === '' && $create) {
      // @todo Although extremely unlikely, it may happen that one randomly
      // generated UUID may already be in the LRS or in the internal storage.
      // To avoid that, consider generating it using the site UUID, user ID,
      // entity type ID and entity ID.
      $inserted = FALSE;
      do {
        $uuid = Util::getUUID();
        try {
          $this->storage->saveLrsUuid($uuid, $lms_id);
          $inserted = TRUE;
        }
        catch (DatabaseException $e) {

        }
      } while (!$inserted);
    }
    return $uuid;
  }

  /**
   * Gets the remote LRS via configuration.
   */
  public function getRemoteLrs(): ?RemoteLrs {
    $config = ($this->getConfig)(self::CONFIG_NAME);
    $settings = [
      'endpoint' => $config->get('endpoint'),
      'username' => $config->get('username'),
      'password' => $config->get('password'),
    ];
    foreach ($settings as $value) {
      if ($value === NULL || $value === '') {
        return NULL;
      }
    }

    // Support relative URLs.
    if (!\str_starts_with($settings['endpoint'], 'http')) {
      $settings['endpoint'] = Url::fromRoute('<front>', [], [
        'absolute' => TRUE,
      ])->toString() . \ltrim($settings['endpoint'], '/');
    }

    // @todo This logic is taken from opigno_tincan_activity module, should be
    // revised as it's a bit messy. Also maybe worth considering to drop the
    // rusticisoftware/tincan dependency as it doesn't have interfaces, docs
    // or type hints in most cases.
    // It'll be much cleaner to use Guzzle here since those requests are
    // really simple as well as returned JSON.
    $lrs = new RemoteLRS(
      $settings['endpoint'],
      '1.0.1',
      $settings['username'],
      $settings['password']
    );

    return $lrs;
  }

  /**
   * Helper method that gets latest statement by uuid.
   */
  public function getLatestStatementByUuid(string $uuid, array $verbs = []): ?Statement {
    if (\count($verbs) === 0) {
      $verbs = [
        'passed' => new Verb(['id' => 'http://adlnet.gov/expapi/verbs/passed']),
        'failed' => new Verb(['id' => 'http://adlnet.gov/expapi/verbs/failed']),
      ];
    }

    $lrs = $this->getRemoteLrs();

    $latest_statement = NULL;
    try {
      foreach ($verbs as $verb) {
        $result = $lrs->queryStatements([
          'registration' => $uuid,
          'verb' => $verb,
        ]);
        if (\is_object($result->content)) {
          foreach ($result->content->getStatements() as $statement) {
            if (
              $latest_statement === NULL ||
              $latest_statement->getTimestamp() < $statement->getTimestamp()
            ) {
              $latest_statement = $statement;
            }
          }
        }
      }
    }
    catch (\Exception $e) {
      $this->logger->error($e->getMessage());
    }
    return $latest_statement;
  }

  /**
   * Get activity score from LRS.
   */
  public function getScoreFromLrs(string $lms_id): ?float {

    $uuid = $this->getLrsUuid($lms_id, FALSE);
    if ($uuid === '') {
      return NULL;
    }

    $latest_statement = $this->getLatestStatementByUuid($uuid);

    if ($latest_statement === NULL) {
      return NULL;
    }

    $result = $latest_statement->getResult();
    if ($result === NULL) {
      return NULL;
    }

    $score = $result->getScore();
    if ($score !== NULL) {
      $scaled = $score->getScaled();
      if ($scaled !== NULL && $scaled >= 0) {
        return $scaled;
      }

      $raw = $score->getRaw();
      $max = $score->getMax();
      $min = $score->getMin();
      if (!(bool) $min) {
        $min = 0;
      }

      if ((bool) $raw && (bool) $max) {
        return ((float) ($raw - $min) / ($max - $min));
      }
    }

    $success = $result->getSuccess();
    if ((bool) $success) {
      return 1;
    }

    return NULL;
  }

  /**
   * Get package launch URL.
   */
  public function getLaunchUrl(
    string $package_path,
    EntityInterface $entity,
    AccountInterface $account,
    CacheableMetadata $cacheable_metadata,
  ): ?string {
    $tincan_file = $package_path . '/tincan.xml';
    if (!\file_exists($tincan_file)) {
      $this->logger->error(\sprintf('Xapi package file missing: %s.', $tincan_file));
      return NULL;
    }
    $tincan_file_contents = \file_get_contents($tincan_file);
    if ($tincan_file_contents === FALSE) {
      $this->logger->error(\sprintf('Unable to get contents of %s file.', $tincan_file));
      return NULL;
    }

    $config = ($this->getConfig)(self::CONFIG_NAME);
    $launch_config = [
      'endpoint' => $config->get('endpoint'),
    ];
    if (Settings::get('lms_xapi_disable_auth') !== TRUE) {
      $launch_config += [
        'username' => $config->get('username'),
        'password' => $config->get('password'),
      ];
    }
    foreach ($launch_config as $key => $value) {
      if ($value === NULL || $value === '') {
        $this->logger->error(\sprintf('Missing Xapi config: %s.', $key));
        return NULL;
      }
    }

    $xml = new \SimpleXMLElement($tincan_file_contents);
    $launch_file = $package_path . '/' . $xml->activities->activity->launch;
    $launch_url = $this->fileUrlGenerator->generateAbsoluteString($launch_file);

    // Support relative URLs.
    if (!\str_starts_with($launch_config['endpoint'], 'http')) {
      $launch_config['endpoint'] = Url::fromRoute('<front>', [], [
        'absolute' => TRUE,
      ])->toString() . \ltrim($launch_config['endpoint'], '/');
    }
    $lms_id = $this->getLmsId($entity, $account, $cacheable_metadata);

    $query_args = [
      'endpoint' => $launch_config['endpoint'],
      'actor' => Json::encode([
        'mbox_sha1sum' => sha1('mailto:' . $account->getEmail()),
        'name' => $account->getAccountName(),
      ]),
      'registration' => $this->getLrsUuid($lms_id),
    ];
    if (Settings::get('lms_xapi_disable_auth') !== TRUE) {
      $query_args['auth'] = 'Basic ' . \base64_encode($launch_config['username'] . ':' . $launch_config['password']);
    }
    $launch_url .= '?' . \http_build_query($query_args);
    return $launch_url;
  }

}
