<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Plugin\FlowDropNodeProcessor;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ExecutionContextDTO;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ExecutionContextAwareInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ResumableNodeProcessorInterface;
use Drupal\flowdrop_interrupt\Message\HitlInterruptMessage;
use Drupal\flowdrop_interrupt\Enum\InterruptType;
use Drupal\flowdrop_interrupt\Exception\InterruptRequiredException;
use Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Choice node processor for human-in-loop workflows.
 *
 * Pauses workflow execution and presents the user with a list of options
 * to choose from. Supports both single selection and multiple selection modes.
 *
 * Config:
 * - message: The prompt message to display.
 * - options: Array of options with value, label, and optional description.
 * - multiple: Whether to allow multiple selections (default: false).
 * - min_selections: Minimum number of selections required (default: 1).
 * - max_selections: Maximum number of selections allowed (optional).
 *
 * Outputs:
 * - selected: The selected value(s) - string for single, array for multiple.
 * - selection_count: Number of options selected.
 * - response_time: Timestamp when the response was received.
 * - user_id: ID of the user who responded (if authenticated).
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "choice_input",
  label: new TranslatableMarkup("Choice Input"),
  description: "Pauses workflow for user to select from options",
  version: "1.0.0"
)]
class ChoiceNode extends AbstractFlowDropNodeProcessor implements ExecutionContextAwareInterface, ResumableNodeProcessorInterface {

  /**
   * The execution context.
   *
   * @var \Drupal\flowdrop\DTO\ExecutionContextDTO|null
   */
  protected ?ExecutionContextDTO $executionContext = NULL;

  /**
   * {@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 setExecutionContext(ExecutionContextDTO $context): void {
    $this->executionContext = $context;
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    if (empty($params["message"])) {
      return ValidationResult::error("message", "Prompt message is required.", "REQUIRED");
    }

    if (empty($params["options"]) || !is_array($params["options"])) {
      return ValidationResult::error("options", "At least one option is required.", "REQUIRED");
    }

    // Validate each option has value and label.
    foreach ($params["options"] as $index => $option) {
      if (!is_array($option)) {
        return ValidationResult::error("options", "Option {$index} must be an object.", "INVALID_TYPE");
      }
      if (empty($option["value"])) {
        return ValidationResult::error("options", "Option {$index} is missing a value.", "INVALID_OPTION");
      }
      if (empty($option["label"])) {
        return ValidationResult::error("options", "Option {$index} is missing a label.", "INVALID_OPTION");
      }
    }

    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   *
   * First-time entry only. The orchestrator routes resume invocations to
   * {@see self::resume()} via the ResumableNodeProcessorInterface contract.
   */
  public function process(ParameterBagInterface $params): array {
    $message = $params->getString("message", "Please select an option");
    $options = $params->getArray("options", []);
    $multiple = $params->getBool("multiple", FALSE);
    $minSelections = $params->getInt("min_selections", 1);
    $maxSelections = $params->get("max_selections");

    $enum = array_values(array_map(
      static fn (array $option): string => (string) ($option['value'] ?? ''),
      $options,
    ));
    // Persist labels (and descriptions, if any) parallel to enum so the
    // session→frontend bridge can rebuild `{value, label, description}`
    // option objects when the interrupt is rendered. Falling back to
    // `label = value` was OK as a stopgap but loses the operator's
    // configured display text.
    $labels = array_values(array_map(
      static fn (array $option): string => (string) ($option['label'] ?? $option['value'] ?? ''),
      $options,
    ));
    $descriptions = array_values(array_map(
      static fn (array $option): ?string => isset($option['description']) && $option['description'] !== ''
        ? (string) $option['description']
        : NULL,
      $options,
    ));
    $hasDescriptions = array_filter($descriptions, static fn ($d): bool => $d !== NULL) !== [];

    if ($multiple) {
      $items = ['type' => 'string', 'enum' => $enum, 'enumLabels' => $labels];
      if ($hasDescriptions) {
        $items['enumDescriptions'] = $descriptions;
      }
      $schema = [
        'type' => 'array',
        'items' => $items,
        'multiple' => TRUE,
        'minSelections' => $minSelections,
      ];
    }
    else {
      $schema = [
        'type' => 'string',
        'enum' => $enum,
        'enumLabels' => $labels,
      ];
      if ($hasDescriptions) {
        $schema['enumDescriptions'] = $descriptions;
      }
    }
    if ($maxSelections !== NULL) {
      $schema['maxSelections'] = (int) $maxSelections;
    }

    throw new InterruptRequiredException(new HitlInterruptMessage(
      type: InterruptType::SchemaForm,
      message: $message,
      schema: $schema,
    ), [], "Workflow paused for user selection");
  }

  /**
   * {@inheritdoc}
   *
   * Reads the response data from the resolved interrupt and normalises it
   * into the node's output shape. Multiple-selection mode promotes scalar
   * responses to arrays; single-selection mode flattens array responses.
   */
  public function resume(
    ResolvedInterruptInterface $interrupt,
    ParameterBagInterface $params,
  ): array {
    $multiple = $params->getBool("multiple", FALSE);
    $responseData = $interrupt->getResponseData();

    $selected = $responseData;
    if ($multiple && !is_array($selected)) {
      $selected = $selected !== NULL ? [$selected] : [];
    }
    elseif (!$multiple && is_array($selected)) {
      $selected = !empty($selected) ? reset($selected) : NULL;
    }

    $selectionCount = is_array($selected) ? count($selected) : ($selected !== NULL ? 1 : 0);

    return [
      "selected" => $selected,
      "selection_count" => $selectionCount,
      "response_time" => $interrupt->getResolvedAt() ?? time(),
      "user_id" => $interrupt->getResolvedBy(),
      "interrupt_id" => $interrupt->getInterruptId(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "message" => [
          "type" => "string",
          "title" => "Prompt Message",
          "description" => "The message to display to the user",
          "default" => "Please select an option",
          "format" => "multiline",
        ],
        "options" => [
          "type" => "array",
          "title" => "Options",
          "description" => "The options for the user to choose from",
          "items" => [
            "type" => "object",
            "properties" => [
              "value" => [
                "type" => "string",
                "title" => "Value",
                "description" => "The value returned when selected",
              ],
              "label" => [
                "type" => "string",
                "title" => "Label",
                "description" => "The display label for the option",
              ],
              "description" => [
                "type" => "string",
                "title" => "Description",
                "description" => "Optional description for the option",
              ],
            ],
            "required" => ["value", "label"],
          ],
        ],
        "multiple" => [
          "type" => "boolean",
          "title" => "Allow Multiple",
          "description" => "Allow selecting multiple options",
          "default" => FALSE,
        ],
        "min_selections" => [
          "type" => "integer",
          "title" => "Minimum Selections",
          "description" => "Minimum number of selections required",
          "default" => 1,
          "minimum" => 0,
        ],
        "max_selections" => [
          "type" => "integer",
          "title" => "Maximum Selections",
          "description" => "Maximum number of selections allowed (leave empty for no limit)",
          "minimum" => 1,
        ],
        "__interrupt_id__" => [
          "type" => "string",
          "description" => "Internal: Interrupt ID for resume operations",
          "connectable" => FALSE,
          "configurable" => FALSE,
        ],
      ],
      "required" => ["message", "options"],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "selected" => [
          "type" => "mixed",
          "description" => "The selected value(s) - string for single, array for multiple",
        ],
        "selection_count" => [
          "type" => "integer",
          "description" => "Number of options selected",
        ],
        "response_time" => [
          "type" => "integer",
          "description" => "Unix timestamp when the user responded",
        ],
        "user_id" => [
          "type" => "integer",
          "description" => "ID of the user who responded (null if anonymous)",
        ],
        "interrupt_id" => [
          "type" => "string",
          "description" => "UUID of the interrupt that was resolved",
        ],
      ],
    ];
  }

}
