<?php

use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\image\Entity\ImageStyle;
use Drupal\Core\Archiver\Zip;
use Drupal\Core\Archiver\ArchiverException;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Process\Process;
use Drupal\dfg_3dviewer\ModelFormatManager;

function dfg_3dviewer_config(): array {
  static $cfg;

  if ($cfg === NULL) {
    $config = \Drupal::config('dfg_3dviewer.settings');

	$main_url = trim((string) (
		$config->get('dfg_3dviewer_main_url')
		?? $config->get('main_url')
	)) ?: 'https://3d-repository.hs-mainz.de';

	$json_export_base_url = trim((string) (
		$config->get('dfg_3dviewer_json_export_base_url')
		?? $config->get('json_export_base_url')
	)) ?: $main_url;

	$entity_bundle = trim((string) (
		$config->get('dfg_3dviewer_entitybundle')
		?? $config->get('entitybundle')
	)) ?: 'bd3d7baa74856d141bcff7b4193fa128';

	$viewer_file_upload = trim((string) (
		$config->get('dfg_3dviewer_viewer_file_upload')
		?? $config->get('viewer_file_upload')
	)) ?: 'fad29437cb2a561b91b26aca5dbb7c42';

	$viewer_file_name = trim((string) (
		$config->get('dfg_3dviewer_viewer_file_name')
		?? $config->get('viewer_file_name')
	)) ?: 'fb76901eb219495fee0512b5cdfdaa18';

	$api_3d_file_field = trim((string) (
		$config->get('dfg_3dviewer_api_3d_file_field')
		?? $config->get('api_3d_file_field')
	));

	$image_generation = trim((string) (
		$config->get('dfg_3dviewer_image_generation')
		?? $config->get('image_generation')
	)) ?: 'f605dc6b727a1099b9e52b3ccbdf5673';

	$field_df = trim((string) (
		$config->get('dfg_3dviewer_field_df')
		?? $config->get('field_df')
	)) ?: 'field_df';

	$export_viewer = trim((string) (
		$config->get('dfg_3dviewer_export_viewer')
		?? $config->get('export_viewer')
	)) ?: 'export_viewer';

	$export_viewer_url = trim((string) (
		$config->get('dfg_3dviewer_export_viewer_url')
		?? $config->get('export_viewer_url')
	)) ?: '';

	$lightweight = filter_var(
	$config->get('dfg_3dviewer_lightweight')
		?? $config->get('lightweight')
		?? false,
	FILTER_VALIDATE_BOOLEAN
	);

	$gallery_image_class = trim((string) (
		$config->get('dfg_3dviewer_gallery_image_class')
		?? $config->get('gallery_image_class')
	)) ?: 'field--name-fd6a974b7120d422c7b21b5f1f2315d9';

    $cfg = [
      'main_url' => $main_url,
      'json_export_base_url' => $json_export_base_url,
      'entity_bundle' => $entity_bundle,
      'viewer_file_upload' => $viewer_file_upload,
      'viewer_file_name' => $viewer_file_name,
      'api_3d_file_field' => $api_3d_file_field,
      'image_generation' => $image_generation,
      'field_df' => $field_df,
      'lightweight' => $lightweight,
      'gallery_image_class' => $gallery_image_class,
	  'export_viewer' => $export_viewer,
	  'export_viewer_url' => $export_viewer_url,
    ];
  }
  return $cfg;
}

function preload() {
	$cfg = dfg_3dviewer_config();
	if (isset($cfg)) {
		foreach ($cfg as $key => $value) {
			if (!defined($key)) {
				define($key, $value);
			}
		}
	}
	else {
		\Drupal::messenger()->addMessage("Cant load settings file", 'warning');
	}
}

function dfg_3dviewer_init_constants(): void {
  static $done = false;
  if ($done) {
    return;
  }

  $cfg = dfg_3dviewer_config();

  foreach ($cfg as $key => $value) {
    $const = 'DFG_3DVIEWER_' . strtoupper($key);

    if (!defined($const)) {
      define($const, $value);
    }
  }

  $done = true;
}

/**
 * Returns the queue runner log path.
 */
function dfg_3dviewer_queue_runner_log_path(): string {
  return '/opt/drupal/dfg3dworker.log';
}

/**
 * Returns the persistent worker PID file path.
 */
function dfg_3dviewer_queue_runner_pid_path(): string {
  return '/opt/drupal/dfg3dworker.pid';
}

/**
 * Checks whether a persistent queue worker loop is already running.
 */
function dfg_3dviewer_has_persistent_convert_worker(): bool {
  $pid_file = dfg_3dviewer_queue_runner_pid_path();
  if (!is_file($pid_file) || !is_readable($pid_file)) {
    return FALSE;
  }

  $pid = trim((string) @file_get_contents($pid_file));
  if ($pid === '' || !ctype_digit($pid)) {
    @unlink($pid_file);
    return FALSE;
  }

  $pid_int = (int) $pid;
  if ($pid_int <= 0) {
    @unlink($pid_file);
    return FALSE;
  }

  $is_running = FALSE;
  if (function_exists('posix_kill')) {
    $is_running = @posix_kill($pid_int, 0);
  }
  else {
    $is_running = is_dir('/proc/' . $pid);
  }

  if (!$is_running) {
    @unlink($pid_file);
    return FALSE;
  }

  return TRUE;
}

/**
 * Builds a detached drush queue runner command with line timestamps.
 */
function dfg_3dviewer_build_timestamped_queue_runner_command(array $arguments, string $runner_log): string {
  $escaped = array_map('escapeshellarg', $arguments);
  $drush_command = implode(' ', $escaped);
  $awk_script = "awk '{ print strftime(\"%Y-%m-%d %H:%M:%S\"), \$0; fflush(); }'";

  return sprintf(
    '{ %s 2>&1 | %s >> %s; } &',
    $drush_command,
    $awk_script,
    escapeshellarg($runner_log)
  );
}

/**
 * Starts a background queue runner in a non-blocking way.
 */
function dfg_3dviewer_kick_convert_worker(): void {
  \Drupal::logger('dfg_3dviewer')->notice('dfg_3dviewer_kick_convert_worker() invoked.');

  if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) {
    return;
  }

  $lock = \Drupal::lock();
  $lock_name = 'dfg_3dviewer_convert_runner_kick';
  if (!$lock->acquire($lock_name, 30.0)) {
    \Drupal::logger('dfg_3dviewer')->notice('Skip queue runner kick: lock "@lock" is already held.', ['@lock' => $lock_name]);
    return;
  }

  try {
    $queue = \Drupal::service('queue')->get('dfg_3dviewer_convert');
    $queue_size = method_exists($queue, 'numberOfItems') ? (int) $queue->numberOfItems() : -1;
    \Drupal::logger('dfg_3dviewer')->notice(
      'Queue runner kick requested. Queue size before start: @size',
      ['@size' => (string) $queue_size]
    );

    if (dfg_3dviewer_has_persistent_convert_worker()) {
      \Drupal::logger('dfg_3dviewer')->notice(
        'Skip queue runner kick: persistent worker loop is already active (pid file: @pid_file).',
        ['@pid_file' => dfg_3dviewer_queue_runner_pid_path()]
      );
      return;
    }

    $drush = dfg_3dviewer_find_drush_binary();
    if ($drush === '') {
      \Drupal::logger('dfg_3dviewer')->warning('Cannot kick queue runner: drush binary not found.');
      return;
    }

    $arguments = [$drush];
    $site_uri = '';
    $site_uri_candidates = [];
    try {
      $request = \Drupal::service('request_stack')->getCurrentRequest();
      if ($request) {
        $site_uri_candidates[] = $request->getSchemeAndHttpHost();
      }
    }
    catch (\Throwable $e) {
      // Fall back to the configured canonical URL below.
    }
    $site_uri_candidates[] = trim((string) (dfg_3dviewer_config()['main_url'] ?? ''));

    foreach ($site_uri_candidates as $candidate) {
      $candidate = trim((string) $candidate);
      $candidate_parts = parse_url($candidate);
      $candidate_host = is_array($candidate_parts) ? (string) ($candidate_parts['host'] ?? '') : '';
      if (is_array($candidate_parts)
        && !empty($candidate_parts['scheme'])
        && $candidate_host !== ''
        && strpos($candidate_host, '_') === FALSE
        && strtolower($candidate_host) !== 'default') {
        $site_uri = rtrim($candidate, '/');
        break;
      }
    }

    if ($site_uri !== '') {
      $arguments[] = '--uri=' . $site_uri;
    }
    else {
      \Drupal::logger('dfg_3dviewer')->warning(
        'Queue runner starts without --uri because neither the current request nor configured main URL is a safe absolute URL.'
      );
    }
    $arguments = array_merge($arguments, [
      'queue:run',
      'dfg_3dviewer_convert',
      '--time-limit=30',
    ]);

    // Using Process::start() here is unreliable because the Process object
    // is destroyed at the end of this function and may terminate the child.
    // Spawn a detached shell command instead.
    $runner_log = dfg_3dviewer_queue_runner_log_path();
    $command = dfg_3dviewer_build_timestamped_queue_runner_command($arguments, $runner_log);

    // Drush may fail in web/PHP contexts when HOME is missing.
    $home = (string) getenv('HOME');
    if ($home === '') {
      $home = '/tmp';
    }
    $composer_home = (string) getenv('COMPOSER_HOME');
    if ($composer_home === '') {
      $composer_home = rtrim($home, '/') . '/.composer';
    }
    $xdg_config_home = (string) getenv('XDG_CONFIG_HOME');
    if ($xdg_config_home === '') {
      $xdg_config_home = rtrim($home, '/') . '/.config';
    }

    $env = [
      'HOME' => $home,
      'COMPOSER_HOME' => $composer_home,
      'XDG_CONFIG_HOME' => $xdg_config_home,
    ];

    $process = Process::fromShellCommandline($command, DRUPAL_ROOT, $env);
    $process->disableOutput();
    $process->run();

    \Drupal::logger('dfg_3dviewer')->notice(
      'Triggered background queue runner for dfg_3dviewer_convert (log: @log).',
      ['@log' => $runner_log]
    );
  }
  catch (\Throwable $e) {
    \Drupal::logger('dfg_3dviewer')->warning(
      'Failed to trigger background queue runner: @msg',
      ['@msg' => $e->getMessage()]
    );
  }
  finally {
    $lock->release($lock_name);
  }
}

/**
 * Finds a usable drush executable path.
 */
function dfg_3dviewer_find_drush_binary(): string {
  $candidates = [
    DRUPAL_ROOT . '/vendor/bin/drush',
    DRUPAL_ROOT . '/vendor/bin/drush.bat',
    DRUPAL_ROOT . '/../vendor/bin/drush',
    DRUPAL_ROOT . '/../vendor/bin/drush.bat',
  ];

  foreach ($candidates as $candidate) {
    if (is_file($candidate)) {
      return $candidate;
    }
  }

  return '';
}

/**
 * Tries to resolve file ID from entity field values.
 */
function dfg_3dviewer_extract_file_id(array $file_info): ?int {
  $first = $file_info[0] ?? NULL;
  if (!is_array($first)) {
    return NULL;
  }

  foreach (['target_id', 'fid', 'id', 'value', 'original_target_id', 'uri'] as $key) {
    if (!array_key_exists($key, $first)) {
      continue;
    }
    $candidate = (string) $first[$key];
    if ($candidate !== '' && ctype_digit($candidate)) {
      return (int) $candidate;
    }
    if ($candidate !== '') {
      $mapped = dfg_3dviewer_file_id_from_location($candidate);
      if ($mapped !== NULL) {
        return $mapped;
      }
    }
  }

  return NULL;
}

/**
 * Maps URL/public path to a file entity ID when possible.
 */
function dfg_3dviewer_file_id_from_location(string $location): ?int {
  $uri = dfg_3dviewer_location_to_public_uri($location);
  if ($uri === NULL) {
    return NULL;
  }

  try {
    $storage = \Drupal::entityTypeManager()->getStorage('file');
    $existing = $storage->loadByProperties(['uri' => $uri]);
    if (!empty($existing)) {
      $file = reset($existing);
      return $file ? (int) $file->id() : NULL;
    }
  }
  catch (\Throwable $e) {
    return NULL;
  }

  return NULL;
}

/**
 * Converts full URL or public path into public:// URI.
 */
function dfg_3dviewer_location_to_public_uri(string $location): ?string {
  $location = trim($location);
  if ($location === '') {
    return NULL;
  }

  if (str_starts_with($location, 'public://')) {
    return $location;
  }

  if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $location)) {
    $path = parse_url($location, PHP_URL_PATH);
    if (!is_string($path) || $path === '') {
      return NULL;
    }
    $location = $path;
  }

  $location = ltrim($location, '/');
  if (str_starts_with($location, 'sites/default/files/')) {
    return 'public://' . substr($location, strlen('sites/default/files/'));
  }

  return NULL;
}

/**
 * Tries to detect a non-empty file reference field on entity.
 *
 * @return array{field_name:string,file_id:int}|null
 */
function dfg_3dviewer_detect_file_field_fallback(Drupal\Core\Entity\EntityInterface $entity): ?array {
  if (!method_exists($entity, 'getFields')) {
    return NULL;
  }

  foreach ($entity->getFields() as $field_name => $field_item_list) {
    try {
      if ($field_item_list->isEmpty()) {
        continue;
      }

      $definition = $field_item_list->getFieldDefinition();
      $storage_definition = $definition ? $definition->getFieldStorageDefinition() : NULL;
      if (!$storage_definition) {
        continue;
      }

      $field_type = (string) $storage_definition->getType();
      if (!in_array($field_type, ['entity_reference', 'file'], TRUE)) {
        continue;
      }

      $settings = $definition->getSettings();
      $target_type = (string) ($settings['target_type'] ?? '');
      if ($target_type !== '' && $target_type !== 'file') {
        continue;
      }

      $file_info = $field_item_list->getValue();
      $file_id = dfg_3dviewer_extract_file_id($file_info);
      if ($file_id !== NULL) {
        return [
          'field_name' => (string) $field_name,
          'file_id' => $file_id,
        ];
      }
    }
    catch (\Throwable $e) {
      continue;
    }
  }

  return NULL;
}

/**
 * Tries to resolve file ID from a field on base/untranslated/translated entity.
 */
function dfg_3dviewer_extract_file_id_from_entity_field(
  Drupal\Core\Entity\EntityInterface $entity,
  string $field_name
): ?int {
  if ($field_name === '' || !method_exists($entity, 'hasField') || !$entity->hasField($field_name)) {
    return NULL;
  }

  $file_info = $entity->{$field_name}->getValue();
  $file_id = dfg_3dviewer_extract_file_id($file_info);
  if ($file_id !== NULL) {
    return $file_id;
  }

  if (method_exists($entity, 'getUntranslated')) {
    try {
      $untranslated = $entity->getUntranslated();
      if ($untranslated && method_exists($untranslated, 'hasField') && $untranslated->hasField($field_name)) {
        $file_info = $untranslated->{$field_name}->getValue();
        $file_id = dfg_3dviewer_extract_file_id($file_info);
        if ($file_id !== NULL) {
          return $file_id;
        }
      }
    }
    catch (\Throwable $e) {
      // Ignore and continue.
    }
  }

  if (method_exists($entity, 'getTranslationLanguages')
    && method_exists($entity, 'hasTranslation')
    && method_exists($entity, 'getTranslation')) {
    try {
      foreach ($entity->getTranslationLanguages() as $langcode => $language) {
        if (!method_exists($entity, 'hasTranslation') || !$entity->hasTranslation($langcode)) {
          continue;
        }
        $translation = $entity->getTranslation($langcode);
        if (!method_exists($translation, 'hasField') || !$translation->hasField($field_name)) {
          continue;
        }
        $file_info = $translation->{$field_name}->getValue();
        $file_id = dfg_3dviewer_extract_file_id($file_info);
        if ($file_id !== NULL) {
          return $file_id;
        }
      }
    }
    catch (\Throwable $e) {
      // Ignore and return NULL below.
    }
  }

  if (!empty($entity->original)
    && method_exists($entity->original, 'hasField')
    && $entity->original->hasField($field_name)) {
    try {
      $file_info = $entity->original->{$field_name}->getValue();
      $file_id = dfg_3dviewer_extract_file_id($file_info);
      if ($file_id !== NULL) {
        return $file_id;
      }
    }
    catch (\Throwable $e) {
      // Ignore and return NULL below.
    }
  }

  return NULL;
}

/**
 * Resolves current file reference for entity from configured field or fallback.
 *
 * @return array{field_name:string,file_id:int,configured_field:string,used_fallback:bool,configured_raw:array}
 */
function dfg_3dviewer_resolve_current_file_reference(
  Drupal\Core\Entity\EntityInterface $entity,
  string $configured_field
): ?array {
  $configured_raw = [];
  if ($configured_field !== ''
    && method_exists($entity, 'hasField')
    && $entity->hasField($configured_field)) {
    $file_info = $entity->{$configured_field}->getValue();
    $configured_raw = is_array($file_info[0] ?? NULL) ? $file_info[0] : [];
    $file_id = dfg_3dviewer_extract_file_id_from_entity_field($entity, $configured_field);
    if ($file_id !== NULL) {
      return [
        'field_name' => $configured_field,
        'file_id' => $file_id,
        'configured_field' => $configured_field,
        'used_fallback' => FALSE,
        'configured_raw' => $configured_raw,
      ];
    }
  }

  $fallback = dfg_3dviewer_detect_file_field_fallback($entity);
  if ($fallback !== NULL) {
    return [
      'field_name' => (string) $fallback['field_name'],
      'file_id' => (int) $fallback['file_id'],
      'configured_field' => $configured_field,
      'used_fallback' => TRUE,
      'configured_raw' => $configured_raw,
    ];
  }

  return NULL;
}

/**
 * Resolves previous file ID from original entity by selected field.
 */
function dfg_3dviewer_resolve_previous_file_id(
  Drupal\Core\Entity\EntityInterface $entity,
  string $field_name
): ?int {
  if (empty($entity->original)
    || !method_exists($entity->original, 'hasField')
    || !$entity->original->hasField($field_name)) {
    return NULL;
  }

  $old = $entity->original->{$field_name}->getValue();
  return dfg_3dviewer_extract_file_id($old);
}

/**
 * Stores last resolved file ID per entity for fallback re-queueing.
 */
function dfg_3dviewer_store_last_file_id(string $entity_type, string $entity_id, int $file_id): void {
  if ($entity_type === '' || $entity_id === '' || $file_id <= 0) {
    return;
  }
  $key = 'dfg_3dviewer.last_file_id.' . $entity_type . '.' . $entity_id;
  \Drupal::state()->set($key, $file_id);
}

/**
 * Loads last resolved file ID per entity.
 */
function dfg_3dviewer_load_last_file_id(string $entity_type, string $entity_id): ?int {
  if ($entity_type === '' || $entity_id === '') {
    return NULL;
  }
  $key = 'dfg_3dviewer.last_file_id.' . $entity_type . '.' . $entity_id;
  $value = \Drupal::state()->get($key);
  if ($value === NULL) {
    return NULL;
  }
  $candidate = (string) $value;
  return ($candidate !== '' && ctype_digit($candidate)) ? (int) $candidate : NULL;
}

/**
 * Implements hook_help().
 */
function dfg_3dviewer_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.wisski_iip_image':
      return '<p>' . t('This is the WissKI module for the integration ' .
        'of IIP (https://iipimage.sourceforge.io/).') . '</p>';
  }
}

function in_arrayi($needle, $haystack)
{
	return in_array(strtolower($needle), array_map('strtolower', $haystack));
}

function array_searchi($needle, $haystack)
{
	return array_search(strtolower($needle), array_map('strtolower', $haystack));
}

function url_exists(string $url): bool {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_NOBODY => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_RETURNTRANSFER => true,
    ]);

    curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return $code >= 200 && $code < 400;
}

function automatic_helper (&$autoPath, $prefix, $filename, $value) {
	$autoPath = $prefix . "/" . $filename . "." . $value;
	if(!file_exists($autoPath)) {
		$autoPath = '';
	}				
	else {
		return;
	}
}

/**
* Implements hook_file_validate().
 */
function dfg_3dviewer_hook_file_validate(Drupal\file\FileInterface $file) {

    $errors = [];
	if (!is_array($filename)) {
		$filename = $file->getFilename();
		if (!$filename) {
			$errors[] = t("The file's name is empty. Give a name to the file.");
		}
		if (strlen($filename) > 255) {
			$errors[] = t("The file's name exceeds the 255 characters limit. Rename the file and try again.");
		}
		if (preg_match('/\s/', $filename)) {
			$errors[] = t("Whitespaces are not allowed in filenames");
		}		
	}

    return $errors;

}

/**
* Implements hook_entity_presave().
 */
function dfg_3dviewer_entity_presave(Drupal\Core\Entity\EntityInterface $entity) {
	if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) {
		return;
	}

	$cfg = dfg_3dviewer_config();
	if (empty($cfg) || $entity->bundle() !== $cfg['entity_bundle']) {
		return;
	}

	\Drupal::logger('dfg_3dviewer')->notice(
		'entity_presave matched target entity: type=@type bundle=@bundle id=@id',
		[
			'@type' => $entity->getEntityTypeId(),
			'@bundle' => (string) $entity->bundle(),
			'@id' => (string) ($entity->id() ?? ''),
		]
	);

	$configured_raw_first = [];
	if (method_exists($entity, 'hasField') && $entity->hasField((string) $cfg['viewer_file_upload'])) {
		$configured_values = $entity->{(string) $cfg['viewer_file_upload']}->getValue();
		$configured_raw_first = is_array($configured_values[0] ?? NULL) ? $configured_values[0] : [];
	}

	$resolved = dfg_3dviewer_resolve_current_file_reference($entity, (string) $cfg['viewer_file_upload']);
	if ($resolved === NULL) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_presave skip: upload field "@field" has no file ID in first item on entity @entity_id. Raw value: @raw',
			[
				'@field' => $cfg['viewer_file_upload'],
				'@entity_id' => (string) ($entity->id() ?? ''),
				'@raw' => json_encode($configured_raw_first, JSON_UNESCAPED_SLASHES),
			]
		);
		return;
	}
	$file_id = (int) $resolved['file_id'];
	$source_field = (string) $resolved['field_name'];
	if (!empty($resolved['used_fallback'])) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_presave fallback: configured upload field "@field" is empty; using "@fallback_field" with file_id=@file_id for entity @entity_id. Configured raw: @raw',
			[
				'@field' => (string) $resolved['configured_field'],
				'@fallback_field' => $source_field,
				'@file_id' => (string) $file_id,
				'@entity_id' => (string) ($entity->id() ?? ''),
				'@raw' => json_encode($resolved['configured_raw'], JSON_UNESCAPED_SLASHES),
			]
		);
	}
	$entity_id = $entity->id();

	$uploaded_file = \Drupal\file\Entity\File::load($file_id);
	if ($uploaded_file) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'Upload source field "@field" on entity @entity_id resolved to file_id=@file_id, filename="@filename", uri="@uri".',
			[
				'@field' => $source_field,
				'@entity_id' => (string) ($entity_id ?? ''),
				'@file_id' => $file_id,
				'@filename' => $uploaded_file->getFilename(),
				'@uri' => $uploaded_file->getFileUri(),
			]
		);
	}
	else {
		\Drupal::logger('dfg_3dviewer')->warning(
			'Upload source field "@field" on entity @entity_id points to missing file_id=@file_id.',
			[
				'@field' => $source_field,
				'@entity_id' => (string) ($entity_id ?? ''),
				'@file_id' => $file_id,
			]
		);
	}

	if (empty($entity_id) && $entity->hasField('eid')) {
		$entity_id = $entity->get('eid')->value;
	}

	\Drupal::logger('dfg_3dviewer')->notice(
		'entity_presave force requeue on save: entity @entity_id file @file_id.',
		[
			'@entity_id' => (string) ($entity_id ?? $entity->id() ?? ''),
			'@file_id' => (string) $file_id,
		]
	);

	if ($entity->hasField('field_processing_progress')) {
		$entity->set('field_processing_progress', 0);
	}
	if ($entity->hasField('field_processing_status')) {
		$entity->set('field_processing_status', 'queued');
	}
	if ($entity->hasField('field_processing_message')) {
		$entity->set('field_processing_message', 'Queued for conversion');
	}
	$source_filename = $uploaded_file ? $uploaded_file->getFilename() : '';
	$source_uri = $uploaded_file ? $uploaded_file->getFileUri() : '';

	if (empty($entity_id)) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'Entity ID is not available in presave; enqueue deferred to entity_insert for file @file_id.',
			[
				'@file_id' => $file_id,
			]
		);
		return;
	}

	try {
		\Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([
			'entity_type' => $entity->getEntityTypeId(),
			'entity_id' => $entity_id,
			'file_id' => $file_id,
			'source_filename' => $source_filename,
			'source_uri' => $source_uri,
		]);
		dfg_3dviewer_store_last_file_id((string) $entity->getEntityTypeId(), (string) $entity_id, (int) $file_id);
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_presave enqueue ok: entity @entity_id file @file_id.',
			[
				'@entity_id' => (string) $entity_id,
				'@file_id' => (string) $file_id,
			]
		);
		dfg_3dviewer_kick_convert_worker();
		\Drupal::logger('dfg_3dviewer')->notice(
			'Queued conversion for entity @entity_id and file @file_id.',
			[
				'@entity_id' => $entity_id,
				'@file_id' => $file_id,
			]
		);
	}
	catch (\Throwable $e) {
		\Drupal::logger('dfg_3dviewer')->error(
			'Failed to enqueue conversion in presave for entity @entity_id and file @file_id: @msg',
			[
				'@entity_id' => $entity_id,
				'@file_id' => $file_id,
				'@msg' => $e->getMessage(),
			]
		);
	}

	$key = $entity->getEntityTypeId() . ':' . $entity_id . ':' . $file_id;
	$GLOBALS['dfg_3dviewer_enqueued_in_presave'][$key] = TRUE;
}

/**
 * Implements hook_entity_insert().
 */
function dfg_3dviewer_entity_insert(Drupal\Core\Entity\EntityInterface $entity) {
	if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) {
		return;
	}

	$cfg = dfg_3dviewer_config();
	if (empty($cfg) || $entity->bundle() !== $cfg['entity_bundle']) {
		return;
	}

	\Drupal::logger('dfg_3dviewer')->notice(
		'entity_insert matched target entity: type=@type bundle=@bundle id=@id',
		[
			'@type' => $entity->getEntityTypeId(),
			'@bundle' => (string) $entity->bundle(),
			'@id' => (string) ($entity->id() ?? ''),
		]
	);

	$status = $entity->hasField('field_processing_status')
		? (string) $entity->get('field_processing_status')->value
		: '';

	if ($status !== 'queued') {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_insert skip: processing status is "@status" (expected "queued") for entity @entity_id.',
			[
				'@status' => $status,
				'@entity_id' => (string) ($entity->id() ?? ''),
			]
		);
		return;
	}

	$configured_raw_first = [];
	if (method_exists($entity, 'hasField') && $entity->hasField((string) $cfg['viewer_file_upload'])) {
		$configured_values = $entity->{(string) $cfg['viewer_file_upload']}->getValue();
		$configured_raw_first = is_array($configured_values[0] ?? NULL) ? $configured_values[0] : [];
	}

	$resolved = dfg_3dviewer_resolve_current_file_reference($entity, (string) $cfg['viewer_file_upload']);
	if ($resolved === NULL) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_insert skip: upload field "@field" has no file ID in first item on entity @entity_id. Raw value: @raw',
			[
				'@field' => $cfg['viewer_file_upload'],
				'@entity_id' => (string) ($entity->id() ?? ''),
				'@raw' => json_encode($configured_raw_first, JSON_UNESCAPED_SLASHES),
			]
		);
		return;
	}
	$file_id = (int) $resolved['file_id'];
	if (!empty($resolved['used_fallback'])) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_insert fallback: configured upload field "@field" is empty; using "@fallback_field" with file_id=@file_id for entity @entity_id. Configured raw: @raw',
			[
				'@field' => (string) $resolved['configured_field'],
				'@fallback_field' => (string) $resolved['field_name'],
				'@file_id' => (string) $file_id,
				'@entity_id' => (string) ($entity->id() ?? ''),
				'@raw' => json_encode($resolved['configured_raw'], JSON_UNESCAPED_SLASHES),
			]
		);
	}
	$entity_id = $entity->id();
	if (empty($entity_id) && $entity->hasField('eid')) {
		$entity_id = $entity->get('eid')->value;
	}
	if (empty($entity_id)) {
		\Drupal::logger('dfg_3dviewer')->warning('Cannot enqueue conversion on insert: missing entity ID.');
		return;
	}

	$key = $entity->getEntityTypeId() . ':' . $entity_id . ':' . $file_id;
	if (!empty($GLOBALS['dfg_3dviewer_enqueued_in_presave'][$key])) {
		return;
	}

	$uploaded_file = \Drupal\file\Entity\File::load($file_id);
	$source_filename = $uploaded_file ? $uploaded_file->getFilename() : '';
	$source_uri = $uploaded_file ? $uploaded_file->getFileUri() : '';

	try {
		\Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([
			'entity_type' => $entity->getEntityTypeId(),
			'entity_id' => $entity_id,
			'file_id' => $file_id,
			'source_filename' => $source_filename,
			'source_uri' => $source_uri,
		]);
		dfg_3dviewer_store_last_file_id((string) $entity->getEntityTypeId(), (string) $entity_id, (int) $file_id);
		dfg_3dviewer_kick_convert_worker();

		\Drupal::logger('dfg_3dviewer')->notice(
			'Queued conversion on insert for entity @entity_id and file @file_id.',
			[
				'@entity_id' => $entity_id,
				'@file_id' => $file_id,
			]
		);
	}
	catch (\Throwable $e) {
		\Drupal::logger('dfg_3dviewer')->error(
			'Failed to enqueue conversion on insert for entity @entity_id and file @file_id: @msg',
			[
				'@entity_id' => $entity_id,
				'@file_id' => $file_id,
				'@msg' => $e->getMessage(),
			]
		);
	}
}

/**
 * Implements hook_entity_update().
 */
function dfg_3dviewer_entity_update(Drupal\Core\Entity\EntityInterface $entity) {
	if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) {
		return;
	}

	$cfg = dfg_3dviewer_config();
	if (empty($cfg) || $entity->bundle() !== $cfg['entity_bundle']) {
		return;
	}

	\Drupal::logger('dfg_3dviewer')->notice(
		'entity_update matched target entity: type=@type bundle=@bundle id=@id',
		[
			'@type' => $entity->getEntityTypeId(),
			'@bundle' => (string) $entity->bundle(),
			'@id' => (string) ($entity->id() ?? ''),
		]
	);

	$configured_raw_first = [];
	if (method_exists($entity, 'hasField') && $entity->hasField((string) $cfg['viewer_file_upload'])) {
		$configured_values = $entity->{(string) $cfg['viewer_file_upload']}->getValue();
		$configured_raw_first = is_array($configured_values[0] ?? NULL) ? $configured_values[0] : [];
	}

	$resolved = dfg_3dviewer_resolve_current_file_reference($entity, (string) $cfg['viewer_file_upload']);
	if ($resolved === NULL) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_update skip: upload field "@field" has no file ID in first item on entity @entity_id. Raw value: @raw',
			[
				'@field' => $cfg['viewer_file_upload'],
				'@entity_id' => (string) ($entity->id() ?? ''),
				'@raw' => json_encode($configured_raw_first, JSON_UNESCAPED_SLASHES),
			]
		);

		$entity_id_fallback = $entity->id();
		$entity_type_fallback = $entity->getEntityTypeId();
		if (!empty($entity_id_fallback)) {
			try {
				$storage = \Drupal::entityTypeManager()->getStorage($entity_type_fallback);
				$candidates = [];
				if (!empty($entity->original)) {
					$candidates[] = $entity->original;
				}
				$loaded = $storage->load($entity_id_fallback);
				if ($loaded) {
					$candidates[] = $loaded;
				}
				if (method_exists($storage, 'loadUnchanged')) {
					$loaded_unchanged = $storage->loadUnchanged($entity_id_fallback);
					if ($loaded_unchanged) {
						$candidates[] = $loaded_unchanged;
					}
				}

				foreach ($candidates as $candidate_entity) {
					$stored_resolved = dfg_3dviewer_resolve_current_file_reference($candidate_entity, (string) $cfg['viewer_file_upload']);
					if ($stored_resolved === NULL) {
						continue;
					}

					$stored_file_id = (int) $stored_resolved['file_id'];
					$stored_file = \Drupal\file\Entity\File::load($stored_file_id);
					$stored_source_filename = $stored_file ? $stored_file->getFilename() : '';
					$stored_source_uri = $stored_file ? $stored_file->getFileUri() : '';

					\Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([
						'entity_type' => $entity_type_fallback,
						'entity_id' => $entity_id_fallback,
						'file_id' => $stored_file_id,
						'source_filename' => $stored_source_filename,
						'source_uri' => $stored_source_uri,
					]);
					dfg_3dviewer_store_last_file_id((string) $entity_type_fallback, (string) $entity_id_fallback, (int) $stored_file_id);

					\Drupal::logger('dfg_3dviewer')->notice(
						'entity_update fallback enqueue from candidate entity: entity @entity_id file @file_id.',
						[
							'@entity_id' => (string) $entity_id_fallback,
							'@file_id' => (string) $stored_file_id,
						]
					);
					dfg_3dviewer_kick_convert_worker();
					return;
				}
			}
			catch (\Throwable $e) {
				\Drupal::logger('dfg_3dviewer')->warning(
					'entity_update stored fallback enqueue failed for entity @entity_id: @msg',
					[
						'@entity_id' => (string) $entity_id_fallback,
						'@msg' => $e->getMessage(),
					]
				);
			}
		}

		if (!empty($entity_id_fallback)) {
			$last_file_id = dfg_3dviewer_load_last_file_id((string) $entity_type_fallback, (string) $entity_id_fallback);
			if ($last_file_id !== NULL) {
				try {
					$last_file = \Drupal\file\Entity\File::load($last_file_id);
					$last_source_filename = $last_file ? $last_file->getFilename() : '';
					$last_source_uri = $last_file ? $last_file->getFileUri() : '';
					\Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([
						'entity_type' => $entity_type_fallback,
						'entity_id' => $entity_id_fallback,
						'file_id' => $last_file_id,
						'source_filename' => $last_source_filename,
						'source_uri' => $last_source_uri,
					]);
					\Drupal::logger('dfg_3dviewer')->notice(
						'entity_update state fallback enqueue: entity @entity_id file @file_id.',
						[
							'@entity_id' => (string) $entity_id_fallback,
							'@file_id' => (string) $last_file_id,
						]
					);
					dfg_3dviewer_kick_convert_worker();
					return;
				}
				catch (\Throwable $e) {
					\Drupal::logger('dfg_3dviewer')->warning(
						'entity_update state fallback enqueue failed for entity @entity_id: @msg',
						[
							'@entity_id' => (string) $entity_id_fallback,
							'@msg' => $e->getMessage(),
						]
					);
				}
			}
		}

		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_update fallback behavior: kicking queue runner even without resolved file_id for entity @entity_id.',
			['@entity_id' => (string) ($entity->id() ?? '')]
		);
		try {
			$entity_id_fallback = (string) ($entity->id() ?? '');
			if ($entity_id_fallback !== '') {
				\Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([
					'entity_type' => (string) $entity->getEntityTypeId(),
					'entity_id' => $entity_id_fallback,
					'file_id' => 0,
					'source_filename' => '',
					'source_uri' => '',
				]);
				\Drupal::logger('dfg_3dviewer')->notice(
					'entity_update enqueued entity-only fallback job for entity @entity_id.',
					['@entity_id' => $entity_id_fallback]
				);
			}
		}
		catch (\Throwable $e) {
			\Drupal::logger('dfg_3dviewer')->warning(
				'entity_update failed to enqueue entity-only fallback job for entity @entity_id: @msg',
				[
					'@entity_id' => (string) ($entity->id() ?? ''),
					'@msg' => $e->getMessage(),
				]
			);
		}
		dfg_3dviewer_kick_convert_worker();
		return;
	}
	$file_id = (int) $resolved['file_id'];
	if (!empty($resolved['used_fallback'])) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_update fallback: configured upload field "@field" is empty; using "@fallback_field" with file_id=@file_id for entity @entity_id. Configured raw: @raw',
			[
				'@field' => (string) $resolved['configured_field'],
				'@fallback_field' => (string) $resolved['field_name'],
				'@file_id' => (string) $file_id,
				'@entity_id' => (string) ($entity->id() ?? ''),
				'@raw' => json_encode($resolved['configured_raw'], JSON_UNESCAPED_SLASHES),
			]
		);
	}

	$entity_id = $entity->id();
	if (empty($entity_id) && $entity->hasField('eid')) {
		$entity_id = $entity->get('eid')->value;
	}
	if (empty($entity_id)) {
		\Drupal::logger('dfg_3dviewer')->warning('entity_update cannot enqueue: missing entity ID.');
		dfg_3dviewer_kick_convert_worker();
		return;
	}

	$uploaded_file = \Drupal\file\Entity\File::load($file_id);
	$source_filename = $uploaded_file ? $uploaded_file->getFilename() : '';
	$source_uri = $uploaded_file ? $uploaded_file->getFileUri() : '';
	$key = $entity->getEntityTypeId() . ':' . $entity_id . ':' . $file_id;
	if (!empty($GLOBALS['dfg_3dviewer_enqueued_in_presave'][$key])) {
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_update skip: conversion was already queued in presave for entity @entity_id file @file_id.',
			[
				'@entity_id' => (string) $entity_id,
				'@file_id' => (string) $file_id,
			]
		);
		return;
	}

	try {
		\Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([
			'entity_type' => $entity->getEntityTypeId(),
			'entity_id' => $entity_id,
			'file_id' => $file_id,
			'source_filename' => $source_filename,
			'source_uri' => $source_uri,
		]);
		dfg_3dviewer_store_last_file_id((string) $entity->getEntityTypeId(), (string) $entity_id, (int) $file_id);
		\Drupal::logger('dfg_3dviewer')->notice(
			'entity_update enqueue ok: entity @entity_id file @file_id.',
			[
				'@entity_id' => (string) $entity_id,
				'@file_id' => (string) $file_id,
			]
		);
	}
	catch (\Throwable $e) {
		\Drupal::logger('dfg_3dviewer')->error(
			'entity_update enqueue failed for entity @entity_id file @file_id: @msg',
			[
				'@entity_id' => (string) $entity_id,
				'@file_id' => (string) $file_id,
				'@msg' => $e->getMessage(),
			]
		);
	}

	\Drupal::logger('dfg_3dviewer')->notice(
		'entity_update force kick on save: entity @entity_id file @file_id.',
		[
			'@entity_id' => (string) $entity_id,
			'@file_id' => (string) $file_id,
		]
	);

	dfg_3dviewer_kick_convert_worker();
}
/**
 * Normalizes the viewer asset base path to a path-first value.
 */
function dfg_3dviewer_normalize_base_module_path(string $value): string {
  $value = trim($value);

  if ($value === '') {
    return $value;
  }

  if (preg_match('@^https?://@i', $value)) {
    $parts = parse_url($value);
    if (!empty($parts['path'])) {
      $value = $parts['path'];
    }
  }
  elseif (preg_match('@^/[^/]+\.[^/]+/.+@', $value)) {
    $segments = array_values(array_filter(explode('/', $value), 'strlen'));
    array_shift($segments);
    $value = '/' . implode('/', $segments);
  }
  elseif (preg_match('@^[^/]+\.[^/]+/.+@', $value)) {
    $segments = explode('/', $value);
    array_shift($segments);
    $value = '/' . implode('/', $segments);
  }

  $value = preg_replace('@/{2,}@', '/', $value);

  if ($value !== '' && $value[0] !== '/') {
    $value = '/' . $value;
  }

  return rtrim($value, '/');
}

/**
 * Builds the viewer runtime settings object for drupalSettings / JS CONFIG.
 */
function dfg_3dviewer_build_js_settings(): array {
  $config = \Drupal::config('dfg_3dviewer.settings');

  $main_url = trim((string) (
    $config->get('dfg_3dviewer_main_url')
    ?? $config->get('main_url')
    ?? ''
  ));

  $metadata_url = trim((string) (
    $config->get('dfg_3dviewer_metadata_url')
    ?? $config->get('metadata_url')
    ?? ''
  ));

  $json_export_base_url = trim((string) (
    $config->get('dfg_3dviewer_json_export_base_url')
    ?? $config->get('json_export_base_url')
    ?? ''
  ));

  $base_namespace = trim((string) (
    $config->get('dfg_3dviewer_basenamespace')
    ?? $config->get('base_namespace')
    ?? ''
  ));

  $base_module_path = dfg_3dviewer_normalize_base_module_path(trim((string) (
    $config->get('dfg_3dviewer_base_module_path')
    ?? $config->get('base_path')
    ?? ''
  )));

  if ($base_module_path === '') {
    $base_module_path = '/libraries/dfg-3dviewer/assets';
  }

  $lightweight = filter_var(
    $config->get('dfg_3dviewer_lightweight')
      ?? $config->get('lightweight')
      ?? FALSE,
    FILTER_VALIDATE_BOOLEAN
  );

  return [
    'baseNamespace' => $base_namespace,
    'mainUrl' => $main_url,
    'metadataUrl' => $metadata_url,
    'jsonExportBaseUrl' => $json_export_base_url,
    'baseModulePath' => $base_module_path,
    'entity' => [
      'bundle' => trim((string) (
        $config->get('dfg_3dviewer_entitybundle')
        ?? $config->get('entity_bundle')
        ?? ''
      )),
      'fieldDf' => trim((string) (
        $config->get('dfg_3dviewer_field_df')
        ?? $config->get('field_df')
        ?? 'field_df'
      )),
      'idUri' => trim((string) (
        $config->get('dfg_3dviewer_entity_id_uri')
        ?? $config->get('entity_id_uri')
        ?? '/wisski/navigate/(.*)/view'
      )),
      'viewEntityPath' => trim((string) (
        $config->get('dfg_3dviewer_view_entity_path')
        ?? $config->get('view_entity_path')
        ?? '/wisski/navigate/'
      )),
      'attributeId' => trim((string) (
        $config->get('dfg_3dviewer_attribute_id')
        ?? $config->get('attribute_id')
        ?? 'wisski_id'
      )),
      'exportViewer' => trim((string) (
        $config->get('dfg_3dviewer_export_viewer')
        ?? $config->get('export_viewer')
        ?? 'field_df'
      )),
      'exportViewerUrl' => trim((string) (
        $config->get('dfg_3dviewer_export_viewer_url')
        ?? $config->get('export_viewer_url')
        ?? ''
      )),
      'metadata' => [
        'source' => 'Drupal',
        'sourceType' => '',
        'url' => '',
      ],
    ],
    'viewer' => [
      'container' => trim((string) (
        $config->get('dfg_3dviewer_container')
        ?? $config->get('container')
        ?? 'DFG_3DViewer'
      )),
      'fileUpload' => trim((string) (
        $config->get('dfg_3dviewer_viewer_file_upload')
        ?? $config->get('viewer_file_upload')
        ?? ''
      )),
      'fileName' => trim((string) (
        $config->get('dfg_3dviewer_viewer_file_name')
        ?? $config->get('viewer_file_name')
        ?? ''
      )),
      'api3dFileField' => trim((string) (
        $config->get('dfg_3dviewer_api_3d_file_field')
        ?? $config->get('api_3d_file_field')
        ?? ''
      )),
      'imageGeneration' => trim((string) (
        $config->get('dfg_3dviewer_image_generation')
        ?? $config->get('image_generation')
        ?? ''
      )),
      'lightweight' => $lightweight,
      'editor' => !$lightweight,
      'scaleContainer' => [
        'x' => (string) (
          $config->get('dfg_3dviewer_scale_container_x')
          ?? $config->get('scale_container_x')
          ?? '1'
        ),
        'y' => (string) (
          $config->get('dfg_3dviewer_scale_container_y')
          ?? $config->get('scale_container_y')
          ?? '1'
        ),
      ],
      'gallery' => [
        'container' => trim((string) (
          $config->get('dfg_3dviewer_gallery_container')
          ?? $config->get('gallery_container')
          ?? ''
        )),
        'imageClass' => trim((string) (
          $config->get('dfg_3dviewer_gallery_image_class')
          ?? $config->get('gallery_image_class')
          ?? ''
        )),
        'imageId' => trim((string) (
          $config->get('dfg_3dviewer_gallery_image_id')
          ?? $config->get('gallery_image_id')
          ?? ''
        )),
        'build' => TRUE,
      ],
    ],
  ];
}

/**
 * Attaches viewer settings and CSRF token to a render element.
 */
function dfg_3dviewer_attach_settings(array &$element): void {
  $settings = dfg_3dviewer_build_js_settings();
  $token = \Drupal::csrfToken()->get('rest');
  $settings['csrfToken'] = $token;
  $element['#attached']['drupalSettings']['dfg_3dviewer'] = $settings;
  $element['#attached']['html_head'][] = [
    [
      '#tag' => 'script',
      '#value' => 'window.CSRF_TOKEN = ' . json_encode($token, JSON_THROW_ON_ERROR) . ';',
    ],
    'dfg_3dviewer_csrf_token',
  ];
}

function dfg_3dviewer_get_library(): string {
  return 'dfg_3dviewer/dfg_3dviewer.viewer';
}
