<?php

namespace Drupal\castorcito;

use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Component\Utility\Html;
use Drupal\file\Entity\File;

/**
 * Provides callbacks for processing files in Castorcito components.
 */
final class CastorcitoFileProcessor implements TrustedCallbackInterface {

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['process'];
  }

  /**
   * Process a form submission and update the file usage records for the entity.
   *
   * @param array $form
   *   The form data.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public static function process(&$form, FormStateInterface $form_state) {
    $entity = $form_state->getFormObject()->getEntity();

    if (!$entity->isNew()) {
      static::cleanEntityUsage($entity);
    }

    $fields = static::jsonFieldDefinitions($entity);

    foreach ($fields as $f_key => $field) {
      $field_values = $form_state->getValue($f_key);

      if (empty($field_values)) {
        continue;
      }

      foreach ($field_values as $value) {
        if (is_array($value) && isset($value['value']) && !empty($value['value'])) {
          static::processData($entity, json_decode($value['value']));
        }
      }
    }
  }

  /**
   * Deletes file usage records for a given entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to delete file usage records.
   */
  public static function cleanEntityUsage($entity) {
    $fids = \Drupal::database()->select('file_usage', 'fu')
      ->fields('fu', ['fid'])
      ->condition('module', 'castorcito')
      ->condition('type', $entity->getEntityTypeId())
      ->condition('id', $entity->id())
      ->execute()
      ->fetchCol();

    if (!empty($fids)) {
      $file_usage = \Drupal::service('file.usage');
      $files = File::loadMultiple($fids);

      foreach ($files as $file) {
        $file_usage->delete($file, 'castorcito', $entity->getEntityTypeId(), $entity->id(), 0);
      }
    }
  }

  /**
   * Retrieves field definitions for a given entity that are of type json.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to retrieve field definitions.
   *
   * @return array
   *   An array of field definitions which are of type json.
   */
  public static function jsonFieldDefinitions($entity) {
    $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());

    $field_types = ['json', 'json_native', 'json_native_binary'];
    $fields = [];

    foreach ($field_definitions as $field_name => $field_definition) {
      if (!empty($field_definition->getTargetBundle()) && in_array($field_definition->getType(), $field_types)) {
        $fields[$field_name] = $field_name;
      }
    }

    return $fields;
  }

  /**
   * Process data from a given Castorcito component field.
   *
   * Recursively processes data from a given Castorcito component field,
   * extracting file UUIDs and processing them accordingly.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to process the data.
   * @param object $data
   *   The data to be processed.
   */
  private static function processData($entity, $data) {
    $entity_repository = \Drupal::service('entity.repository');

    foreach ($data->fields as $cfield) {
      if ($cfield->type === 'container') {
        foreach ($cfield->items as $item) {
          static::processData($entity, $item);
        }
      }

      if ($cfield->type === 'advanced_container') {
        foreach ($cfield->items as $item) {
          static::processData($entity, $item->head);
          static::processData($entity, $item->body);
        }
      }

      if ($cfield->type === 'formatted_text') {
        $uuids = static::extractFilesUuid($cfield->value);

        if (empty($uuids)) {
          continue;
        }

        try {
          foreach ($uuids as $uuid) {
            if ($file = $entity_repository->loadEntityByUuid('file', $uuid)) {
              static::processFile($file, $entity);
            }
          }
        }
        catch (EntityStorageException $e) {
          \Drupal::logger('castorcito')->warning($e->getMessage());
        }
      }

      if ($cfield->type === 'image') {
        if (empty($cfield->fid)) {
          continue;
        }

        try {
          if ($file = File::load($cfield->fid)) {
            static::processFile($file, $entity);
          }
        }
        catch (EntityStorageException $e) {
          \Drupal::logger('castorcito')->warning($e->getMessage());
        }
      }
    }
  }

  /**
   * Process a file and add it to the file usage service.
   *
   * @param \Drupal\file\FileInterface $file
   *   The file to process.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity that the file is being added to.
   */
  private static function processFile($file, $entity) {
    $file_usage = \Drupal::service('file.usage');

    if ($file->isTemporary()) {
      $file->setPermanent();
      $file->save();
    }

    if (!empty($entity->id())) {
      $file_usage->add($file, 'castorcito', $entity->getEntityTypeId(), $entity->id());
    }
  }

  /**
   * Extracts the UUIDs of files from a given HTML text.
   *
   * @param string $text
   *   The HTML text to extract the UUIDs from.
   *
   * @return array
   *   An array of UUIDs.
   */
  private static function extractFilesUuid($text) {
    $dom = Html::load($text);
    $xpath = new \DOMXPath($dom);
    $uuids = [];

    foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $file) {
      $uuids[] = $file->getAttribute('data-entity-uuid');
    }

    return $uuids;
  }

}
