<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Controller\Api;

use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\FlowDropNodeProcessorInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop\Utility\NodeAnchor;
use Drupal\flowdrop_node_type\Service\NodeMetadataResolver;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Serves the GET /api/flowdrop/workflows/{id}/schema endpoint.
 *
 * Returns the workflow's stored FlowDrop Schema snapshot plus the schema
 * version. ETag is the schema_version itself; clients can issue
 * If-None-Match for cheap 304s. ?annotated=1 re-attaches live title /
 * description from source plugins for UI consumers.
 *
 * @internal
 */
final class WorkflowSchemaController extends ControllerBase {

  public function __construct(
    private readonly FlowDropNodeProcessorPluginManager $pluginManager,
    private readonly NodeMetadataResolver $metadataResolver,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('flowdrop.node_processor_plugin_manager'),
      $container->get('flowdrop_node_type.metadata_resolver'),
    );
  }

  /**
   * Endpoint handler.
   */
  public function getSchema(string $id, Request $request): JsonResponse {
    $workflow = $this->entityTypeManager()
      ->getStorage('flowdrop_workflow')
      ->load($id);

    if (!$workflow instanceof WorkflowDefinitionInterface) {
      return new JsonResponse(['error' => 'Workflow not found.'], Response::HTTP_NOT_FOUND);
    }

    $version = $workflow->getSchemaVersion();
    $etag = '"' . $version . '"';

    $ifNoneMatch = $request->headers->get('If-None-Match');
    if ($ifNoneMatch !== NULL && $ifNoneMatch === $etag) {
      $notModified = new JsonResponse(NULL, Response::HTTP_NOT_MODIFIED);
      $notModified->headers->set('ETag', $etag);
      return $notModified;
    }

    $annotated = $request->query->getBoolean('annotated');
    $parameterSchema = $workflow->getParameterSchema();
    $outputSchema = $workflow->getOutputSchema();

    if ($annotated) {
      $parameterSchema = $this->reattachAnnotations($workflow, $parameterSchema, $workflow->getInputPorts(), 'input');
      $outputSchema = $this->reattachAnnotations($workflow, $outputSchema, $workflow->getOutputPorts(), 'output');
    }

    $payload = [
      'schema_version' => $version,
      'parameter_schema' => $parameterSchema,
      'output_schema' => $outputSchema,
    ];

    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheTags(['config:flowdrop_workflow.flowdrop_workflow.' . $id]);
    $cacheMetadata->addCacheContexts(['url.query_args:annotated']);
    if ($annotated) {
      $cacheMetadata->addCacheContexts(['languages:language_interface']);
    }

    $response = new CacheableJsonResponse($payload, Response::HTTP_OK);
    $response->addCacheableDependency($cacheMetadata);
    $response->headers->set('ETag', $etag);
    if ($annotated) {
      $response->headers->set('Cache-Control', 'no-cache');
    }
    else {
      $response->headers->set('Cache-Control', 'public, max-age=60');
    }
    return $response;
  }

  /**
   * Re-attaches annotation keys (title, description) from live source plugins.
   *
   * @param \Drupal\flowdrop_workflow\WorkflowDefinitionInterface $workflow
   *   The owning workflow.
   * @param array<string, mixed>|null $snapshot
   *   The stored schema snapshot to enrich.
   * @param list<array{name: string, node_id: string, port: string}> $ports
   *   The exposure entries to consult for live annotations.
   * @param string $direction
   *   The direction (input or output).
   *
   * @return array<string, mixed>|null
   *   The annotated snapshot, or NULL when the input was NULL.
   */
  private function reattachAnnotations(
    WorkflowDefinitionInterface $workflow,
    ?array $snapshot,
    array $ports,
    string $direction,
  ): ?array {
    if ($snapshot === NULL) {
      return NULL;
    }

    $nodesById = [];
    foreach ($workflow->getNodes() as $node) {
      $nodeId = $node['id'] ?? NULL;
      if (is_string($nodeId) && $nodeId !== '') {
        $nodesById[$nodeId] = $node;
      }
    }

    $properties = $snapshot['properties'] ?? [];
    foreach ($ports as $entry) {
      $name = $entry['name'] ?? '';
      $nodeId = $entry['node_id'] ?? '';
      $port = $entry['port'] ?? '';
      if ($name === '' || $nodeId === '' || $port === '' || !isset($properties[$name], $nodesById[$nodeId])) {
        continue;
      }

      $nodeTypeId = NodeAnchor::read($nodesById[$nodeId]);
      $pluginId = (is_string($nodeTypeId) && $nodeTypeId !== ''
        ? $this->metadataResolver->resolveExecutorPlugin($nodeTypeId)
        : NULL) ?? '';
      if ($pluginId === '') {
        continue;
      }

      try {
        $plugin = $this->pluginManager->createInstance($pluginId);
      }
      catch (PluginException) {
        continue;
      }

      if (!$plugin instanceof FlowDropNodeProcessorInterface) {
        continue;
      }

      $sourceSchema = $direction === 'input' ? $plugin->getParameterSchema() : $plugin->getOutputSchema();
      $sourcePort = $sourceSchema['properties'][$port] ?? NULL;
      if (!is_array($sourcePort)) {
        continue;
      }

      foreach (['title', 'description'] as $annotation) {
        if (isset($sourcePort[$annotation])) {
          $properties[$name][$annotation] = (string) $sourcePort[$annotation];
        }
      }
    }
    $snapshot['properties'] = $properties;
    return $snapshot;
  }

}
