<?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 to Dataframe nodes.
 */
#[FlowDropNodeProcessor(
  id: "data_to_dataframe",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Data to Dataframe"),
  description: "Convert data to dataframe format",
  version: "1.0.0"
)]
class DataToDataframe 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 {
    // Data to dataframe nodes can accept any inputs or none.
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $format = $params->get("format", "json");
    $includeIndex = $params->get("includeIndex", FALSE);
    $orient = $params->get("orient", "records");
    $data = $params->get("data") ?: [];

    $dataframe = $this->createDataframe($data, $includeIndex, $orient);

    return [
      "dataframe" => $dataframe,
      "format" => $format,
      "rows_count" => count($dataframe["data"]),
      "columns_count" => count($dataframe["columns"]),
      "include_index" => $includeIndex,
      "orient" => $orient,
    ];
  }

  /**
   * Create a dataframe-like structure from data.
   *
   * @param array<int|string, mixed> $data
   *   The input data.
   * @param bool $includeIndex
   *   Whether to include index.
   * @param string $orient
   *   The orientation.
   *
   * @return array{data: array<int|string, mixed>, columns: array<int, string>, index: array<int, int|string>}
   *   The dataframe structure.
   */
  private function createDataframe(array $data, bool $includeIndex, string $orient): array {
    if (empty($data)) {
      return [
        "data" => [],
        "columns" => [],
        "index" => [],
      ];
    }

    // Determine columns from first item.
    $columns = [];
    $firstItem = reset($data);
    if (is_array($firstItem)) {
      $columns = array_keys($firstItem);
    }

    // Create index if requested.
    $index = [];
    if ($includeIndex) {
      $index = array_keys($data);
    }

    // Normalize data based on orient.
    $normalizedData = [];
    switch ($orient) {
      case "records":
        $normalizedData = $data;
        break;

      case "index":
        $normalizedData = array_values($data);
        break;

      case "columns":
        $normalizedData = $this->transposeData($data);
        break;

      case "split":
        $normalizedData = $this->splitData($data);
        break;

      default:
        $normalizedData = $data;
    }

    return [
      "data" => $normalizedData,
      "columns" => $columns,
      "index" => $index,
    ];
  }

  /**
   * Transpose data for columns orient.
   *
   * @param array<int|string, mixed> $data
   *   The input data.
   *
   * @return array<string, array<int, mixed>>
   *   The transposed data.
   */
  private function transposeData(array $data): array {
    if (empty($data)) {
      return [];
    }

    $transposed = [];
    $firstItem = reset($data);
    if (is_array($firstItem)) {
      foreach (array_keys($firstItem) as $key) {
        $transposed[$key] = array_column($data, $key);
      }
    }

    return $transposed;
  }

  /**
   * Split data for split orient.
   *
   * @param array<int|string, mixed> $data
   *   The input data.
   *
   * @return array{data: array<int, mixed>, index: array<int, int|string>, columns: array<int, string>}
   *   The split data.
   */
  private function splitData(array $data): array {
    if (empty($data)) {
      return ["data" => [], "index" => [], "columns" => []];
    }

    $firstItem = reset($data);
    if (!is_array($firstItem)) {
      return ["data" => $data, "index" => [], "columns" => []];
    }

    return [
      "data" => array_values($data),
      "index" => array_keys($data),
      "columns" => array_keys($firstItem),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => "array",
          "title" => "Data",
          "description" => "The data to convert to dataframe",
          "required" => FALSE,
        ],
        "format" => [
          "type" => "string",
          "title" => "Format",
          "description" => "Output format to use",
          "default" => "json",
          "enum" => ["json", "csv", "parquet"],
        ],
        "includeIndex" => [
          "type" => "boolean",
          "title" => "Include Index",
          "description" => "Whether to include row indices",
          "default" => FALSE,
        ],
        "orient" => [
          "type" => "string",
          "title" => "Orientation",
          "description" => "Data orientation to use",
          "default" => "records",
          "enum" => ["records", "index", "columns", "split"],
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "dataframe" => [
          "type" => "object",
          "description" => "The dataframe structure",
        ],
        "format" => [
          "type" => "string",
          "description" => "The output format",
        ],
        "rows_count" => [
          "type" => "integer",
          "description" => "Number of rows in dataframe",
        ],
        "columns_count" => [
          "type" => "integer",
          "description" => "Number of columns in dataframe",
        ],
        "include_index" => [
          "type" => "boolean",
          "description" => "Whether index is included",
        ],
        "orient" => [
          "type" => "string",
          "description" => "The orientation used",
        ],
      ],
    ];
  }

}
