<?php

declare(strict_types=1);

namespace Drupal\lms_xapi\Plugin\Field\FieldType;

use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\file\FileInterface;
use Drupal\file\Plugin\Field\FieldType\FileItem;
use Symfony\Component\Filesystem\Filesystem;

/**
 * Xapi field item plugin implementation.
 */
#[FieldType(
  id: "lms_xapi",
  label: new TranslatableMarkup("Xapi package"),
  description: new TranslatableMarkup("Stores path to Xapi package files."),
  category: "lms",
  default_widget: "lms_xapi",
  default_formatter: "lms_xapi",
  cardinality: 1,
)]
final class XapiItem extends FileItem {

  use StringTranslationTrait;

  public const PATH_BASE = 'public://lms_xapi_packages/';

  /**
   * {@inheritdoc}
   */
  public static function defaultFieldSettings(): array {
    $settings = parent::defaultFieldSettings();
    $settings['file_extensions'] = 'zip';
    $settings['file_directory'] = 'lms_xapi_packages';
    return $settings;
  }

  /**
   * {@inheritdoc}
   */
  public function fieldSettingsForm(array $form, FormStateInterface $form_state): array {
    $element = parent::fieldSettingsForm($form, $form_state);

    $defaults = self::defaultFieldSettings();
    foreach (['file_extensions', 'file_directory'] as $key) {
      $element[$key] = [
        '#type' => 'value',
        '#value' => $defaults[$key],
      ];
    }

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function delete(): void {
    if ($this->isEmpty()) {
      return;
    }

    $path = $this->getPackagePath();
    $filesystem = new Filesystem();
    if (
      $path === '' ||
      !$filesystem->exists($path)
    ) {
      return;
    }

    $filesystem->remove($path);
  }

  /**
   * {@inheritdoc}
   */
  public function preSave(): void {
    $path = $this->getPackagePath();
    if ($path === '') {
      parent::preSave();
      return;
    }

    $filesystem = new Filesystem();

    if ($filesystem->exists($path)) {
      // Check if a new file was uploaded and only then proceed.
      if ($this->entity->getCreatedTime() < \filemtime($path)) {
        parent::preSave();
        return;
      }
      $filesystem->remove($path);
    }

    $file = $this->entity;
    \assert($file instanceof FileInterface);

    $zip = new \ZipArchive();
    $zip_path = \Drupal::service('file_system')->realpath($file->getFileUri());
    $result = $zip->open($zip_path);
    if ($result !== TRUE) {
      \Drupal::messenger()->addError($this->getError($result));
      return;
    }

    $filesystem->mkdir($path);
    $zip->extractTo($path);
    $zip->close();

    if (!$this->validatePackage($path)) {
      $filesystem->remove($path);
    }

    parent::preSave();
  }

  /**
   * Get full path of the extracted package.
   */
  public function getPackagePath(): string {
    $file = $this->entity;
    if (!$file instanceof FileInterface) {
      return '';
    }
    $folder = \preg_replace(["/[^a-zA-Z0-9]/"], "_", \pathinfo($file->getFilename(), PATHINFO_FILENAME));
    return self::PATH_BASE . $folder;
  }

  /**
   * Validate package.
   */
  private function validatePackage(string $path): bool {
    $tincan_file = $path . '/tincan.xml';
    if (!\file_exists($tincan_file)) {
      \Drupal::messenger()->addError($this->t('No tincan.xml file present in the package root.'));
      return FALSE;
    }
    $xml = new \SimpleXMLElement(\file_get_contents($tincan_file));
    if (
      !\property_exists($xml, 'activities') ||
      !\property_exists($xml->activities, 'activity') ||
      !\property_exists($xml->activities->activity, 'launch')
    ) {
      \Drupal::messenger()->addError($this->t('No launch file information present in tincan.xml.'));
      return FALSE;
    }

    return TRUE;
  }

  /**
   * Error codes to message.
   */
  private function getError(int $code): TranslatableMarkup {
    if ($code === \ZipArchive::ER_EXISTS) {
      return $this->t('File already exists.');
    }
    if ($code === \ZipArchive::ER_INCONS) {
      return $this->t('Zip archive inconsistent.');
    }
    if ($code === \ZipArchive::ER_INVAL) {
      return $this->t('Invalid argument.');
    }
    if ($code === \ZipArchive::ER_MEMORY) {
      return $this->t('Malloc failure.');
    }
    if ($code === \ZipArchive::ER_NOENT) {
      return $this->t('No such file.');
    }
    if ($code === \ZipArchive::ER_NOZIP) {
      return $this->t('Not a zip archive.');
    }
    if ($code === \ZipArchive::ER_OPEN) {
      return $this->t("Can't open file.");
    }
    if ($code === \ZipArchive::ER_READ) {
      return $this->t('Read error.');
    }
    if ($code === \ZipArchive::ER_SEEK) {
      return $this->t('Seek error.');
    }

    return $this->t('Unknown error.');
  }

}
