<?php

namespace Drupal\eb\Service\ChangeDetection;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\eb\Enum\ChangeDetectionMode;
use Drupal\eb\Exception\ValidationException;
use Drupal\eb\PluginManager\EbExtensionPluginManager;
use Drupal\eb\Service\Traits\EntityKeyTrait;

/**
 * Service for detecting changes between YAML and Drupal state.
 *
 * Determines whether operations should create, update, or skip entities
 * based on current Drupal configuration and mode settings.
 */
class ChangeDetector {

  use EntityKeyTrait;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\eb\PluginManager\EbExtensionPluginManager $extensionManager
   *   The extension plugin manager.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected EbExtensionPluginManager $extensionManager,
  ) {
  }

  /**
   * Detect changes and modify operation queue accordingly.
   *
   * @param array<array<string, mixed>> $operations
   *   Array of operation data arrays.
   * @param \Drupal\eb\Enum\ChangeDetectionMode|string $mode
   *   Mode: ChangeDetectionMode enum or string (sync, create_only, etc.).
   *
   * @return array<array<string, mixed>>
   *   Modified array of operation data arrays.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If mode constraints violated (e.g., create_only but entity exists).
   */
  public function detectChanges(
    array $operations,
    ChangeDetectionMode|string $mode,
  ): array {
    // Convert string mode to enum for internal processing.
    $modeEnum = $mode instanceof ChangeDetectionMode
      ? $mode
      : ChangeDetectionMode::fromString($mode);

    $modifiedOperations = [];

    foreach ($operations as $operation) {
      $operationType = $operation['operation'] ?? '';

      // Only process create/update operations.
      if (!in_array($operationType, $this->getMutableOperations())) {
        $modifiedOperations[] = $operation;
        continue;
      }

      $exists = $this->entityExists($operation);
      $changes = NULL;

      if ($exists) {
        $current = $this->getCurrentConfig($operation);
        $changes = $this->compareConfig($operation, $current);
      }

      // Determine operation based on mode and existence.
      $newOperationType = $this->determineOperationForMode(
        $operation,
        $exists,
        $changes,
        $modeEnum,
      );

      if ($newOperationType === NULL) {
        // Skip this operation.
        continue;
      }

      // Update operation type if changed.
      $operation['operation'] = $newOperationType;
      $modifiedOperations[] = $operation;
    }

    return $modifiedOperations;
  }

  /**
   * Get list of operations that create or update entities.
   *
   * @return array<string>
   *   Array of operation types.
   */
  protected function getMutableOperations(): array {
    return [
      'create_bundle',
      'update_bundle',
      'create_field',
      'update_field',
      'create_menu',
      'create_menu_link',
      'configure_form_mode',
      'configure_view_mode',
    ];
  }

  /**
   * Check if entity exists in Drupal.
   *
   * @param array<string, mixed> $operation
   *   Operation data.
   *
   * @return bool
   *   TRUE if entity exists.
   */
  protected function entityExists(array $operation): bool {
    $operationType = $operation['operation'] ?? '';

    try {
      if (in_array($operationType, ['create_bundle', 'update_bundle'])) {
        $entityType = $operation['entity_type'] ?? '';
        $bundleId = $operation['bundle_id'] ?? '';

        if (!$entityType || !$bundleId) {
          return FALSE;
        }

        $bundleEntityType = $this->entityTypeManager
          ->getDefinition($entityType)
          ->getBundleEntityType();

        if (!$bundleEntityType) {
          return FALSE;
        }

        $storage = $this->entityTypeManager->getStorage($bundleEntityType);
        return $storage->load($bundleId) !== NULL;
      }

      if (in_array($operationType, ['create_field', 'update_field'])) {
        $entityType = $operation['entity_type'] ?? '';
        $bundle = $operation['bundle'] ?? '';
        $fieldName = $operation['field_name'] ?? '';

        if (!$entityType || !$bundle || !$fieldName) {
          return FALSE;
        }

        $fieldConfig = $this->entityTypeManager
          ->getStorage('field_config')
          ->load("{$entityType}.{$bundle}.{$fieldName}");

        return $fieldConfig !== NULL;
      }

      if ($operationType === 'create_menu') {
        $menuId = $operation['menu_id'] ?? '';
        if (!$menuId) {
          return FALSE;
        }

        $menu = $this->entityTypeManager
          ->getStorage('menu')
          ->load($menuId);

        return $menu !== NULL;
      }

      if (in_array($operationType, ['configure_form_mode', 'configure_view_mode'])) {
        // Display configurations are handled by the operation itself.
        // Return FALSE to always configure displays.
        return FALSE;
      }
    }
    catch (\Exception $e) {
      return FALSE;
    }

    return FALSE;
  }

  /**
   * Get current Drupal configuration for entity.
   *
   * @param array<string, mixed> $operation
   *   Operation data.
   *
   * @return array<string, mixed>
   *   Current configuration.
   */
  protected function getCurrentConfig(array $operation): array {
    $operationType = $operation['operation'] ?? '';

    try {
      if (in_array($operationType, ['create_bundle', 'update_bundle'])) {
        $entityType = $operation['entity_type'] ?? '';
        $bundleId = $operation['bundle_id'] ?? '';

        $bundleEntityType = $this->entityTypeManager
          ->getDefinition($entityType)
          ->getBundleEntityType();

        $storage = $this->entityTypeManager->getStorage($bundleEntityType);
        $bundle = $storage->load($bundleId);

        if (!$bundle) {
          return [];
        }

        $result = ['label' => $bundle->label()];

        // Try to get description if the method exists.
        if (method_exists($bundle, 'getDescription')) {
          $result['description'] = $bundle->getDescription();
        }
        elseif ($bundle instanceof FieldableEntityInterface && $bundle->hasField('description')) {
          $result['description'] = $bundle->get('description')->value ?? '';
        }

        return $result;
      }

      if (in_array($operationType, ['create_field', 'update_field'])) {
        $entityType = $operation['entity_type'] ?? '';
        $bundle = $operation['bundle'] ?? '';
        $fieldName = $operation['field_name'] ?? '';

        $fieldConfig = $this->entityTypeManager
          ->getStorage('field_config')
          ->load("{$entityType}.{$bundle}.{$fieldName}");

        if (!$fieldConfig) {
          return [];
        }

        return [
          'label' => $fieldConfig->label(),
          'description' => $fieldConfig->getDescription(),
          'required' => $fieldConfig->isRequired(),
          'translatable' => $fieldConfig->isTranslatable(),
          'default_value' => $fieldConfig->getDefaultValueLiteral(),
          'field_config_settings' => $fieldConfig->getSettings(),
        ];
      }

      if ($operationType === 'create_menu') {
        $menuId = $operation['menu_id'] ?? '';
        $menu = $this->entityTypeManager
          ->getStorage('menu')
          ->load($menuId);

        if (!$menu) {
          return [];
        }

        return [
          'label' => $menu->label(),
          'description' => $menu->getDescription(),
        ];
      }
    }
    catch (\Exception $e) {
      return [];
    }

    return [];
  }

  /**
   * Compare operation data vs current config.
   *
   * @param array<string, mixed> $operation
   *   Desired operation data.
   * @param array<string, mixed> $current
   *   Current Drupal config.
   *
   * @return array<string>|null
   *   Array of changed keys, or NULL if identical.
   */
  protected function compareConfig(array $operation, array $current): ?array {
    if (empty($current)) {
      return NULL;
    }

    $changes = [];
    $compareKeys = $this->getComparableKeys($operation['operation'] ?? '');

    foreach ($compareKeys as $key) {
      $desiredValue = $operation[$key] ?? NULL;
      $currentValue = $current[$key] ?? NULL;

      // Skip if not specified in operation (partial mode).
      if ($desiredValue === NULL) {
        continue;
      }

      // Compare values.
      if ($this->valuesAreDifferent($desiredValue, $currentValue)) {
        $changes[] = $key;
      }
    }

    return empty($changes) ? NULL : $changes;
  }

  /**
   * Get keys to compare for an operation type.
   *
   * @param string $operationType
   *   Operation type.
   *
   * @return array<string>
   *   Array of keys to compare.
   */
  protected function getComparableKeys(string $operationType): array {
    return match ($operationType) {
      'create_bundle', 'update_bundle' => ['label', 'description', 'settings'],
      'create_field', 'update_field' => [
        'label', 'description', 'required', 'translatable',
        'default_value', 'field_config_settings',
      ],
      'create_menu' => ['label', 'description'],
      default => [],
    };
  }

  /**
   * Check if two values are different.
   *
   * @param mixed $value1
   *   First value.
   * @param mixed $value2
   *   Second value.
   *
   * @return bool
   *   TRUE if values are different.
   */
  protected function valuesAreDifferent($value1, $value2): bool {
    // Normalize arrays for comparison.
    if (is_array($value1) && is_array($value2)) {
      return serialize($value1) !== serialize($value2);
    }

    return $value1 !== $value2;
  }

  /**
   * Determine operation based on mode and existence (legacy string mode).
   *
   * @param array<string, mixed> $operation
   *   Operation data.
   * @param bool $exists
   *   Whether entity exists.
   * @param array<string>|null $changes
   *   Changed keys, or NULL if no changes.
   * @param string $mode
   *   Mode (sync, create_only, update_only, replace).
   *
   * @return string|null
   *   Operation type (create_*, update_*) or NULL to skip.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If mode constraints violated.
   */
  protected function determineOperation(array $operation, bool $exists, ?array $changes, string $mode): ?string {
    return $this->determineOperationForMode(
      $operation,
      $exists,
      $changes,
      ChangeDetectionMode::fromString($mode),
    );
  }

  /**
   * Determine operation based on mode enum and entity existence.
   *
   * Uses PHP 8 match expressions to cleanly handle all mode cases in one place.
   *
   * @param array<string, mixed> $operation
   *   Operation data.
   * @param bool $exists
   *   Whether entity exists.
   * @param array<string>|null $changes
   *   Changed keys, or NULL if no changes.
   * @param \Drupal\eb\Enum\ChangeDetectionMode $mode
   *   The change detection mode.
   *
   * @return string|null
   *   Operation type (create_*, update_*) or NULL to skip.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If mode constraints violated.
   */
  protected function determineOperationForMode(
    array $operation,
    bool $exists,
    ?array $changes,
    ChangeDetectionMode $mode,
  ): ?string {
    // Allow extension plugins to handle change detection first.
    $context = [
      'exists' => $exists,
      'changes' => $changes,
      'mode' => $mode->value,
    ];
    foreach ($this->extensionManager->getExtensions() as $extension) {
      if ($extension->appliesTo($operation)) {
        $result = $extension->detectChanges($operation, $context);
        if ($result !== NULL) {
          return $result;
        }
      }
    }

    // Get base type (e.g., "bundle" from "create_bundle").
    $baseType = preg_replace('/^(create_|update_)/', '', $operation['operation'] ?? '');
    $hasChanges = $changes !== NULL && !empty($changes);

    // Unified mode handling using match expression.
    return match ($mode) {
      ChangeDetectionMode::Sync => match (TRUE) {
        !$exists => "create_{$baseType}",
        $hasChanges => "update_{$baseType}",
        default => NULL,
      },

      ChangeDetectionMode::CreateOnly => $this->handleCreateOnlyMode(
        $operation,
        $baseType,
        $exists,
      ),

      ChangeDetectionMode::UpdateOnly => $this->handleUpdateOnlyMode(
        $operation,
        $baseType,
        $exists,
        $hasChanges,
      ),

      ChangeDetectionMode::Replace => "create_{$baseType}",
    };
  }

  /**
   * Handle create_only mode with validation.
   *
   * @param array<string, mixed> $operation
   *   Operation data.
   * @param string $baseType
   *   Base operation type.
   * @param bool $exists
   *   Whether entity exists.
   *
   * @return string
   *   Operation type.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If entity already exists.
   */
  private function handleCreateOnlyMode(array $operation, string $baseType, bool $exists): string {
    if ($exists) {
      $entityKey = $this->getHumanReadableEntityKey($operation);
      throw new ValidationException("{$entityKey} already exists (create_only mode)");
    }
    return "create_{$baseType}";
  }

  /**
   * Handle update_only mode with validation.
   *
   * @param array<string, mixed> $operation
   *   Operation data.
   * @param string $baseType
   *   Base operation type.
   * @param bool $exists
   *   Whether entity exists.
   * @param bool $hasChanges
   *   Whether there are changes.
   *
   * @return string|null
   *   Operation type or NULL to skip.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If entity doesn't exist.
   */
  private function handleUpdateOnlyMode(
    array $operation,
    string $baseType,
    bool $exists,
    bool $hasChanges,
  ): ?string {
    if (!$exists) {
      $entityKey = $this->getHumanReadableEntityKey($operation);
      throw new ValidationException("{$entityKey} not found (update_only mode)");
    }
    return $hasChanges ? "update_{$baseType}" : NULL;
  }

}
