<?php

namespace Drupal\nodehive_admin_api\Controller;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\tool\Tool\ToolManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

/**
 * Controller for NodeHive admin tool API endpoints.
 */
class ToolsApiController extends ControllerBase {

  /**
   * The tool plugin manager.
   *
   * @var \Drupal\tool\Tool\ToolManager
   */
  protected ToolManager $toolManager;

  /**
   * Constructs a ToolsApiController object.
   *
   * @param \Drupal\tool\Tool\ToolManager $tool_manager
   *   The tool plugin manager.
   */
  public function __construct(ToolManager $tool_manager) {
    $this->toolManager = $tool_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('plugin.manager.tool')
    );
  }

  /**
   * Returns basic API metadata.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   A JSON response.
   */
  public function index(): JsonResponse {
    return new JsonResponse([
      'name' => 'NodeHive Admin API',
      'version' => '1.0',
      'endpoints' => [
        [
          'name' => 'List tools',
          'method' => 'GET',
          'path' => '/api/nodehive/admin/v1/tools',
        ],
        [
          'name' => 'Execute tool',
          'method' => 'POST',
          'path' => '/api/nodehive/admin/v1/tools/execute',
          'body' => [
            'tool_id' => 'display_message',
            'arguments' => [
              'message' => 'Hello from API',
              'type' => 'status',
            ],
          ],
        ],
      ],
    ]);
  }

  /**
   * Returns tools that are currently accessible for the user.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   A JSON response with available tools.
   */
  public function listTools(): CacheableJsonResponse {
    try {
      $definitions = $this->toolManager->getDefinitions();
      ksort($definitions);

      $tools = [];
      foreach ($definitions as $tool_id => $definition) {
        try {
          // Use static plugin definition metadata for listing so tools with
          // strict runtime input validation cannot break the endpoint.
          $tools[] = [
            'id' => $tool_id,
            'label' => (string) $definition->getLabel(),
            'description' => (string) $definition->getDescription(),
            'provider' => $definition->getProvider(),
            'operation' => $this->normalizeValue($definition->getOperation()),
            'destructive' => method_exists($definition, 'isDestructive') ? (bool) $definition->isDestructive() : FALSE,
            'input' => $this->normalizeContextDefinitions($definition->getInputDefinitions()),
            'output' => $this->normalizeContextDefinitions($definition->getOutputDefinitions()),
          ];
        }
        catch (\Throwable $tool_exception) {
          $this->getLogger('nodehive_admin_api')->warning(
            'Skipping tool @tool in list endpoint due to metadata error: @message',
            [
              '@tool' => $tool_id,
              '@message' => $tool_exception->getMessage(),
            ]
          );
        }
      }

      $response = new CacheableJsonResponse([
        'count' => count($tools),
        'tools' => $tools,
      ]);

      $cache_metadata = new CacheableMetadata();
      $cache_metadata->addCacheContexts(['user.permissions', 'user.roles']);
      $cache_metadata->addCacheTags(['tool_list']);
      $response->addCacheableDependency($cache_metadata);

      return $response;
    }
    catch (\Exception $e) {
      $this->getLogger('nodehive_admin_api')->error(
        'Error listing tools in admin API: @message',
        ['@message' => $e->getMessage()]
      );

      return new CacheableJsonResponse([
        'error' => $this->t('An error occurred while listing tools.'),
      ], 500);
    }
  }

  /**
   * Executes a tool by ID.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   The execution response.
   */
  public function executeTool(Request $request): JsonResponse {
    $payload = json_decode($request->getContent(), TRUE);
    if (!is_array($payload)) {
      return new JsonResponse([
        'error' => $this->t('Request body must be valid JSON.'),
      ], 400);
    }

    $tool_id = $payload['tool_id'] ?? '';
    $arguments = $payload['arguments'] ?? [];

    if (!is_string($tool_id) || $tool_id === '') {
      return new JsonResponse([
        'error' => $this->t('Missing required field: tool_id.'),
      ], 400);
    }
    if (!is_array($arguments)) {
      return new JsonResponse([
        'error' => $this->t('The arguments field must be an object/map.'),
      ], 400);
    }

    if (!$this->toolManager->hasDefinition($tool_id)) {
      return new JsonResponse([
        'error' => $this->t('Tool @tool_id was not found.', ['@tool_id' => $tool_id]),
      ], 404);
    }

    try {
      $tool = $this->toolManager->createInstance($tool_id);

      $input_definitions = $tool->getInputDefinitions();
      $unknown_arguments = array_diff(array_keys($arguments), array_keys($input_definitions));
      if (!empty($unknown_arguments)) {
        return new JsonResponse([
          'error' => $this->t('Unknown input argument(s) provided.'),
          'unknown_arguments' => array_values($unknown_arguments),
        ], 400);
      }

      $missing_required = [];
      foreach ($input_definitions as $name => $definition) {
        if ($definition->isRequired() && !array_key_exists($name, $arguments)) {
          $missing_required[] = $name;
        }
      }
      if (!empty($missing_required)) {
        return new JsonResponse([
          'error' => $this->t('Missing required input argument(s).'),
          'missing_required' => $missing_required,
        ], 400);
      }

      foreach ($arguments as $name => $value) {
        $tool->setInputValue($name, $value);
      }

      // Some tools evaluate access based on input values, so evaluate access
      // only after all requested inputs were populated.
      if (!$tool->access()) {
        return new JsonResponse([
          'error' => $this->t('Access denied for tool @tool_id.', ['@tool_id' => $tool_id]),
        ], 403);
      }

      $tool->execute();
      $result = $tool->getResult();

      $this->getLogger('nodehive_admin_api')->notice(
        'Tool @tool executed by UID @uid (success: @success).',
        [
          '@tool' => $tool_id,
          '@uid' => $this->currentUser()->id(),
          '@success' => $result->isSuccess() ? 'yes' : 'no',
        ]
      );

      return new JsonResponse([
        'tool_id' => $tool_id,
        'success' => $result->isSuccess(),
        'message' => (string) $tool->getResultMessage(),
        'output' => $this->normalizeValue($tool->getOutputValues()),
      ], $result->isSuccess() ? 200 : 422);
    }
    catch (\Exception $e) {
      $this->getLogger('nodehive_admin_api')->error(
        'Tool execution failed for @tool by UID @uid: @message',
        [
          '@tool' => $tool_id,
          '@uid' => $this->currentUser()->id(),
          '@message' => $e->getMessage(),
        ]
      );

      return new JsonResponse([
        'tool_id' => $tool_id,
        'success' => FALSE,
        'error' => $this->t('Tool execution failed: @message', ['@message' => $e->getMessage()]),
      ], 400);
    }
  }

  /**
   * Normalizes context definitions for API output.
   *
   * @param \Drupal\Core\Plugin\Context\ContextDefinitionInterface[] $definitions
   *   Context definitions.
   *
   * @return array
   *   Normalized context definition metadata.
   */
  protected function normalizeContextDefinitions(array $definitions): array {
    $normalized = [];
    foreach ($definitions as $name => $definition) {
      $item = [
        'type' => $definition->getDataType(),
        'label' => $this->normalizeValue($definition->getLabel()),
        'description' => $this->normalizeValue($definition->getDescription()),
        'required' => $definition->isRequired(),
        'multiple' => $definition->isMultiple(),
        'default' => $this->normalizeValue($definition->getDefaultValue()),
        'constraints' => $this->normalizeValue($definition->getConstraints()),
      ];

      // Map-like definitions can expose nested property definitions.
      if (method_exists($definition, 'getPropertyDefinitions')) {
        $properties = $definition->getPropertyDefinitions();
        if (is_array($properties) && !empty($properties)) {
          $item['properties'] = $this->normalizeContextDefinitions($properties);
        }
      }

      $normalized[$name] = $item;
    }
    return $normalized;
  }

  /**
   * Converts complex values to JSON-safe structures.
   *
   * @param mixed $value
   *   The value to normalize.
   *
   * @return mixed
   *   Normalized value.
   */
  protected function normalizeValue(mixed $value): mixed {
    if ($value === NULL || is_scalar($value)) {
      return $value;
    }

    if ($value instanceof EntityInterface) {
      return [
        'entity_type' => $value->getEntityTypeId(),
        'id' => $value->id(),
        'uuid' => $value->uuid(),
        'label' => $value->label(),
      ];
    }

    if ($value instanceof \BackedEnum) {
      return $value->value;
    }
    if ($value instanceof \UnitEnum) {
      return $value->name;
    }

    if ($value instanceof \JsonSerializable) {
      return $this->normalizeValue($value->jsonSerialize());
    }

    if ($value instanceof \Stringable) {
      return (string) $value;
    }

    if (is_array($value)) {
      $normalized = [];
      foreach ($value as $key => $item) {
        $normalized[$key] = $this->normalizeValue($item);
      }
      return $normalized;
    }

    if ($value instanceof \Traversable) {
      return $this->normalizeValue(iterator_to_array($value));
    }

    if (is_object($value)) {
      return $this->normalizeValue(get_object_vars($value));
    }

    return (string) $value;
  }

}
