<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Controller\Api;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\flowdrop\Controller\Api\ApiResponseTrait;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_node_type\Service\NodeMetadataResolver;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Controller for node type API endpoints.
 *
 * Node metadata (ports, config schema, defaults, executor plugin, visual type)
 * is derived from the live node type + plugin schema by
 * \Drupal\flowdrop_node_type\Service\NodeMetadataResolver, the single source of
 * truth shared with the workflow read path.
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see docs/development/flowdrop-node-processor.md
 */
final class NodesController extends ControllerBase {

  use ApiResponseTrait;

  public function __construct(
    protected readonly NodeMetadataResolver $metadataResolver,
  ) {}

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

  /**
   * Get all node types.
   *
   * Returns node types sorted alphabetically by category name in ascending
   * order (A-Z), then by node name within each category (also A-Z).
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with all node types.
   */
  public function getNodes(): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);
    // Add list cache tag - invalidated when any node type is added/removed.
    $cacheMetadata->addCacheTags(['config:flowdrop_node_type_list']);

    try {
      $storage = $this->entityTypeManager()->getStorage("flowdrop_node_type");
      $entities = $storage->loadMultiple();

      $nodes = [];
      foreach ($entities as $entity) {
        if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
          // Add cache tag for each individual entity.
          $cacheMetadata->addCacheableDependency($entity);

          $nodeMetadata = $this->metadataResolver->buildForNodeType($entity, $cacheMetadata);
          if ($nodeMetadata !== NULL) {
            $nodes[] = $nodeMetadata->toArray();
          }
        }
      }

      // Sort nodes alphabetically by category name (ascending A-Z),
      // then by node name within each category.
      usort($nodes, static function (array $a, array $b): int {
        $categoryA = $a["category"] ?? "";
        $categoryB = $b["category"] ?? "";
        $categoryComparison = strcasecmp($categoryA, $categoryB);

        // If categories are the same, sort by node name.
        if ($categoryComparison === 0) {
          $nameA = $a["name"] ?? "";
          $nameB = $b["name"] ?? "";
          return strcasecmp($nameA, $nameB);
        }

        return $categoryComparison;
      });

      return $this->cacheableSuccessResponse($nodes, $cacheMetadata);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to fetch node types: @message', [
        '@message' => $e->getMessage(),
      ]);
      return $this->cacheableErrorResponse('Failed to fetch node types', Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
    }
  }

  /**
   * Get node type metadata.
   *
   * @param string $node_type_id
   *   The node type ID.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with node type metadata.
   */
  public function getNodeMetadata(string $node_type_id): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);

    try {
      $entity = $this->entityTypeManager()->getStorage("flowdrop_node_type")->load($node_type_id);

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        // Cache the 404 response but invalidate if this entity is created.
        $cacheMetadata->addCacheTags(['config:flowdrop_node_type_list']);
        return $this->cacheableErrorResponse("Node type not found", Response::HTTP_NOT_FOUND, $cacheMetadata);
      }

      // Add cache dependency on the entity.
      $cacheMetadata->addCacheableDependency($entity);

      $nodeMetadata = $this->metadataResolver->buildForNodeType($entity, $cacheMetadata);
      if ($nodeMetadata === NULL) {
        return $this->cacheableErrorResponse("Failed to build node metadata", Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
      }

      return $this->cacheableSuccessResponse([
        "node_type_id" => $node_type_id,
        "metadata" => $nodeMetadata->toArray(),
      ], $cacheMetadata);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to fetch node metadata for @id: @message', [
        '@id' => $node_type_id,
        '@message' => $e->getMessage(),
      ]);
      return $this->cacheableErrorResponse('Failed to fetch node metadata', Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
    }
  }

  /**
   * Get node types by category.
   *
   * @param string $category
   *   The category to filter by (plural form from API).
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with filtered node types.
   */
  public function getNodesByCategory(string $category): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);
    // Add list cache tag - invalidated when any node type is added/removed.
    $cacheMetadata->addCacheTags(['config:flowdrop_node_type_list']);

    try {

      $storage = $this->entityTypeManager()->getStorage("flowdrop_node_type");
      $ids = $storage->getQuery()
        ->condition("category", $category)
        ->accessCheck(TRUE)
        ->execute();
      $entities = $storage->loadMultiple($ids);

      $nodes = [];
      foreach ($entities as $entity) {
        if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
          // Add cache dependency on each entity.
          $cacheMetadata->addCacheableDependency($entity);

          $nodeMetadata = $this->metadataResolver->buildForNodeType($entity, $cacheMetadata);
          if ($nodeMetadata !== NULL) {
            $nodes[] = $nodeMetadata->toArray();
          }
        }
      }

      return $this->cacheableSuccessResponse($nodes, $cacheMetadata);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to fetch nodes by category @category: @message', [
        '@category' => $category,
        '@message' => $e->getMessage(),
      ]);
      return $this->cacheableErrorResponse('Failed to fetch nodes by category', Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
    }
  }

  /**
   * Validate node type configuration.
   *
   * @param string $node_type_id
   *   The node type ID.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with validation results.
   */
  public function validateConfiguration(string $node_type_id, Request $request): JsonResponse {
    // Decode JSON outside try/catch so BadRequestHttpException propagates.
    $content = $this->decodeJsonRequest($request);

    try {
      $config = $content["config"] ?? [];

      $entity = $this->entityTypeManager()->getStorage("flowdrop_node_type")->load($node_type_id);

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        return $this->errorResponse("Node type not found", Response::HTTP_NOT_FOUND);
      }

      $executorPlugin = $entity->getExecutorPlugin();
      $parameterSchema = $this->metadataResolver->getPluginParameterSchema($executorPlugin);
      $entityParameters = $entity->getParameters();

      $errors = [];
      $properties = $parameterSchema["properties"] ?? [];

      foreach ($properties as $propertyName => $propertySchema) {
        $entityParam = $entityParameters[$propertyName] ?? [];

        // Flags from entity config with system defaults.
        $required = $entityParam["required"] ?? FALSE;
        $configurable = $entityParam["configurable"] ?? FALSE;

        // Only validate configurable parameters in config.
        if ($configurable && $required) {
          if (!array_key_exists($propertyName, $config)) {
            $errors[] = "Required property '{$propertyName}' is missing";
          }
        }

        if (array_key_exists($propertyName, $config)) {
          $value = $config[$propertyName];
          $expectedType = $propertySchema["type"] ?? "mixed";

          if (!$this->validateValueType($value, $expectedType)) {
            $errors[] = "Property '{$propertyName}' must be of type '{$expectedType}'";
          }
        }
      }

      return $this->successResponse([
        "node_type_id" => $node_type_id,
        "validation_result" => [
          "valid" => empty($errors),
          "errors" => $errors,
        ],
      ]);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to validate configuration for @id: @message', [
        '@id' => $node_type_id,
        '@message' => $e->getMessage(),
      ]);
      return $this->errorResponse('Failed to validate configuration', Response::HTTP_INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Validate value type.
   *
   * @param mixed $value
   *   The value to validate.
   * @param string $expectedType
   *   The expected type.
   *
   * @return bool
   *   TRUE if the value matches the expected type.
   */
  protected function validateValueType(mixed $value, string $expectedType): bool {
    return match ($expectedType) {
      "string" => is_string($value),
      "integer" => is_int($value),
      "number" => is_float($value) || is_int($value),
      "boolean" => is_bool($value),
      "array" => is_array($value),
      "object" => is_array($value) || is_object($value),
      "mixed" => TRUE,
      default => TRUE,
    };
  }

  /**
   * Get port configuration for the FlowDrop system.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with port configuration.
   */
  public function getPortConfiguration(): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);
    $cacheMetadata->addCacheTags(['config:flowdrop_node_type.port_config']);

    $config = $this->config("flowdrop_node_type.port_config");

    $defaultConfig = [
      "version" => "1.0.0",
      "defaultDataType" => "string",
      "dataTypes" => [
        [
          "id" => "mixed",
          "name" => "Mixed Data",
          "description" => "Mixed data that can be bool, int, string, ...",
          "color" => "var(--fd-node-teal)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
        [
          "id" => "string",
          "name" => "String",
          "description" => "Text data",
          "color" => "var(--fd-node-emerald)",
          "category" => "basic",
          "enabled" => TRUE,
        ],
        [
          "id" => "number",
          "name" => "Number",
          "description" => "Numeric data",
          "color" => "var(--fd-node-blue)",
          "category" => "numeric",
          "enabled" => TRUE,
        ],
        [
          "id" => "boolean",
          "name" => "Boolean",
          "description" => "True/false values",
          "color" => "var(--fd-node-purple)",
          "category" => "logical",
          "enabled" => TRUE,
        ],
        [
          "id" => "array",
          "name" => "Array",
          "description" => "Ordered list of items",
          "color" => "var(--fd-node-amber)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "string[]",
          "name" => "String Array",
          "description" => "Array of strings",
          "color" => "var(--fd-node-emerald)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "number[]",
          "name" => "Number Array",
          "description" => "Array of numbers",
          "color" => "var(--fd-node-blue)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "boolean[]",
          "name" => "Boolean Array",
          "description" => "Array of true/false values",
          "color" => "var(--fd-node-purple)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "json[]",
          "name" => "JSON Array",
          "description" => "Array of JSON objects",
          "color" => "var(--fd-node-orange)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "file[]",
          "name" => "File Array",
          "description" => "Array of files",
          "color" => "var(--fd-node-red)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "image[]",
          "name" => "Image Array",
          "description" => "Array of images",
          "color" => "var(--fd-node-pink)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "json",
          "name" => "JSON",
          "description" => "JSON structured data",
          "color" => "var(--fd-node-orange)",
          "category" => "complex",
          "enabled" => TRUE,
        ],
        [
          "id" => "file",
          "name" => "File",
          "description" => "File data",
          "color" => "var(--fd-node-red)",
          "category" => "file",
          "enabled" => TRUE,
        ],
        [
          "id" => "image",
          "name" => "Image",
          "description" => "Image data",
          "color" => "var(--fd-node-pink)",
          "category" => "media",
          "enabled" => TRUE,
        ],
        [
          "id" => "audio",
          "name" => "Audio",
          "description" => "Audio data",
          "color" => "var(--fd-node-indigo)",
          "category" => "media",
          "enabled" => TRUE,
        ],
        [
          "id" => "video",
          "name" => "Video",
          "description" => "Video data",
          "color" => "var(--fd-node-teal)",
          "category" => "media",
          "enabled" => TRUE,
        ],
        [
          "id" => "url",
          "name" => "URL",
          "description" => "Web address",
          "color" => "var(--fd-node-cyan)",
          "category" => "special",
          "enabled" => TRUE,
        ],
        [
          "id" => "email",
          "name" => "Email",
          "description" => "Email address",
          "color" => "var(--fd-node-cyan)",
          "category" => "special",
          "enabled" => TRUE,
        ],
        [
          "id" => "date",
          "name" => "Date",
          "description" => "Date value",
          "color" => "var(--fd-node-lime)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
        [
          "id" => "datetime",
          "name" => "DateTime",
          "description" => "Date and time value",
          "color" => "var(--fd-node-lime)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
        [
          "id" => "time",
          "name" => "Time",
          "description" => "Time value",
          "color" => "var(--fd-node-lime)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
      ],
      "compatibilityRules" => [],
    ];

    $portConfig = $config->get("config") ?: $defaultConfig;

    return $this->cacheableSuccessResponse($portConfig, $cacheMetadata);
  }

}
