<?php

namespace Drupal\eb\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\eb\Entity\EbDefinition;
use Drupal\eb\Exception\ValidationException;

/**
 * Factory service for creating EbDefinition entities from flat YAML.
 *
 * Supports v5.0 flat YAML format with these root-level arrays:
 * - bundle_definitions: Content types/vocabularies with extension columns.
 * - field_definitions: Field storage and config.
 * - field_group_definitions: Visual field groupings.
 * - display_field_definitions: One row per field-in-display.
 * - menu_definitions: Menu configurations.
 */
class DefinitionFactory {

  /**
   * Maximum length for definition IDs.
   */
  protected const MAX_ID_LENGTH = 64;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
  ) {}

  /**
   * Creates an EbDefinition from flat YAML data.
   *
   * @param array<mixed> $data
   *   The raw parsed YAML data in flat format.
   * @param string|null $filename
   *   The source filename (used to derive id if not in YAML).
   *
   * @return \Drupal\eb\Entity\EbDefinition
   *   The created (but not saved) definition entity.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If required fields are missing.
   */
  public function createFromYaml(array $data, ?string $filename = NULL): EbDefinition {
    // Validate flat format.
    $this->validateFlatFormat($data);

    $storage = $this->entityTypeManager->getStorage('eb_definition');

    // Extract metadata from YAML root.
    $id = $this->extractId($data, $filename);
    $label = $this->extractLabel($data, $id);
    $dependencies = $data['dependencies'] ?? [];

    // Read flat format arrays directly.
    $bundleDefinitions = $data['bundle_definitions'] ?? [];
    $fieldDefinitions = $data['field_definitions'] ?? [];
    $fieldGroupDefinitions = $data['field_group_definitions'] ?? [];
    $displayFieldDefinitions = $data['display_field_definitions'] ?? [];
    $menuDefinitions = $data['menu_definitions'] ?? [];

    /** @var \Drupal\eb\Entity\EbDefinition $definition */
    $definition = $storage->create([
      'id' => $id,
      'label' => $label,
      'description' => $data['description'] ?? '',
      'dependencies_data' => $dependencies,
      'bundle_definitions' => $bundleDefinitions,
      'field_definitions' => $fieldDefinitions,
      'field_group_definitions' => $fieldGroupDefinitions,
      'display_field_definitions' => $displayFieldDefinitions,
      'menu_definitions' => $menuDefinitions,
      'application_status' => 'draft',
    ]);

    return $definition;
  }

  /**
   * Checks if a definition with the given ID already exists.
   *
   * @param string $id
   *   The definition ID.
   *
   * @return bool
   *   TRUE if the definition exists.
   */
  public function definitionExists(string $id): bool {
    $storage = $this->entityTypeManager->getStorage('eb_definition');
    return $storage->load($id) !== NULL;
  }

  /**
   * Loads an existing definition by ID.
   *
   * @param string $id
   *   The definition ID.
   *
   * @return \Drupal\eb\Entity\EbDefinition|null
   *   The definition entity or NULL if not found.
   */
  public function loadDefinition(string $id): ?EbDefinition {
    $storage = $this->entityTypeManager->getStorage('eb_definition');
    $definition = $storage->load($id);
    return $definition instanceof EbDefinition ? $definition : NULL;
  }

  /**
   * Updates an existing definition with new flat YAML data.
   *
   * @param \Drupal\eb\Entity\EbDefinition $definition
   *   The existing definition entity.
   * @param array<mixed> $data
   *   The raw parsed YAML data in flat format.
   *
   * @return \Drupal\eb\Entity\EbDefinition
   *   The updated definition entity.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If required fields are missing.
   */
  public function updateFromYaml(EbDefinition $definition, array $data): EbDefinition {
    // Validate flat format.
    $this->validateFlatFormat($data);

    // Update label if provided.
    if (isset($data['label'])) {
      $definition->set('label', $data['label']);
    }

    // Update description if provided.
    if (isset($data['description'])) {
      $definition->setDescription($data['description']);
    }

    // Update dependencies.
    if (isset($data['dependencies'])) {
      $definition->setDependenciesData($data['dependencies']);
    }

    // Update all definitions from flat format.
    $definition->setBundleDefinitions($data['bundle_definitions'] ?? []);
    $definition->setFieldDefinitions($data['field_definitions'] ?? []);
    $definition->setFieldGroupDefinitions($data['field_group_definitions'] ?? []);
    $definition->setDisplayFieldDefinitions($data['display_field_definitions'] ?? []);
    $definition->setMenuDefinitions($data['menu_definitions'] ?? []);

    // Mark as outdated if it was previously applied.
    if ($definition->isApplied()) {
      $definition->markAsOutdated();
    }

    return $definition;
  }

  /**
   * Extracts the definition ID from YAML or derives from filename.
   *
   * @param array<string, mixed> $data
   *   The parsed YAML data.
   * @param string|null $filename
   *   The source filename.
   *
   * @return string
   *   The definition ID.
   */
  protected function extractId(array $data, ?string $filename): string {
    // Use explicit id from YAML if provided.
    if (!empty($data['id'])) {
      return $this->sanitizeMachineName($data['id']);
    }

    // Derive from filename.
    if ($filename) {
      return $this->deriveIdFromFilename($filename);
    }

    // Fallback to timestamp-based id.
    return 'definition_' . time();
  }

  /**
   * Extracts the label from YAML or derives from ID.
   *
   * @param array<string, mixed> $data
   *   The parsed YAML data.
   * @param string $id
   *   The definition ID.
   *
   * @return string
   *   The definition label.
   */
  protected function extractLabel(array $data, string $id): string {
    // Use explicit label from YAML if provided.
    if (!empty($data['label'])) {
      return $data['label'];
    }

    // Derive from id: convert underscores to spaces, apply title case.
    return ucwords(str_replace('_', ' ', $id));
  }

  /**
   * Validates flat YAML format.
   *
   * @param array<string, mixed> $data
   *   The parsed YAML data.
   *
   * @throws \Drupal\eb\Exception\ValidationException
   *   If validation fails.
   */
  protected function validateFlatFormat(array $data): void {
    $errors = [];

    // Reject hierarchical format (entities key).
    if (isset($data['entities'])) {
      throw new ValidationException(
        'Hierarchical YAML format (entities key) is no longer supported. Please use flat format with bundle_definitions, field_definitions, display_field_definitions.'
      );
    }

    // Validate bundle definitions.
    $bundleDefinitions = $data['bundle_definitions'] ?? [];
    foreach ($bundleDefinitions as $index => $bundle) {
      if (empty($bundle['entity_type'])) {
        $errors[] = "bundle_definitions[{$index}] missing required key: entity_type";
      }
      if (empty($bundle['bundle_id'])) {
        $errors[] = "bundle_definitions[{$index}] missing required key: bundle_id";
      }
      if (empty($bundle['label'])) {
        $errors[] = "bundle_definitions[{$index}] missing required key: label";
      }
    }

    // Validate field definitions.
    $fieldDefinitions = $data['field_definitions'] ?? [];
    foreach ($fieldDefinitions as $index => $field) {
      if (empty($field['entity_type'])) {
        $errors[] = "field_definitions[{$index}] missing required key: entity_type";
      }
      if (empty($field['bundle'])) {
        $errors[] = "field_definitions[{$index}] missing required key: bundle";
      }
      if (empty($field['field_name'])) {
        $errors[] = "field_definitions[{$index}] missing required key: field_name";
      }
      if (empty($field['field_type']) && empty($field['type'])) {
        $errors[] = "field_definitions[{$index}] missing required key: field_type";
      }
    }

    // Validate field group definitions.
    $fieldGroupDefinitions = $data['field_group_definitions'] ?? [];
    foreach ($fieldGroupDefinitions as $index => $group) {
      if (empty($group['entity_type'])) {
        $errors[] = "field_group_definitions[{$index}] missing required key: entity_type";
      }
      if (empty($group['bundle'])) {
        $errors[] = "field_group_definitions[{$index}] missing required key: bundle";
      }
      if (empty($group['display_type'])) {
        $errors[] = "field_group_definitions[{$index}] missing required key: display_type";
      }
      if (empty($group['mode'])) {
        $errors[] = "field_group_definitions[{$index}] missing required key: mode";
      }
      if (empty($group['group_name'])) {
        $errors[] = "field_group_definitions[{$index}] missing required key: group_name";
      }
    }

    // Validate display field definitions.
    $displayFieldDefinitions = $data['display_field_definitions'] ?? [];
    foreach ($displayFieldDefinitions as $index => $displayField) {
      if (empty($displayField['entity_type'])) {
        $errors[] = "display_field_definitions[{$index}] missing required key: entity_type";
      }
      if (empty($displayField['bundle'])) {
        $errors[] = "display_field_definitions[{$index}] missing required key: bundle";
      }
      if (empty($displayField['display_type'])) {
        $errors[] = "display_field_definitions[{$index}] missing required key: display_type";
      }
      if (empty($displayField['mode'])) {
        $errors[] = "display_field_definitions[{$index}] missing required key: mode";
      }
      if (empty($displayField['field_name'])) {
        $errors[] = "display_field_definitions[{$index}] missing required key: field_name";
      }
    }

    if (!empty($errors)) {
      throw new ValidationException(implode('; ', $errors));
    }
  }

  /**
   * Derives a definition ID from a filename.
   *
   * @param string $filename
   *   The filename.
   *
   * @return string
   *   The derived machine name.
   */
  protected function deriveIdFromFilename(string $filename): string {
    // Remove path if present.
    $basename = basename($filename);

    // Remove extension.
    $name = preg_replace('/\.(ya?ml|json|csv)$/i', '', $basename);
    if ($name === NULL) {
      $name = $basename;
    }

    return $this->sanitizeMachineName($name);
  }

  /**
   * Sanitizes a string to a valid machine name.
   *
   * @param string $name
   *   The input name.
   *
   * @return string
   *   The sanitized machine name.
   */
  protected function sanitizeMachineName(string $name): string {
    // Convert to lowercase.
    $name = strtolower($name);

    // Replace spaces and special chars with underscores.
    $name = preg_replace('/[^a-z0-9_]+/', '_', $name);
    if ($name === NULL) {
      $name = '';
    }

    // Remove leading/trailing underscores.
    $name = trim($name, '_');

    // Ensure it doesn't start with a number.
    if (preg_match('/^[0-9]/', $name)) {
      $name = 'def_' . $name;
    }

    // Truncate to max length.
    if (strlen($name) > self::MAX_ID_LENGTH) {
      $name = substr($name, 0, self::MAX_ID_LENGTH);
    }

    return $name ?: 'definition';
  }

}
