<?php

declare(strict_types=1);

namespace Drupal\castorcito\Plugin\rest\resource;

use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\rest\ModifiedResourceResponse;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\file\Entity\File;
use Drupal\file\Validation\FileValidatorInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\Image\ImageFactory;
use Drupal\image\Entity\ImageStyle;
use Drupal\Component\Utility\Bytes;
use Drupal\Component\Utility\Environment;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\HttpException;
use enshrined\svgSanitize\Sanitizer;

/**
 * Represents CastorcitoAjaxUploadImage records as resources.
 *
 * @RestResource (
 *   id = "castorcito_ajax_upload_image_resource",
 *   label = @Translation("Castorcito Ajax Upload Image"),
 *   uri_paths = {
 *     "create" = "/api/castorcito-ajax-upload-image"
 *   }
 * )
 */
final class CastorcitoAjaxUploadImageResource extends ResourceBase
{

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The file system service.
   *
   * @var \Drupal\Core\File\FileSystemInterface
   */
  protected $fileSystem;

  /**
   * The file validator service.
   *
   * @var \Drupal\file\FileValidatorInterface
   */
  protected $fileValidator;

  /**
   * The image factory service.
   *
   * @var \Drupal\Core\Image\ImageFactory
   */
  protected $imageFactory;

  /**
   * Constructs a new CastocitoAjaxUploadImageResource object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param array $serializer_formats
   *   The available serialization formats.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user service.
   * @param \Drupal\Core\File\FileSystemInterface $file_system
   *   The file system service.
   * @param \Drupal\file\FileValidatorInterface $file_validator
   *   The file validator service.
   * @param \Drupal\Core\Image\ImageFactory $image_factory
   *   The image factory service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    array $serializer_formats,
    LoggerInterface $logger,
    AccountProxyInterface $current_user,
    FileSystemInterface $file_system,
    FileValidatorInterface $file_validator,
    ImageFactory $image_factory,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
    $this->currentUser = $current_user;
    $this->fileSystem = $file_system;
    $this->fileValidator = $file_validator;
    $this->imageFactory = $image_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self
  {
    return new self(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->getParameter('serializer.formats'),
      $container->get('logger.factory')->get('rest'),
      $container->get('current_user'),
      $container->get('file_system'),
      $container->get('file.validator'),
      $container->get('image.factory')
    );
  }

  /**
   * Handles POST requests to upload and save an image file.
   *
   * Processes a base64-encoded image, sanitizes SVG files for security,
   * validates the image against configured constraints, and creates
   * a permanent file entity.
   *
   * @param array $data
   *   The request data containing:
   *   - image: Base64-encoded image data.
   *   - name: The filename for the uploaded image.
   *   - size: The file size in bytes.
   *   - settings_image: Image validation settings including extensions,
   *     max_size, max_dimensions, directory and uri_scheme.
   *
   * @return \Drupal\rest\ModifiedResourceResponse
   *   A response containing the file data on success (201) or
   *   validation errors on failure (400).
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   *   Thrown when no file is provided (400), SVG is invalid (400),
   *   or file saving fails (500).
   */
  public function post(array $data): ModifiedResourceResponse
  {
    if (empty($data['image'])) {
      throw new HttpException(400, 'No file provided.');
    }

    $image = base64_decode($data['image']);

    // Sanitize SVG files to prevent XSS and other security vulnerabilities.
    $finfo = new \finfo(FILEINFO_MIME_TYPE);
    $mime_type = $finfo->buffer($image);
    $is_svg = ($mime_type === 'image/svg+xml');
    if ($is_svg) {
      $sanitizer = new Sanitizer();
      $sanitizer->removeRemoteReferences(TRUE);
      $image = $sanitizer->sanitize($image);
      if ($image === FALSE) {
        throw new HttpException(400, 'Invalid or malicious SVG file.');
      }
    }

    // Get extensions supported by the image toolkit and add SVG manually.
    $supported_extensions = $this->imageFactory->getSupportedExtensions();
    $supported_extensions[] = 'svg';

    // Intersect configured extensions with supported ones for validation.
    $config_extensions = \explode(',', \str_replace(' ', '', $data['settings_image']['extensions']));
    $allowed_extensions = \array_intersect($config_extensions, $supported_extensions);
    $data['settings_image']['extensions'] = \implode(' ', $allowed_extensions);
    $data['settings_image']['is_svg'] = $is_svg;

    // Prepare the destination directory and save the image data.
    $uri_scheme = $data['settings_image']['uri_scheme'] ?? 'public';
    $directory = $uri_scheme . '://' . $data['settings_image']['directory'];
    $this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
    $destination = $directory . '/' . $data['name'];
    $replace = class_exists('Drupal\Core\File\FileExists') ? FileExists::Rename : $this->fileSystem::EXISTS_RENAME;
    $destination_uri = $this->fileSystem->saveData($image, $destination, $replace);

    // Create the file entity.
    $file = File::create([
      'uri' => $destination_uri,
      'uid' => $this->currentUser->id(),
      'filesize' => $data['size'],
    ]);

    try {
      $response = $this->validateImage($file, $data['settings_image']);
      if (!empty($response['errors'])) {
        // Delete the physical file from filesystem (entity is not saved yet).
        $this->fileSystem->delete($file->getFileUri());
        return new ModifiedResourceResponse($response, 400);
      }

      $file->save();

      $img = $this->imageFactory->get($file->getFileUri());

      // Use original URI for SVG, thumbnail style for raster images.
      if ($is_svg) {
        $thumbnail_uri = $file->getFileUri();
      }
      else {
        $image_style = ImageStyle::load('thumbnail');
        $itok = $image_style->getPathToken($file->getFileUri());
        $uri = $image_style->buildUri($file->getFileUri());

        if (!file_exists($uri)) {
          $image_style->createDerivative($file->getFileUri(), $uri);
        }

        $thumbnail_uri = $uri . '?itok=' . $itok;
      }

      $size = ByteSizeMarkup::create($file->getSize());
      $response['data'] = [
        'fid' => $file->id(),
        'name' => $file->getFilename(),
        'size' => $size,
        'value' => $file->getFileUri(),
        'thumbnail' => $thumbnail_uri,
      ];
      // Add image dimensions if the file is a valid raster image.
      if ($img->isValid()) {
        $response['data']['width'] = $img->getWidth();
        $response['data']['height'] = $img->getHeight();
      }
    } catch (\Exception $e) {
      throw new HttpException(500, 'Error saving file: ' . $e->getMessage());
    }

    return new ModifiedResourceResponse($response, 201);
  }

  /**
   * Validates an uploaded image file against configured constraints.
   *
   * Applies file validators for name length, size limit, and extension.
   * For non-SVG images, also validates dimensions and image format.
   *
   * @param \Drupal\file\Entity\File $file
   *   The file entity to validate.
   * @param array $settings
   *   Validation settings containing:
   *   - extensions: Allowed file extensions (space-separated).
   *   - max_size: Maximum file size (e.g., '2MB').
   *   - max_dimensions: Array with 'width' and 'height' keys.
   *   - is_svg: Boolean indicating if the file is an SVG.
   *
   * @return array
   *   An array with 'errors' and 'status' keys containing
   *   validation messages.
   */
  private function validateImage(File $file, array $settings): array
  {
    $max_size = $settings['max_size'] ? Bytes::toNumber($settings['max_size']) : Environment::getUploadMaxSize();

    $max_dimensions = 0;
    if (!empty($settings['max_dimensions']['width']) || !empty($settings['max_dimensions']['height'])) {
      $max_dimensions = $settings['max_dimensions']['width'] . 'x' . $settings['max_dimensions']['height'];
    }

    $validators = [
      'FileNameLength' => [],
      'FileSizeLimit' => [
        'fileLimit' => $max_size,
      ],
      'FileExtension' => [
        'extensions' => $settings['extensions'],
      ],
    ];

    // Skip image-specific validators for SVG (not supported by GD toolkit).
    if (!$settings['is_svg']) {
      $validators['FileImageDimensions'] = [
        'maxDimensions' => $max_dimensions,
      ];
      $validators['FileIsImage'] = [];
    }

    $violations = $this->fileValidator->validate($file, $validators);
    $messages['errors'] = [];
    $messages['status'] = [];
    if ($violations->count() > 0) {
      foreach ($violations as $violation) {
        $messages['errors'][] = $violation->getMessage()->render();
      }
    } else {
      $status_messages = $this->messenger()->messagesByType(MessengerInterface::TYPE_STATUS);
      foreach ($status_messages as $message) {
        $messages['status'][] = $message->__toString();
        $this->messenger()->deleteAll();
      }
    }

    return $messages;
  }

}
