<?php

declare(strict_types=1);

namespace Drupal\flowdrop_stategraph\Plugin\FlowDropNodeProcessor;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationError;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * ForEach node processor for iterating over arrays.
 *
 * This node provides a complete loop solution that:
 * - Initializes iteration over an input array on first execution.
 * - Provides the current item and index on each iteration.
 * - Tracks loop state (has_more, is_first, is_last).
 * - Collects results from each iteration.
 * - Outputs all collected results when iteration completes.
 *
 * The node uses the graph state's iterator to maintain loop position
 * across multiple executions, enabling true looping behavior in
 * StateGraph workflows.
 *
 * Inputs:
 * - items: The array to iterate over (only needed on first call).
 * - __iterator__: Iterator state injected from graph state.
 * - item_result: Optional result from processing the current item.
 * - loop_back: Special loopback port that triggers the next iteration.
 *   This port accepts any data type and signals that the loop body has
 *   completed. The data passed is collected as the item_result. Edges
 *   connecting to this port should be marked as loopback edges (excluded
 *   from cycle detection, rendered as dotted lines in UI).
 *
 * Outputs:
 * - current_item: The current item being processed.
 * - current_index: Current position in the array (0-based).
 * - is_first: TRUE if this is the first item.
 * - is_last: TRUE if this is the last item.
 * - has_more: TRUE if more items remain.
 * - total_count: Total number of items.
 * - loop_active: TRUE if loop is still processing.
 * - collected_results: All results collected (only on completion).
 * - state_update: State update to apply to graph state.
 *
 * @see \Drupal\flowdrop_orchestration\DTO\Snapshot\IteratorState
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "foreach",
  label: new TranslatableMarkup("ForEach Loop"),
  description: "Iterates over an array, processing each item in sequence",
  version: "1.0.0"
)]
class ForEachNode extends AbstractFlowDropNodeProcessor {

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ): static {
    return new static($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    $errors = [];

    // Must have either items (for initialization) or __iterator__ (for
    // continuation).
    $hasItems = isset($params["items"]) && is_array($params["items"]);
    $hasIterator = isset($params["__iterator__"]) && is_array($params["__iterator__"]);

    if (!$hasItems && !$hasIterator) {
      $errors[] = new ValidationError('items', "ForEach requires either 'items' array for initialization "
        . "or '__iterator__' state for continuation.");
    }

    if (!empty($errors)) {
      return ValidationResult::failure($errors);
    }

    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $items = $params->get("items");
    $iteratorData = $params->get("__iterator__");
    $itemResult = $params->get("item_result");
    $loopBack = $params->get("loop_back");

    // The loop_back port is a special loopback input that triggers the next
    // iteration. When data arrives on this port, it's treated as the item
    // result from the previous iteration. This allows creating visual
    // loopback edges in the workflow without causing cycle detection issues.
    if ($loopBack !== NULL && $itemResult === NULL) {
      $itemResult = $loopBack;
    }

    // Determine if we're initializing or continuing.
    $isInitializing = $iteratorData === NULL || empty($iteratorData);

    if ($isInitializing) {
      // Initialize new iterator from items.
      return $this->initializeLoop($items);
    }

    // Continue existing iteration.
    return $this->continueLoop($iteratorData, $itemResult);
  }

  /**
   * Initializes a new loop over the given items.
   *
   * @param mixed $items
   *   The items to iterate over.
   *
   * @return array<string, mixed>
   *   The output array with initial loop state.
   */
  private function initializeLoop(mixed $items): array {
    // Ensure items is an array.
    if (!is_array($items)) {
      $items = $items !== NULL ? [$items] : [];
    }

    // Re-index array to ensure sequential keys.
    $items = array_values($items);

    // Handle empty array case.
    if (empty($items)) {
      return [
        "current_item" => NULL,
        "current_index" => 0,
        "is_first" => TRUE,
        "is_last" => TRUE,
        "has_more" => FALSE,
        "total_count" => 0,
        "loop_active" => FALSE,
        "collected_results" => [],
        "state_update" => [
          "iterator" => NULL,
          "data" => [
            "foreach_completed" => TRUE,
            "foreach_results" => [],
          ],
        ],
      ];
    }

    // Create iterator state.
    $iterator = IteratorState::fromItems($items);

    return [
      "current_item" => $iterator->currentItem,
      "current_index" => $iterator->currentIndex,
      "is_first" => TRUE,
      "is_last" => $iterator->getTotalCount() === 1,
      "has_more" => $iterator->hasMore,
      "total_count" => $iterator->getTotalCount(),
      "loop_active" => TRUE,
      "state_update" => [
        "iterator" => $iterator->toArray(),
        "data" => [
          "foreach_active" => TRUE,
          "foreach_completed" => FALSE,
        ],
      ],
    ];
  }

  /**
   * Continues an existing loop iteration.
   *
   * @param array<string, mixed> $iteratorData
   *   The current iterator state data.
   * @param mixed $itemResult
   *   Optional result from processing the previous item.
   *
   * @return array<string, mixed>
   *   The output array with next loop state.
   */
  private function continueLoop(array $iteratorData, mixed $itemResult): array {
    // Reconstruct iterator state.
    $iterator = IteratorState::fromArray($iteratorData);

    // Add result from previous iteration if provided.
    if ($itemResult !== NULL) {
      $iterator = $iterator->addResult($itemResult);
    }

    // Check if we've exhausted all items.
    if ($iterator->isExhausted() || !$iterator->hasMore) {
      // Loop complete - return all collected results.
      return [
        "current_item" => NULL,
        "current_index" => $iterator->currentIndex,
        "is_first" => FALSE,
        "is_last" => TRUE,
        "has_more" => FALSE,
        "total_count" => $iterator->getTotalCount(),
        "loop_active" => FALSE,
        "collected_results" => $iterator->results,
        "state_update" => [
          "iterator" => NULL,
          "data" => [
            "foreach_active" => FALSE,
            "foreach_completed" => TRUE,
            "foreach_results" => $iterator->results,
          ],
        ],
      ];
    }

    // Advance to next item.
    $iterator = $iterator->advance();

    $currentIndex = $iterator->currentIndex;
    $totalCount = $iterator->getTotalCount();

    return [
      "current_item" => $iterator->currentItem,
      "current_index" => $currentIndex,
      "is_first" => FALSE,
      "is_last" => $currentIndex === $totalCount - 1,
      "has_more" => $iterator->hasMore,
      "total_count" => $totalCount,
      "loop_active" => TRUE,
      "state_update" => [
        "iterator" => $iterator->toArray(),
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "items" => [
          "type" => "array",
          "description" => "The array to iterate over (required for initialization)",
        ],
        "__iterator__" => [
          "type" => "object",
          "description" => "Iterator state (automatically injected from graph state)",
        ],
        "item_result" => [
          "type" => "mixed",
          "description" => "Result from processing the current item (collected for final output)",
        ],
        "loop_back" => [
          "type" => "any",
          "description" => "Loopback trigger - connect loop body output here to continue iteration",
          "is_loopback" => TRUE,
          "loopback_metadata" => [
            "edge_style" => "dotted",
            "skip_cycle_detection" => TRUE,
            "label" => "Loop Back",
          ],
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "current_item" => [
          "type" => "mixed",
          "description" => "The current item being processed",
        ],
        "current_index" => [
          "type" => "integer",
          "description" => "Current position in the array (0-based)",
        ],
        "is_first" => [
          "type" => "boolean",
          "description" => "TRUE if this is the first item",
        ],
        "is_last" => [
          "type" => "boolean",
          "description" => "TRUE if this is the last item",
        ],
        "has_more" => [
          "type" => "boolean",
          "description" => "TRUE if more items remain after this one",
        ],
        "total_count" => [
          "type" => "integer",
          "description" => "Total number of items in the array",
        ],
        "loop_active" => [
          "type" => "boolean",
          "description" => "TRUE if loop is still processing, FALSE when complete",
        ],
        "collected_results" => [
          "type" => "array",
          "description" => "All collected results (only populated on completion)",
        ],
        "state_update" => [
          "type" => "object",
          "description" => "State update to apply to graph state",
        ],
      ],
    ];
  }

}
