<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Executor for Data Operations nodes.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "data_operations",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Data Operations"),
  description: "Perform various data operations on arrays and objects",
  version: "1.0.0"
)]
class DataOperations extends AbstractFlowDropNodeProcessor {

  /**
   * Allowed data operations.
   *
   * @var string[]
   */
  private const ALLOWED_OPERATIONS = [
    'filter', 'sort', 'group', 'map', 'reduce', 'unique', 'slice', 'merge',
  ];

  /**
   * Allowed filter conditions.
   *
   * @var string[]
   */
  private const ALLOWED_CONDITIONS = [
    'equals', 'not_equals', 'contains', 'greater_than', 'less_than',
    'greater_than_or_equal', 'less_than_or_equal', 'is_empty', 'is_not_empty',
  ];

  /**
   * {@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 {
    $operation = $params["operation"] ?? "filter";
    if (!in_array($operation, self::ALLOWED_OPERATIONS, TRUE)) {
      return ValidationResult::error(
        "operation",
        "Invalid operation: " . $operation . ". Allowed: " . implode(", ", self::ALLOWED_OPERATIONS),
        "INVALID_OPERATION"
      );
    }

    $condition = $params["condition"] ?? "equals";
    if ($operation === "filter" && !in_array($condition, self::ALLOWED_CONDITIONS, TRUE)) {
      return ValidationResult::error(
        "condition",
        "Invalid condition: " . $condition . ". Allowed: " . implode(", ", self::ALLOWED_CONDITIONS),
        "INVALID_CONDITION"
      );
    }

    return ValidationResult::success();
  }

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

    $result = [];
    switch ($operation) {
      case "filter":
        $result = $this->filterData($data, $key, $value, $condition);
        break;

      case "sort":
        $result = $this->sortData($data, $key, $value === "desc");
        break;

      case "group":
        $result = $this->groupData($data, $key);
        break;

      case "map":
        $result = $this->mapData($data, $key, $value);
        break;

      case "reduce":
        $result = $this->reduceData($data, $key, $value);
        break;

      case "unique":
        $result = $this->uniqueData($data, $key);
        break;

      case "slice":
        $start = (int) $key;
        $length = (int) $value;
        $result = array_slice($data, $start, $length);
        break;

      case "merge":
        $result = $this->mergeData($data);
        break;
    }

    return [
      "result" => $result,
      "operation" => $operation,
      "input_count" => count($data),
      "output_count" => count($result),
    ];
  }

  /**
   * Filter data based on key-value condition.
   *
   * @param array<int|string, mixed> $data
   *   The data to filter.
   * @param string $key
   *   The key to filter on.
   * @param mixed $value
   *   The value to compare against.
   * @param string $condition
   *   The comparison condition.
   *
   * @return array<int|string, mixed>
   *   The filtered data.
   */
  private function filterData(array $data, string $key, mixed $value, string $condition): array {
    if (empty($key)) {
      return $data;
    }

    return array_filter($data, function ($item) use ($key, $value, $condition) {
      $itemValue = is_array($item) ? ($item[$key] ?? NULL) : (is_object($item) ? ($item->$key ?? NULL) : NULL);

      switch ($condition) {
        case "equals":
          return $itemValue === $value;

        case "not_equals":
          return $itemValue !== $value;

        case "contains":
          return strpos((string) $itemValue, (string) $value) !== FALSE;

        case "greater_than":
          return $itemValue > $value;

        case "less_than":
          return $itemValue < $value;

        case "greater_than_or_equal":
          return $itemValue >= $value;

        case "less_than_or_equal":
          return $itemValue <= $value;

        case "is_empty":
          return empty($itemValue);

        case "is_not_empty":
          return !empty($itemValue);

        default:
          return TRUE;
      }
    });
  }

  /**
   * Sort data by key.
   *
   * @param array<int|string, mixed> $data
   *   The data to sort.
   * @param string $key
   *   The key to sort by.
   * @param bool $descending
   *   Whether to sort in descending order.
   *
   * @return array<int|string, mixed>
   *   The sorted data.
   */
  private function sortData(array $data, string $key, bool $descending = FALSE): array {
    if (empty($key)) {
      return $data;
    }

    usort($data, function ($a, $b) use ($key, $descending) {
      $aValue = is_array($a) ? ($a[$key] ?? "") : (is_object($a) ? ($a->$key ?? "") : "");
      $bValue = is_array($b) ? ($b[$key] ?? "") : (is_object($b) ? ($b->$key ?? "") : "");

      $result = $aValue <=> $bValue;
      return $descending ? -$result : $result;
    });

    return $data;
  }

  /**
   * Group data by key.
   *
   * @param array<int|string, mixed> $data
   *   The data to group.
   * @param string $key
   *   The key to group by.
   *
   * @return array<string, array<int, mixed>>
   *   The grouped data.
   */
  private function groupData(array $data, string $key): array {
    if (empty($key)) {
      return $data;
    }

    $grouped = [];
    foreach ($data as $item) {
      $groupKey = is_array($item) ? ($item[$key] ?? "") : (is_object($item) ? ($item->$key ?? "") : "");
      $grouped[$groupKey][] = $item;
    }

    return $grouped;
  }

  /**
   * Map data using a key.
   *
   * @param array<int|string, mixed> $data
   *   The data to map.
   * @param string $key
   *   The key to map by.
   * @param mixed $value
   *   The value to use for mapping.
   *
   * @return array<int|string, mixed>
   *   The mapped data.
   */
  private function mapData(array $data, string $key, mixed $value): array {
    if (empty($key)) {
      return $data;
    }

    return array_map(function ($item) use ($key, $value) {
      if (is_array($item)) {
        $item[$key] = $value;
      }
      elseif (is_object($item)) {
        $item->$key = $value;
      }
      return $item;
    }, $data);
  }

  /**
   * Reduce data using a key.
   *
   * @param array<int|string, mixed> $data
   *   The data to reduce.
   * @param string $key
   *   The key to reduce by.
   * @param mixed $initialValue
   *   The initial value.
   *
   * @return mixed
   *   The reduced value.
   */
  private function reduceData(array $data, string $key, mixed $initialValue): mixed {
    return array_reduce($data, function ($carry, $item) use ($key) {
      $itemValue = is_array($item) ? ($item[$key] ?? 0) : (is_object($item) ? ($item->$key ?? 0) : 0);
      return $carry + $itemValue;
    }, $initialValue);
  }

  /**
   * Get unique data by key.
   *
   * @param array<int|string, mixed> $data
   *   The data to make unique.
   * @param string $key
   *   The key to check uniqueness by.
   *
   * @return array<int, mixed>
   *   The unique data.
   */
  private function uniqueData(array $data, string $key): array {
    if (empty($key)) {
      return array_unique($data, SORT_REGULAR);
    }

    $seen = [];
    $unique = [];

    foreach ($data as $item) {
      $itemValue = is_array($item) ? ($item[$key] ?? "") : (is_object($item) ? ($item->$key ?? "") : "");
      if (!in_array($itemValue, $seen, TRUE)) {
        $seen[] = $itemValue;
        $unique[] = $item;
      }
    }

    return $unique;
  }

  /**
   * Merge data arrays.
   *
   * @param array<int|string, mixed> $data
   *   The data to merge.
   *
   * @return array<int|string, mixed>
   *   The merged data.
   */
  private function mergeData(array $data): array {
    $merged = [];
    foreach ($data as $item) {
      if (is_array($item)) {
        $merged = array_merge($merged, $item);
      }
    }
    return $merged;
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => "array",
          "title" => "Data",
          "description" => "Data to process",
          "required" => FALSE,
        ],
        "operation" => [
          "type" => "string",
          "title" => "Operation",
          "description" => "Data operation to perform",
          "default" => "filter",
          "enum" => [
            "filter",
            "sort",
            "group",
            "map",
            "reduce",
            "unique",
            "slice",
            "merge",
          ],
        ],
        "key" => [
          "type" => "string",
          "title" => "Key",
          "description" => "Key to operate on",
          "default" => "",
        ],
        "value" => [
          "type" => "mixed",
          "title" => "Value",
          "description" => "Value for the operation",
          "default" => "",
        ],
        "condition" => [
          "type" => "string",
          "title" => "Condition",
          "description" => "Condition for filtering",
          "default" => "equals",
          "enum" => [
            "equals",
            "not_equals",
            "contains",
            "greater_than",
            "less_than",
            "greater_than_or_equal",
            "less_than_or_equal",
            "is_empty",
            "is_not_empty",
          ],
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "result" => [
          "type" => "array",
          "description" => "The processed data result",
        ],
        "operation" => [
          "type" => "string",
          "description" => "The operation performed",
        ],
        "input_count" => [
          "type" => "integer",
          "description" => "Number of input items",
        ],
        "output_count" => [
          "type" => "integer",
          "description" => "Number of output items",
        ],
      ],
    ];
  }

}
