<?php

namespace Drupal\eb\Service;

use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\eb\PluginManager\EbExtensionPluginManager;

/**
 * Service for generating definition data from Drupal entities.
 *
 * Reverse-engineers Drupal entity architecture (bundles, fields, displays)
 * into definition format data suitable for EbDefinition entities.
 * This enables the export workflow to be definition-centric.
 */
class DefinitionGenerator implements DefinitionGeneratorInterface {

  /**
   * Constructor.
   *
   * @param \Drupal\eb\Service\DiscoveryServiceInterface $discoveryService
   *   The discovery service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
   *   The entity field manager.
   * @param \Drupal\eb\PluginManager\EbExtensionPluginManager $extensionManager
   *   The extension plugin manager.
   */
  public function __construct(
    protected DiscoveryServiceInterface $discoveryService,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected EntityFieldManagerInterface $entityFieldManager,
    protected EbExtensionPluginManager $extensionManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function generate(array $bundleSelection, array $options = []): array {
    $options = $this->normalizeOptions($options);

    $data = [
      'bundle_definitions' => [],
      'field_definitions' => [],
      'display_field_definitions' => [],
      'menu_definitions' => [],
    ];

    // Generate definitions for each selected bundle.
    foreach ($bundleSelection as $entityTypeId => $bundles) {
      foreach ($bundles as $bundleId) {
        $bundleData = $this->generateBundle($entityTypeId, $bundleId, $options);
        $this->mergeDefinitionData($data, $bundleData);
      }
    }

    // Collect extension data.
    if ($options['include_extensions']) {
      foreach ($this->extensionManager->getExtensions() as $extension) {
        foreach ($bundleSelection as $entityTypeId => $bundles) {
          foreach ($bundles as $bundleId) {
            $extensionData = $extension->extractConfig($entityTypeId, $bundleId);
            if (!empty($extensionData)) {
              $this->mergeDefinitionData($data, $extensionData);
            }
          }
        }
      }
    }

    // Filter out empty arrays to keep output clean.
    return array_filter($data, fn($value) => !empty($value));
  }

  /**
   * {@inheritdoc}
   */
  public function generateBundle(string $entityTypeId, string $bundleId, array $options = []): array {
    $options = $this->normalizeOptions($options);

    $data = [
      'bundle_definitions' => [],
      'field_definitions' => [],
      'display_field_definitions' => [],
    ];

    // Extract bundle definition.
    $bundleDefinition = $this->extractBundleDefinition($entityTypeId, $bundleId);
    if ($bundleDefinition) {
      $data['bundle_definitions'][] = $bundleDefinition;
    }

    // Extract field definitions.
    if ($options['include_fields']) {
      $fieldDefinitions = $this->extractFieldDefinitions($entityTypeId, $bundleId, $options);

      // If displays are included, merge default mode display settings into
      // field definitions (field-centric architecture).
      if ($options['include_displays']) {
        $allDisplayDefinitions = $this->extractDisplayDefinitions($entityTypeId, $bundleId);
        $fieldDefinitions = $this->mergeDefaultDisplayIntoFields(
          $fieldDefinitions,
          $allDisplayDefinitions
        );

        // Keep only non-default mode display definitions.
        $nonDefaultDisplays = [];
        foreach ($allDisplayDefinitions as $displayDef) {
          $mode = $displayDef['mode'] ?? 'default';
          if ($mode !== 'default') {
            $nonDefaultDisplays[] = $displayDef;
          }
        }
        $data['display_field_definitions'] = $nonDefaultDisplays;
      }

      $data['field_definitions'] = $fieldDefinitions;
    }
    elseif ($options['include_displays']) {
      // Extract display definitions without field merging.
      $displayDefinitions = $this->extractDisplayDefinitions($entityTypeId, $bundleId);
      $data['display_field_definitions'] = $displayDefinitions;
    }

    return $data;
  }

  /**
   * Merges default mode display settings into field definitions.
   *
   * Implements field-centric architecture: widget/formatter for default mode
   * are stored in field_definitions, not separate display_field_definitions.
   *
   * @param array<int, array<string, mixed>> $fieldDefinitions
   *   The field definitions.
   * @param array<int, array<string, mixed>> $displayDefinitions
   *   All display definitions (form and view).
   *
   * @return array<int, array<string, mixed>>
   *   Field definitions with merged default display settings, sorted by weight.
   */
  protected function mergeDefaultDisplayIntoFields(array $fieldDefinitions, array $displayDefinitions): array {
    // Build lookup maps for default mode displays indexed by field_name.
    $formDisplaysByField = [];
    $viewDisplaysByField = [];

    foreach ($displayDefinitions as $displayDef) {
      $mode = $displayDef['mode'] ?? 'default';
      $fieldName = $displayDef['field_name'] ?? '';
      $displayType = $displayDef['display_type'] ?? '';

      // Skip non-default modes and entries without field_name.
      if ($mode !== 'default' || empty($fieldName)) {
        continue;
      }

      if ($displayType === 'form') {
        $formDisplaysByField[$fieldName] = $displayDef;
      }
      elseif ($displayType === 'view') {
        $viewDisplaysByField[$fieldName] = $displayDef;
      }
    }

    // Merge display settings into each field definition.
    foreach ($fieldDefinitions as $index => $fieldDef) {
      $fieldName = $fieldDef['field_name'] ?? '';
      if (empty($fieldName)) {
        continue;
      }

      // Merge form display settings (widget).
      if (isset($formDisplaysByField[$fieldName])) {
        $formDisplay = $formDisplaysByField[$fieldName];

        if (!empty($formDisplay['widget_type'])) {
          $fieldDefinitions[$index]['widget'] = $formDisplay['widget_type'];
        }
        if (!empty($formDisplay['widget_settings'])) {
          $fieldDefinitions[$index]['widget_settings'] = $formDisplay['widget_settings'];
        }
        if (isset($formDisplay['weight'])) {
          $fieldDefinitions[$index]['weight'] = $formDisplay['weight'];
        }
      }

      // Merge view display settings (formatter).
      if (isset($viewDisplaysByField[$fieldName])) {
        $viewDisplay = $viewDisplaysByField[$fieldName];

        if (!empty($viewDisplay['formatter_type'])) {
          $fieldDefinitions[$index]['formatter'] = $viewDisplay['formatter_type'];
        }
        if (!empty($viewDisplay['formatter_settings'])) {
          $fieldDefinitions[$index]['formatter_settings'] = $viewDisplay['formatter_settings'];
        }
        if (isset($viewDisplay['label'])) {
          $fieldDefinitions[$index]['label_display'] = $viewDisplay['label'];
        }
        // Use view display weight if form display weight not set.
        if (!isset($fieldDefinitions[$index]['weight']) && isset($viewDisplay['weight'])) {
          $fieldDefinitions[$index]['weight'] = $viewDisplay['weight'];
        }
      }
    }

    // Sort by weight to maintain proper field order.
    usort($fieldDefinitions, function (array $a, array $b): int {
      $weightA = $a['weight'] ?? 0;
      $weightB = $b['weight'] ?? 0;
      return $weightA <=> $weightB;
    });

    return $fieldDefinitions;
  }

  /**
   * Normalizes generation options with defaults.
   *
   * @param array<string, bool> $options
   *   The provided options.
   *
   * @return array<string, bool>
   *   The normalized options with defaults applied.
   */
  protected function normalizeOptions(array $options): array {
    return [
      'include_fields' => $options['include_fields'] ?? TRUE,
      'include_displays' => $options['include_displays'] ?? TRUE,
      'include_extensions' => $options['include_extensions'] ?? TRUE,
      'normalize_settings' => $options['normalize_settings'] ?? TRUE,
    ];
  }

  /**
   * Extracts bundle definition from a Drupal bundle entity.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   *
   * @return array<string, mixed>|null
   *   The bundle definition array or NULL if bundle not found.
   */
  protected function extractBundleDefinition(string $entityTypeId, string $bundleId): ?array {
    $entityType = $this->entityTypeManager->getDefinition($entityTypeId, FALSE);
    if (!$entityType) {
      return NULL;
    }

    $bundleDefinition = [
      'entity_type' => $entityTypeId,
      'bundle_id' => $bundleId,
    ];

    // Load bundle entity if exists (e.g., node_type, taxonomy_vocabulary).
    $bundleEntityType = $entityType->getBundleEntityType();
    if ($bundleEntityType) {
      $bundleEntity = $this->entityTypeManager->getStorage($bundleEntityType)->load($bundleId);
      if ($bundleEntity) {
        $bundleDefinition['label'] = $bundleEntity->label();

        // Get description if available.
        if (method_exists($bundleEntity, 'getDescription')) {
          $description = $bundleEntity->getDescription();
          if (!empty($description)) {
            $bundleDefinition['description'] = $description;
          }
        }
      }
      else {
        // Bundle entity not found but might still exist as a bundle.
        $bundles = $this->discoveryService->getBundlesForEntityType($entityTypeId);
        if (isset($bundles[$bundleId])) {
          $bundleDefinition['label'] = $bundles[$bundleId]['label'] ?? $bundleId;
        }
        else {
          return NULL;
        }
      }
    }
    else {
      // Entity type without bundle entity (e.g., user).
      $bundleDefinition['label'] = $bundleId;
    }

    return $bundleDefinition;
  }

  /**
   * Extracts field definitions for a bundle.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   * @param array<string, bool> $options
   *   Generation options.
   *
   * @return array<int, array<string, mixed>>
   *   Array of field definition arrays.
   */
  protected function extractFieldDefinitions(string $entityTypeId, string $bundleId, array $options): array {
    $fieldDefinitions = [];
    $definitions = $this->entityFieldManager->getFieldDefinitions($entityTypeId, $bundleId);

    foreach ($definitions as $fieldName => $definition) {
      // Skip base fields.
      if ($definition->getFieldStorageDefinition()->isBaseField()) {
        continue;
      }

      $fieldDef = [
        'entity_type' => $entityTypeId,
        'bundle' => $bundleId,
        'field_name' => $fieldName,
        'field_type' => $definition->getType(),
        'label' => (string) $definition->getLabel(),
      ];

      // Add required flag.
      if ($definition->isRequired()) {
        $fieldDef['required'] = TRUE;
      }

      // Add description if present.
      $description = $definition->getDescription();
      if (!empty($description)) {
        $fieldDef['description'] = (string) $description;
      }

      // Add cardinality.
      $cardinality = $definition->getFieldStorageDefinition()->getCardinality();
      if ($cardinality !== 1) {
        $fieldDef['cardinality'] = $cardinality;
      }

      // Add settings (normalized if requested).
      $settings = $this->extractFieldSettings($definition, $options['normalize_settings']);
      if (!empty($settings)) {
        $fieldDef['settings'] = $settings;
      }

      // Add field storage settings if different from defaults.
      $storageSettings = $this->extractFieldStorageSettings($definition, $options['normalize_settings']);
      if (!empty($storageSettings)) {
        $fieldDef['field_storage_settings'] = $storageSettings;
      }

      $fieldDefinitions[] = $fieldDef;
    }

    return $fieldDefinitions;
  }

  /**
   * Extracts field instance settings.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
   *   The field definition.
   * @param bool $normalize
   *   Whether to normalize (strip defaults).
   *
   * @return array<string, mixed>
   *   The field settings.
   */
  protected function extractFieldSettings($definition, bool $normalize): array {
    $settings = $definition->getSettings();
    $fieldType = $definition->getType();

    if (!$normalize) {
      return $settings;
    }

    // For entity_reference fields, structure settings properly.
    if ($fieldType === 'entity_reference') {
      return $this->extractEntityReferenceSettings($definition);
    }

    // For image fields, extract the relevant settings.
    if ($fieldType === 'image') {
      return $this->extractImageSettings($settings);
    }

    // For other fields, normalize against defaults.
    return $this->normalizeFieldSettings($fieldType, $settings);
  }

  /**
   * Extracts entity reference field settings.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
   *   The field definition.
   *
   * @return array<string, mixed>
   *   The structured entity reference settings.
   */
  protected function extractEntityReferenceSettings($definition): array {
    $settings = $definition->getSettings();
    $handlerSettings = $definition->getSetting('handler_settings') ?? [];

    $result = [
      'target_type' => $settings['target_type'] ?? NULL,
    ];

    // Include handler if not default.
    $handler = $settings['handler'] ?? 'default';
    if ($handler !== 'default') {
      $result['handler'] = $handler;
    }

    // Include target bundles if specified.
    if (!empty($handlerSettings['target_bundles'])) {
      $result['handler_settings'] = [
        'target_bundles' => $handlerSettings['target_bundles'],
      ];
    }

    return array_filter($result, fn($v) => $v !== NULL);
  }

  /**
   * Extracts image field settings.
   *
   * @param array<string, mixed> $settings
   *   The full settings array.
   *
   * @return array<string, mixed>
   *   The relevant image settings.
   */
  protected function extractImageSettings(array $settings): array {
    $defaults = [
      'file_directory' => 'inline-images',
      'file_extensions' => 'png gif jpg jpeg',
      'max_filesize' => '',
      'max_resolution' => '',
      'min_resolution' => '',
      'alt_field' => TRUE,
      'alt_field_required' => TRUE,
      'title_field' => FALSE,
      'title_field_required' => FALSE,
    ];

    $result = [];

    foreach ($defaults as $key => $default) {
      if (isset($settings[$key]) && $settings[$key] !== $default) {
        $result[$key] = $settings[$key];
      }
    }

    return $result;
  }

  /**
   * Extracts field storage settings.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface $definition
   *   The field definition.
   * @param bool $normalize
   *   Whether to normalize (strip defaults).
   *
   * @return array<string, mixed>
   *   The field storage settings.
   */
  protected function extractFieldStorageSettings($definition, bool $normalize): array {
    $storage = $definition->getFieldStorageDefinition();
    $settings = $storage->getSettings();

    if (!$normalize || empty($settings)) {
      return $settings;
    }

    $fieldType = $definition->getType();
    return $this->normalizeFieldSettings($fieldType, $settings, TRUE);
  }

  /**
   * Normalizes field settings by removing default values.
   *
   * @param string $fieldType
   *   The field type.
   * @param array<string, mixed> $settings
   *   The settings to normalize.
   * @param bool $isStorage
   *   Whether these are storage settings.
   *
   * @return array<string, mixed>
   *   The normalized settings (defaults removed).
   */
  protected function normalizeFieldSettings(string $fieldType, array $settings, bool $isStorage = FALSE): array {
    // Get default settings for the field type.
    $fieldTypeInfo = $this->discoveryService->getFieldTypeDefinition($fieldType);
    if (!$fieldTypeInfo) {
      return $settings;
    }

    $defaults = $isStorage
      ? ($fieldTypeInfo['storage_settings'] ?? [])
      : ($fieldTypeInfo['field_settings'] ?? []);

    // If we don't have defaults, return all settings.
    if (empty($defaults)) {
      return $settings;
    }

    $normalized = [];
    foreach ($settings as $key => $value) {
      // Skip if value matches default.
      if (isset($defaults[$key]) && $defaults[$key] === $value) {
        continue;
      }
      // Skip empty arrays and null values.
      if ($value === NULL || $value === []) {
        continue;
      }
      $normalized[$key] = $value;
    }

    return $normalized;
  }

  /**
   * Extracts display definitions for a bundle.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   *
   * @return array<int, array<string, mixed>>
   *   Array of display field definition arrays.
   */
  protected function extractDisplayDefinitions(string $entityTypeId, string $bundleId): array {
    $displayDefinitions = [];

    // Extract form display configuration.
    $formDisplayDefinitions = $this->extractFormDisplayDefinitions($entityTypeId, $bundleId);
    $displayDefinitions = array_merge($displayDefinitions, $formDisplayDefinitions);

    // Extract view display configuration.
    $viewDisplayDefinitions = $this->extractViewDisplayDefinitions($entityTypeId, $bundleId);
    $displayDefinitions = array_merge($displayDefinitions, $viewDisplayDefinitions);

    return $displayDefinitions;
  }

  /**
   * Extracts form display definitions.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   *
   * @return array<int, array<string, mixed>>
   *   Array of form display field definitions.
   */
  protected function extractFormDisplayDefinitions(string $entityTypeId, string $bundleId): array {
    $definitions = [];

    try {
      $storage = $this->entityTypeManager->getStorage('entity_form_display');
      // @phpstan-ignore nullCoalesce.expr
      $displays = $storage->loadByProperties([
        'targetEntityType' => $entityTypeId,
        'bundle' => $bundleId,
      ]) ?? [];

      foreach ($displays as $display) {
        /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
        $mode = $display->getMode();
        $components = $display->getComponents();

        foreach ($components as $fieldName => $component) {
          // Skip components without widget type (e.g., hidden fields).
          if (empty($component['type'])) {
            continue;
          }

          $def = [
            'entity_type' => $entityTypeId,
            'bundle' => $bundleId,
            'display_type' => 'form',
            'mode' => $mode,
            'field_name' => $fieldName,
            'widget_type' => $component['type'],
          ];

          if (isset($component['weight'])) {
            $def['weight'] = $component['weight'];
          }

          if (!empty($component['settings'])) {
            $def['widget_settings'] = $component['settings'];
          }

          if (!empty($component['third_party_settings'])) {
            $def['third_party_settings'] = $component['third_party_settings'];
          }

          $definitions[] = $def;
        }
      }
    }
    catch (\Exception $e) {
      // Silently handle missing form displays.
    }

    return $definitions;
  }

  /**
   * Extracts view display definitions.
   *
   * @param string $entityTypeId
   *   The entity type ID.
   * @param string $bundleId
   *   The bundle ID.
   *
   * @return array<int, array<string, mixed>>
   *   Array of view display field definitions.
   */
  protected function extractViewDisplayDefinitions(string $entityTypeId, string $bundleId): array {
    $definitions = [];

    try {
      $storage = $this->entityTypeManager->getStorage('entity_view_display');
      // @phpstan-ignore nullCoalesce.expr
      $displays = $storage->loadByProperties([
        'targetEntityType' => $entityTypeId,
        'bundle' => $bundleId,
      ]) ?? [];

      foreach ($displays as $display) {
        /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
        $mode = $display->getMode();
        $components = $display->getComponents();

        foreach ($components as $fieldName => $component) {
          // Skip components without formatter type (e.g., hidden fields).
          if (empty($component['type'])) {
            continue;
          }

          $def = [
            'entity_type' => $entityTypeId,
            'bundle' => $bundleId,
            'display_type' => 'view',
            'mode' => $mode,
            'field_name' => $fieldName,
            'formatter_type' => $component['type'],
          ];

          if (isset($component['weight'])) {
            $def['weight'] = $component['weight'];
          }

          if (isset($component['label'])) {
            $def['label'] = $component['label'];
          }

          if (!empty($component['settings'])) {
            $def['formatter_settings'] = $component['settings'];
          }

          if (!empty($component['third_party_settings'])) {
            $def['third_party_settings'] = $component['third_party_settings'];
          }

          $definitions[] = $def;
        }
      }
    }
    catch (\Exception $e) {
      // Silently handle missing view displays.
    }

    return $definitions;
  }

  /**
   * Merges definition data into a target array.
   *
   * @param array<string, mixed> $target
   *   The target array to merge into.
   * @param array<string, mixed> $source
   *   The source array to merge from.
   */
  protected function mergeDefinitionData(array &$target, array $source): void {
    foreach ($source as $key => $value) {
      if (!is_array($value)) {
        continue;
      }

      if (!isset($target[$key])) {
        $target[$key] = [];
      }

      // Merge arrays (append items).
      $target[$key] = array_merge($target[$key], $value);
    }
  }

}
