<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ConfigEditProviderInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_category\FlowDropNodeCategoryInterface;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;

/**
 * Service for managing flowdrop node types.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class FlowDropNodeTypeManager {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The logger factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * The node executor plugin manager.
   *
   * @var \Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager
   */
  protected $pluginManager;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Constructs a FlowDropNodeTypeManager.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager $plugin_manager
   *   The node processor plugin manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    LoggerChannelFactoryInterface $logger_factory,
    FlowDropNodeProcessorPluginManager $plugin_manager,
    ConfigFactoryInterface $config_factory,
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->loggerFactory = $logger_factory;
    $this->pluginManager = $plugin_manager;
    $this->configFactory = $config_factory;
  }

  /**
   * Get all node types.
   *
   * @return array
   *   Array of node type definitions.
   */
  public function getAllNodeTypes(): array {
    $node_types = [];
    $entities = $this->entityTypeManager->getStorage('flowdrop_node_type')->loadMultiple();

    foreach ($entities as $entity) {
      if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
        $node_data = $this->buildNodeData($entity);
        if ($node_data) {
          $node_types[] = $node_data;
        }
      }
    }

    return $node_types;
  }

  /**
   * Get node types by category.
   *
   * @param string $category
   *   The category to filter by.
   *
   * @return array
   *   Array of node type definitions in the specified category.
   */
  public function getNodeTypesByCategory(string $category): array {
    $node_types = [];
    $entities = $this->entityTypeManager->getStorage('flowdrop_node_type')
      ->loadByProperties(['category' => $category]);

    foreach ($entities as $entity) {
      if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
        $node_data = $this->buildNodeData($entity);
        if ($node_data) {
          $node_types[] = $node_data;
        }
      }
    }

    return $node_types;
  }

  /**
   * Get a specific node type by ID.
   *
   * @param string $id
   *   The node type ID.
   *
   * @return array|null
   *   The node type definition or NULL if not found.
   */
  public function getNodeType(string $id): ?array {
    $entity = $this->entityTypeManager->getStorage('flowdrop_node_type')->load($id);

    if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
      return $this->buildNodeData($entity);
    }

    return NULL;
  }

  /**
   * Create a node type from a definition array.
   *
   * @param array $definition
   *   The node type definition.
   *
   * @return \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface|null
   *   The created node type entity or NULL if creation failed.
   */
  public function createNodeType(array $definition): ?FlowDropNodeTypeInterface {
    try {
      $executorPlugin = $definition["executor_plugin"] ?? "";
      $pluginVersion = $this->getPluginVersion($executorPlugin);

      // Note: Color is not stored on node types - it is inherited from the
      // node's category. The color field was intentionally removed.
      $values = [
        "id" => $definition["id"],
        "label" => $definition["name"],
        "description" => $definition["description"] ?? "",
        "category" => $definition["category"] ?? "processing",
        "icon" => $definition["icon"] ?? "mdi:cog",
        "plugin_version" => $pluginVersion,
        "enabled" => $definition["enabled"] ?? TRUE,
        "parameters" => $definition["parameterSchema"] ?? [],
        "tags" => $definition["tags"] ?? [],
        "executor_plugin" => $executorPlugin,
      ];

      $entity = $this->entityTypeManager->getStorage('flowdrop_node_type')->create($values);
      $entity->save();

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        $this->loggerFactory->get('flowdrop_node_type')->error('Error while saving node type @id', ['@id' => $definition['id']]);
        return NULL;
      }

      $this->loggerFactory->get('flowdrop_node_type')->info('Created node type @id', ['@id' => $definition['id']]);

      return $entity;

    }
    catch (\Exception $e) {
      $this->loggerFactory->get('flowdrop_node_type')->error('Failed to create node type @id: @error', [
        '@id' => $definition['id'] ?? 'unknown',
        '@error' => $e->getMessage(),
      ]);
      return NULL;
    }
  }

  /**
   * Update a node type from a definition array.
   *
   * @param string $id
   *   The node type ID.
   * @param array $definition
   *   The updated node type definition.
   *
   * @return \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface|null
   *   The updated node type entity or NULL if update failed.
   */
  public function updateNodeType(string $id, array $definition): ?FlowDropNodeTypeInterface {
    try {
      $entity = $this->entityTypeManager->getStorage('flowdrop_node_type')->load($id);

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        $this->loggerFactory->get('flowdrop_node_type')->error('Node type @id not found for update', ['@id' => $id]);
        return NULL;
      }

      // Note: Color is not stored on node types - it is inherited from the
      // node's category. The color field was intentionally removed.
      $entity->setLabel($definition["name"] ?? $entity->label());
      $entity->setDescription($definition["description"] ?? $entity->getDescription());
      $entity->setCategory($definition["category"] ?? $entity->getCategory());
      $entity->setIcon($definition["icon"] ?? $entity->getIcon());
      $entity->setEnabled($definition["enabled"] ?? $entity->isEnabled());
      $entity->setParameters($definition["parameterSchema"] ?? $entity->getParameters());
      $entity->setTags($definition["tags"] ?? $entity->getTags());

      // Update executor plugin and capture its version.
      $executorPlugin = $definition["executor_plugin"] ?? $entity->getExecutorPlugin();
      $entity->setExecutorPlugin($executorPlugin);
      $entity->setPluginVersion($this->getPluginVersion($executorPlugin));

      $entity->save();

      $this->loggerFactory->get('flowdrop_node_type')->info('Updated node type @id', ['@id' => $id]);

      return $entity;

    }
    catch (\Exception $e) {
      $this->loggerFactory->get('flowdrop_node_type')->error('Failed to update node type @id: @error', [
        '@id' => $id,
        '@error' => $e->getMessage(),
      ]);
      return NULL;
    }
  }

  /**
   * Delete a node type.
   *
   * @param string $id
   *   The node type ID.
   *
   * @return bool
   *   TRUE if deletion was successful.
   */
  public function deleteNodeType(string $id): bool {
    try {
      $entity = $this->entityTypeManager->getStorage('flowdrop_node_type')->load($id);

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        $this->loggerFactory->get('flowdrop_node_type')->error('Node type @id not found for deletion', ['@id' => $id]);
        return FALSE;
      }

      $entity->delete();

      $this->loggerFactory->get('flowdrop_node_type')->info('Deleted node type @id', ['@id' => $id]);

      return TRUE;

    }
    catch (\Exception $e) {
      $this->loggerFactory->get('flowdrop_node_type')->error('Failed to delete node type @id: @error', [
        '@id' => $id,
        '@error' => $e->getMessage(),
      ]);
      return FALSE;
    }
  }

  /**
   * Import node types from the NodeTypeService mapping.
   *
   * @param array $mapping
   *   The node mapping from NodeTypeService.
   *
   * @return array
   *   Array of created/updated node types.
   */
  public function importFromMapping(array $mapping): array {
    $results = [];

    foreach ($mapping as $node_type => $definition) {
      $node_id = $definition['id'] ?? strtolower(str_replace(' ', '-', $node_type));

      // Check if node type already exists.
      $existing = $this->entityTypeManager->getStorage('flowdrop_node_type')->load($node_id);

      if ($existing) {
        $entity = $this->updateNodeType($node_id, $definition);
        $results[] = [
          'id' => $node_id,
          'action' => 'updated',
          'success' => $entity !== NULL,
        ];
      }
      else {
        $entity = $this->createNodeType($definition);
        $results[] = [
          'id' => $node_id,
          'action' => 'created',
          'success' => $entity !== NULL,
        ];
      }
    }

    return $results;
  }

  /**
   * Get available visual types for node rendering.
   *
   * Returns the visual types configured by site administrators.
   * These types determine how nodes appear in the workflow editor.
   *
   * @return array<string, string>
   *   Array of visual type options with machine name as key and label as value.
   */
  public function getAvailableVisualTypes(): array {
    $options = [];
    $config = $this->configFactory->get("flowdrop_node_type.settings");
    $visualTypes = $config->get("visual_types") ?? [];

    foreach ($visualTypes as $key => $typeConfig) {
      $label = $typeConfig["label"] ?? $key;
      $options[$key] = $label;
    }

    // Provide default if no types are configured.
    if (empty($options)) {
      $options = [
        "default" => "Default",
        "gateway" => "Gateway",
        "simple" => "Simple",
        "square" => "Square",
      ];
    }

    return $options;
  }

  /**
   * Get available categories.
   *
   * @return array
   *   Array of available categories with ID as key and label as value.
   */
  public function getAvailableCategories(): array {
    $categories = [];

    try {
      $category_storage = $this->entityTypeManager->getStorage('flowdrop_node_category');
      $category_entities = $category_storage->loadMultiple();

      foreach ($category_entities as $category) {
        if ($category instanceof FlowDropNodeCategoryInterface && $category->status()) {
          $categories[$category->id()] = $category->label();
        }
      }
    }
    catch (\Exception $e) {
      // Fallback to category from node types if category module
      // is not available.
      $node_type_entities = $this->entityTypeManager->getStorage('flowdrop_node_type')->loadMultiple();

      foreach ($node_type_entities as $entity) {
        if (!$entity instanceof FlowDropNodeTypeInterface) {
          continue;
        }
        $category = $entity->getCategory();
        if (!in_array($category, $categories)) {
          $categories[$category] = $category;
        }
      }
    }

    asort($categories);
    return $categories;
  }

  /**
   * Search node types by tags.
   *
   * @param array $tags
   *   Array of tags to search for.
   *
   * @return array
   *   Array of matching node type definitions.
   */
  public function searchByTags(array $tags): array {
    $node_types = [];
    $entities = $this->entityTypeManager->getStorage('flowdrop_node_type')->loadMultiple();

    foreach ($entities as $entity) {
      if (!$entity instanceof FlowDropNodeTypeInterface) {
        continue;
      }
      if (!$entity->isEnabled()) {
        continue;
      }

      $entity_tags = $entity->getTags();
      foreach ($tags as $tag) {
        if (in_array($tag, $entity_tags)) {
          $node_types[] = $entity->toNodeDefinition();
          break;
        }
      }
    }

    return $node_types;
  }

  /**
   * Build complete node data by merging entity and plugin data.
   *
   * @param \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface $entity
   *   The node type entity.
   *
   * @return array|null
   *   The complete node data or NULL if plugin not found.
   */
  protected function buildNodeData(FlowDropNodeTypeInterface $entity): ?array {
    try {
      $executor_plugin = $entity->getExecutorPlugin();

      // Get plugin data if available.
      $plugin_data = [];
      $inputs = [];
      $outputs = [];

      if ($executor_plugin && $this->pluginManager->hasDefinition($executor_plugin)) {
        $plugin = $this->pluginManager->createInstance($executor_plugin);

        // Use unified parameter schema.
        $parameter_schema = $plugin->getParameterSchema();
        $output_schema = $plugin->getOutputSchema();

        $plugin_data = [
          'parameterSchema' => $parameter_schema,
          'output_schema' => $output_schema,
        ];

        // Transform parameter schema to inputs array
        // (all parameters as inputs).
        if (!empty($parameter_schema)) {
          $inputs = $this->transformSchemaToPorts($parameter_schema, 'input');
        }

        // Transform output schema to outputs array.
        if (!empty($output_schema)) {
          $outputs = $this->transformSchemaToPorts($output_schema, 'output');
        }
      }

      // Check if the plugin provides configEdit metadata for dynamic
      // configuration. This allows plugins to specify how their configuration
      // UI should be loaded (dynamic schema from API, external edit links,
      // or both). Uses ConfigEditProviderInterface for type-safe interface
      // checking.
      $configEdit = NULL;
      if (isset($plugin) && $plugin instanceof ConfigEditProviderInterface) {
        $configEdit = $plugin->getConfigEdit()->toArray();
      }

      // Build the complete node data.
      $node_data = [
        'id' => $entity->id(),
        'name' => $entity->label(),
        'description' => $entity->getDescription(),
        'category' => $this->transformCategoryToPlural($entity->getCategory()),
        'icon' => $entity->getIcon(),
        "plugin_version" => $entity->getPluginVersion(),
        'enabled' => $entity->isEnabled(),
        'tags' => $entity->getTags(),
        'executor_plugin' => $executor_plugin,
        'inputs' => $inputs,
        'outputs' => $outputs,
      ];

      // Include configEdit if the plugin provides it.
      if ($configEdit !== NULL) {
        $node_data["configEdit"] = $configEdit;
      }

      // Merge plugin data (plugin data takes precedence).
      if (!empty($plugin_data)) {
        $node_data = array_merge($node_data, $plugin_data);
      }

      return $node_data;

    }
    catch (\Exception $e) {
      $this->loggerFactory->get('flowdrop_node_type')->error('Failed to build node data for @id: @error', [
        '@id' => $entity->id(),
        '@error' => $e->getMessage(),
      ]);
      return NULL;
    }
  }

  /**
   * Transform schema to NodePort array format.
   *
   * @param array $schema
   *   The schema array.
   * @param string $port_type
   *   The port type ('input' or 'output').
   *
   * @return array
   *   Array of NodePort objects.
   */
  protected function transformSchemaToPorts(array $schema, string $port_type): array {
    $ports = [];

    // Handle JSON schema format (with properties)
    if (isset($schema['properties'])) {
      foreach ($schema['properties'] as $property_name => $property_schema) {
        $port = [
          'id' => $property_name,
          'name' => $property_schema['title'] ?? $property_name,
          'type' => $port_type,
          'dataType' => $this->mapSchemaTypeToDataType($property_schema['type'] ?? 'string'),
          'required' => in_array($property_name, $schema['required'] ?? []),
          'description' => $property_schema['description'] ?? '',
        ];

        if (isset($property_schema['default'])) {
          $port['defaultValue'] = $property_schema['default'];
        }

        $ports[] = $port;
      }
    }
    // Handle direct object format (for output schemas)
    else {
      foreach ($schema as $property_name => $property_schema) {
        $port = [
          'id' => $property_name,
          'name' => $property_schema['title'] ?? $property_name,
          'type' => $port_type,
          'dataType' => $this->mapSchemaTypeToDataType($property_schema['type'] ?? 'string'),
        // Outputs are typically not required.
          'required' => FALSE,
          'description' => $property_schema['description'] ?? '',
        ];

        if (isset($property_schema['default'])) {
          $port['defaultValue'] = $property_schema['default'];
        }

        $ports[] = $port;
      }
    }

    return $ports;
  }

  /**
   * Map JSON schema types to NodeDataType.
   *
   * @param string $schema_type
   *   The JSON schema type.
   *
   * @return string
   *   The NodeDataType.
   */
  protected function mapSchemaTypeToDataType(string $schema_type): string {
    $type_mapping = [
      'string' => 'string',
      'number' => 'number',
      'integer' => 'number',
      'boolean' => 'boolean',
      'array' => 'array',
      'object' => 'json',
    ];

    return $type_mapping[$schema_type] ?? 'string';
  }

  /**
   * Get the version of a plugin from its definition.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return string
   *   The plugin version, or empty string if not found.
   */
  protected function getPluginVersion(string $pluginId): string {
    if (empty($pluginId)) {
      return "";
    }

    try {
      $definition = $this->pluginManager->getDefinition($pluginId, FALSE);
      if ($definition !== NULL && isset($definition["version"])) {
        return (string) $definition["version"];
      }
    }
    catch (\Exception $e) {
      // Silently fail - version is informational only.
    }

    return "";
  }

  /**
   * Transform singular category to plural form for API compatibility.
   *
   * @param string $category
   *   The singular category.
   *
   * @return string
   *   The plural category.
   */
  protected function transformCategoryToPlural(string $category): string {
    $category_mapping = [
      'input' => 'inputs',
      'output' => 'outputs',
      'model' => 'models',
      'prompt' => 'prompts',
      'processing' => 'processing',
      'logic' => 'logic',
      'data' => 'data',
      'helper' => 'helpers',
      'tool' => 'tools',
      'vectorstore' => 'vectorstores',
      'embedding' => 'embeddings',
      'memory' => 'memories',
      'agent' => 'agents',
      'bundle' => 'bundles',
    ];

    return $category_mapping[$category] ?? $category;
  }

}
