<?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 Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * State Storage node processor for managing graph state data.
 *
 * This node provides a key-value storage mechanism within the graph state,
 * enabling workflows to:
 * - Store values for use by later nodes.
 * - Retrieve previously stored values.
 * - Accumulate data across multiple executions.
 * - Clear or reset stored values.
 *
 * The storage uses the graph state's `data` field, which persists across
 * checkpoints and can be accessed by other state-aware nodes.
 *
 * Operations:
 * - set: Store a value under a key.
 * - get: Retrieve a value by key.
 * - append: Append a value to an array stored under a key.
 * - increment: Increment a numeric value.
 * - delete: Remove a key from storage.
 * - clear: Clear all stored data.
 *
 * Inputs:
 * - operation: The operation to perform (set, get, append, increment,
 *   delete, clear).
 * - key: The storage key (required for most operations).
 * - value: The value to store (required for set, append).
 * - default: Default value if key not found (optional for get).
 * - __data__: Current state data (injected from graph state).
 *
 * Outputs:
 * - value: The stored or retrieved value.
 * - key: The key that was operated on.
 * - operation: The operation that was performed.
 * - exists: Whether the key exists in storage.
 * - state_update: State update to apply to graph state.
 *
 * @see \Drupal\flowdrop_stategraph\DTO\GraphState
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "state_storage",
  label: new TranslatableMarkup("State Storage"),
  description: "Store and retrieve values in the graph state for use across nodes",
  version: "1.0.0"
)]
class StateStorageNode extends AbstractFlowDropNodeProcessor {

  /**
   * Valid operations for state storage.
   *
   * @var array<string>
   */
  private const VALID_OPERATIONS = [
    "set",
    "get",
    "append",
    "increment",
    "delete",
    "clear",
  ];

  /**
   * {@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 = [];

    // Operation is required.
    if (!isset($params["operation"])) {
      $errors[] = new ValidationError('operation', "Missing required parameter: operation");
    }
    elseif (!in_array($params["operation"], self::VALID_OPERATIONS, TRUE)) {
      $validOps = implode(", ", self::VALID_OPERATIONS);
      $errors[] = new ValidationError('operation', "Invalid operation '{$params["operation"]}'. Valid operations are: {$validOps}");
    }
    else {
      $operation = $params["operation"];

      // Key is required for most operations.
      if (in_array($operation, ["set", "get", "append", "increment", "delete"], TRUE)) {
        if (!isset($params["key"]) || $params["key"] === "") {
          $errors[] = new ValidationError('key', "The 'key' parameter is required for '{$operation}' operation.");
        }
      }

      // Value is required for set and append.
      if (in_array($operation, ["set", "append"], TRUE)) {
        if (!array_key_exists("value", $params)) {
          $errors[] = new ValidationError('value', "The 'value' parameter is required for '{$operation}' operation.");
        }
      }
    }

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

    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $operation = $params->get("operation", "get");
    $key = $params->get("key", "");
    $value = $params->get("value");
    $default = $params->get("default");
    $currentData = $params->get("__data__", []);

    // Ensure currentData is an array.
    if (!is_array($currentData)) {
      $currentData = [];
    }

    return match ($operation) {
      "set" => $this->handleSet($key, $value, $currentData),
      "get" => $this->handleGet($key, $default, $currentData),
      "append" => $this->handleAppend($key, $value, $currentData),
      "increment" => $this->handleIncrement($key, $params->get("amount", 1), $currentData),
      "delete" => $this->handleDelete($key, $currentData),
      "clear" => $this->handleClear(),
      default => $this->handleGet($key, $default, $currentData),
    };
  }

  /**
   * Handles the 'set' operation.
   *
   * @param string $key
   *   The storage key.
   * @param mixed $value
   *   The value to store.
   * @param array<string, mixed> $currentData
   *   Current state data.
   *
   * @return array<string, mixed>
   *   The output array.
   */
  private function handleSet(string $key, mixed $value, array $currentData): array {
    // Support nested keys using dot notation.
    $this->setNestedValue($currentData, $key, $value);

    return [
      "value" => $value,
      "key" => $key,
      "operation" => "set",
      "exists" => TRUE,
      "state_update" => [
        "data" => $currentData,
      ],
    ];
  }

  /**
   * Handles the 'get' operation.
   *
   * @param string $key
   *   The storage key.
   * @param mixed $default
   *   Default value if key not found.
   * @param array<string, mixed> $currentData
   *   Current state data.
   *
   * @return array<string, mixed>
   *   The output array.
   */
  private function handleGet(string $key, mixed $default, array $currentData): array {
    $value = $this->getNestedValue($currentData, $key);
    $exists = $value !== NULL || $this->hasNestedKey($currentData, $key);

    return [
      "value" => $exists ? $value : $default,
      "key" => $key,
      "operation" => "get",
      "exists" => $exists,
      // No state update for get operations.
    ];
  }

  /**
   * Handles the 'append' operation.
   *
   * @param string $key
   *   The storage key.
   * @param mixed $value
   *   The value to append.
   * @param array<string, mixed> $currentData
   *   Current state data.
   *
   * @return array<string, mixed>
   *   The output array.
   */
  private function handleAppend(string $key, mixed $value, array $currentData): array {
    $existing = $this->getNestedValue($currentData, $key);

    // Initialize as array if not already.
    if ($existing === NULL) {
      $existing = [];
    }
    elseif (!is_array($existing)) {
      // Convert existing scalar to array.
      $existing = [$existing];
    }

    // Append the new value.
    $existing[] = $value;
    $this->setNestedValue($currentData, $key, $existing);

    return [
      "value" => $existing,
      "key" => $key,
      "operation" => "append",
      "exists" => TRUE,
      "count" => count($existing),
      "state_update" => [
        "data" => $currentData,
      ],
    ];
  }

  /**
   * Handles the 'increment' operation.
   *
   * @param string $key
   *   The storage key.
   * @param int|float $amount
   *   The amount to increment by.
   * @param array<string, mixed> $currentData
   *   Current state data.
   *
   * @return array<string, mixed>
   *   The output array.
   */
  private function handleIncrement(string $key, int|float $amount, array $currentData): array {
    $existing = $this->getNestedValue($currentData, $key);

    // Initialize as 0 if not already numeric.
    if ($existing === NULL || !is_numeric($existing)) {
      $existing = 0;
    }

    $newValue = $existing + $amount;
    $this->setNestedValue($currentData, $key, $newValue);

    return [
      "value" => $newValue,
      "key" => $key,
      "operation" => "increment",
      "exists" => TRUE,
      "previous_value" => $existing,
      "amount" => $amount,
      "state_update" => [
        "data" => $currentData,
      ],
    ];
  }

  /**
   * Handles the 'delete' operation.
   *
   * @param string $key
   *   The storage key.
   * @param array<string, mixed> $currentData
   *   Current state data.
   *
   * @return array<string, mixed>
   *   The output array.
   */
  private function handleDelete(string $key, array $currentData): array {
    $existed = $this->hasNestedKey($currentData, $key);
    $deletedValue = $this->getNestedValue($currentData, $key);

    // Remove the key.
    $this->deleteNestedKey($currentData, $key);

    return [
      "value" => $deletedValue,
      "key" => $key,
      "operation" => "delete",
      "exists" => FALSE,
      "was_present" => $existed,
      "state_update" => [
        "data" => $currentData,
      ],
    ];
  }

  /**
   * Handles the 'clear' operation.
   *
   * @return array<string, mixed>
   *   The output array.
   */
  private function handleClear(): array {
    return [
      "value" => NULL,
      "key" => "",
      "operation" => "clear",
      "exists" => FALSE,
      "state_update" => [
        "data" => [],
      ],
    ];
  }

  /**
   * Gets a nested value using dot notation.
   *
   * @param array<string, mixed> $data
   *   The data array.
   * @param string $key
   *   The dot-notation key (e.g., "user.profile.name").
   *
   * @return mixed
   *   The value or NULL if not found.
   */
  private function getNestedValue(array $data, string $key): mixed {
    $parts = explode(".", $key);
    $current = $data;

    foreach ($parts as $part) {
      if (!is_array($current) || !array_key_exists($part, $current)) {
        return NULL;
      }
      $current = $current[$part];
    }

    return $current;
  }

  /**
   * Sets a nested value using dot notation.
   *
   * @param array<string, mixed> $data
   *   The data array (passed by reference).
   * @param string $key
   *   The dot-notation key.
   * @param mixed $value
   *   The value to set.
   */
  private function setNestedValue(array &$data, string $key, mixed $value): void {
    $parts = explode(".", $key);
    $current = &$data;

    foreach ($parts as $i => $part) {
      // If this is the last part, set the value.
      if ($i === count($parts) - 1) {
        $current[$part] = $value;
        return;
      }

      // Create intermediate arrays if needed.
      if (!isset($current[$part]) || !is_array($current[$part])) {
        $current[$part] = [];
      }
      $current = &$current[$part];
    }
  }

  /**
   * Checks if a nested key exists.
   *
   * @param array<string, mixed> $data
   *   The data array.
   * @param string $key
   *   The dot-notation key.
   *
   * @return bool
   *   TRUE if the key exists.
   */
  private function hasNestedKey(array $data, string $key): bool {
    $parts = explode(".", $key);
    $current = $data;

    foreach ($parts as $part) {
      if (!is_array($current) || !array_key_exists($part, $current)) {
        return FALSE;
      }
      $current = $current[$part];
    }

    return TRUE;
  }

  /**
   * Deletes a nested key.
   *
   * @param array<string, mixed> $data
   *   The data array (passed by reference).
   * @param string $key
   *   The dot-notation key.
   */
  private function deleteNestedKey(array &$data, string $key): void {
    $parts = explode(".", $key);

    if (count($parts) === 1) {
      unset($data[$key]);
      return;
    }

    // Navigate to parent.
    $current = &$data;
    for ($i = 0; $i < count($parts) - 1; $i++) {
      if (!isset($current[$parts[$i]]) || !is_array($current[$parts[$i]])) {
        return;
      }
      $current = &$current[$parts[$i]];
    }

    // Delete the final key.
    unset($current[$parts[count($parts) - 1]]);
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "operation" => [
          "type" => "string",
          "enum" => self::VALID_OPERATIONS,
          "description" => "The storage operation to perform",
        ],
        "key" => [
          "type" => "string",
          "description" => "The storage key (supports dot notation for nested values)",
        ],
        "value" => [
          "type" => "mixed",
          "description" => "The value to store (for set, append operations)",
        ],
        "default" => [
          "type" => "mixed",
          "description" => "Default value if key not found (for get operation)",
        ],
        "amount" => [
          "type" => "number",
          "description" => "Amount to increment by (for increment operation, default: 1)",
        ],
        "__data__" => [
          "type" => "object",
          "description" => "Current state data (automatically injected from graph state)",
          "format" => "hidden",
        ],
      ],
      "required" => ["operation"],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "value" => [
          "type" => "mixed",
          "description" => "The stored, retrieved, or modified value",
        ],
        "key" => [
          "type" => "string",
          "description" => "The key that was operated on",
        ],
        "operation" => [
          "type" => "string",
          "description" => "The operation that was performed",
        ],
        "exists" => [
          "type" => "boolean",
          "description" => "Whether the key exists in storage after the operation",
        ],
        "state_update" => [
          "type" => "object",
          "description" => "State update to apply to graph state",
        ],
      ],
    ];
  }

}
