<?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\ExpressionEvaluator\ExpressionEvaluatorInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop\Service\ExpressionEvaluatorPluginManager;
use Drupal\flowdrop_node_processor\Service\JsonSchemaValidator;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Data Shaper node processor.
 *
 * Takes mixed input data and reshapes it into a clean structure defined by:
 * 1. A mapping configuration (source paths → target paths)
 * 2. A JSON Schema (defines expected output structure for validation)
 *
 * Supports a configurable expression engine for resolving source paths.
 *
 * Mapping features:
 * - Simple scalar mappings using the selected engine's syntax
 * - Array iteration with "_source" and "_each" nested configuration
 * - Special values: _NOW_, _LITERAL:value, _NULL_, _EMPTY_ARRAY_,
 *   _EMPTY_OBJECT_
 * - Optional JSON Schema validation (strict or soft mode)
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "data_shaper",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Data Shaper"),
  description: "Reshape mixed input data into a clean structure using field mappings and optional JSON Schema validation",
  version: "1.1.0"
)]
class DataShaper extends AbstractFlowDropNodeProcessor {

  use ExpressionEvaluatorAwareTrait;

  /**
   * Default expression engine.
   */
  protected const DEFAULT_ENGINE = "property_path";

  /**
   * Constructs a DataShaper object.
   *
   * @param array<string, mixed> $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin ID.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Drupal\flowdrop\Service\ExpressionEvaluatorPluginManager $evaluatorManager
   *   The expression evaluator plugin manager.
   * @param \Drupal\flowdrop_node_processor\Service\JsonSchemaValidator $schemaValidator
   *   The JSON schema validator service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    protected readonly ExpressionEvaluatorPluginManager $evaluatorManager,
    protected readonly JsonSchemaValidator $schemaValidator,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ): static {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get("flowdrop.expression_evaluator_plugin_manager"),
      $container->get("flowdrop_node_processor.json_schema_validator"),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    if (!isset($params["data"])) {
      return ValidationResult::error(
        "data",
        "Data shaper requires data input",
        "MISSING_DATA"
      );
    }
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $sourceData = $params->get("data");
    $mapping = $params->getArray("mapping", []);
    $schema = $params->getArray("schema", []);
    $validationMode = $params->getString("validationMode", "soft");
    $engineId = $params->getString(self::ENGINE_PARAM, self::DEFAULT_ENGINE);

    if (!in_array($validationMode, ["strict", "soft"], TRUE)) {
      $validationMode = "soft";
    }

    if (empty($mapping)) {
      return $this->buildResult($sourceData, $schema, $validationMode, 0);
    }

    $evaluator = $this->getEvaluator($engineId);
    $mappedFieldsCount = 0;
    $result = [];

    foreach ($mapping as $targetPath => $sourceConfig) {
      $value = $this->resolveValue($sourceData, $sourceConfig, $evaluator, $mappedFieldsCount);
      $this->setNestedValue($result, $targetPath, $value);
    }

    return $this->buildResult($result, $schema, $validationMode, $mappedFieldsCount);
  }

  /**
   * Resolve a value from the source data based on configuration.
   *
   * @param mixed $sourceData
   *   The source data.
   * @param mixed $sourceConfig
   *   The source configuration (string path or array for iteration).
   * @param \Drupal\flowdrop\Plugin\ExpressionEvaluator\ExpressionEvaluatorInterface $evaluator
   *   The expression evaluator to use.
   * @param int &$mappedFieldsCount
   *   Counter for successfully mapped fields.
   *
   * @return mixed
   *   The resolved value.
   */
  protected function resolveValue(
    mixed $sourceData,
    mixed $sourceConfig,
    ExpressionEvaluatorInterface $evaluator,
    int &$mappedFieldsCount,
  ): mixed {
    // Handle array iteration configuration.
    if (is_array($sourceConfig) && isset($sourceConfig["_source"])) {
      return $this->resolveArrayMapping($sourceData, $sourceConfig, $evaluator, $mappedFieldsCount);
    }

    // Handle string path or special values.
    if (is_string($sourceConfig)) {
      return $this->resolveStringValue($sourceData, $sourceConfig, $evaluator, $mappedFieldsCount);
    }

    // Return as-is for other types (literal values).
    return $sourceConfig;
  }

  /**
   * Resolve a string value (path or special value).
   *
   * @param mixed $sourceData
   *   The source data.
   * @param string $sourceConfig
   *   The source path or special value.
   * @param \Drupal\flowdrop\Plugin\ExpressionEvaluator\ExpressionEvaluatorInterface $evaluator
   *   The expression evaluator.
   * @param int &$mappedFieldsCount
   *   Counter for successfully mapped fields.
   *
   * @return mixed
   *   The resolved value.
   */
  protected function resolveStringValue(
    mixed $sourceData,
    string $sourceConfig,
    ExpressionEvaluatorInterface $evaluator,
    int &$mappedFieldsCount,
  ): mixed {
    $specialValue = $this->resolveSpecialValue($sourceConfig);
    if ($specialValue !== NULL) {
      $mappedFieldsCount++;
      return $specialValue["value"];
    }

    $value = $evaluator->evaluate($sourceConfig, $sourceData);
    if ($value !== NULL) {
      $mappedFieldsCount++;
    }
    return $value;
  }

  /**
   * Resolve special values like _NOW_, _LITERAL:, etc.
   *
   * @param string $value
   *   The value to check.
   *
   * @return array{value: mixed}|null
   *   An array with the resolved value, or null if not a special value.
   */
  protected function resolveSpecialValue(string $value): ?array {
    switch ($value) {
      case "_NOW_":
        return ["value" => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)];

      case "_NULL_":
        return ["value" => NULL];

      case "_EMPTY_ARRAY_":
        return ["value" => []];

      case "_EMPTY_OBJECT_":
        return ["value" => new \stdClass()];
    }

    if (str_starts_with($value, "_LITERAL:")) {
      return ["value" => substr($value, 9)];
    }

    return NULL;
  }

  /**
   * Resolve array mapping with _source and _each configuration.
   *
   * @param mixed $sourceData
   *   The source data.
   * @param array<string, mixed> $config
   *   The array mapping configuration with _source and _each keys.
   * @param \Drupal\flowdrop\Plugin\ExpressionEvaluator\ExpressionEvaluatorInterface $evaluator
   *   The expression evaluator.
   * @param int &$mappedFieldsCount
   *   Counter for successfully mapped fields.
   *
   * @return array<int, mixed>
   *   The mapped array.
   */
  protected function resolveArrayMapping(
    mixed $sourceData,
    array $config,
    ExpressionEvaluatorInterface $evaluator,
    int &$mappedFieldsCount,
  ): array {
    $sourcePath = $config["_source"] ?? "";
    $eachMapping = $config["_each"] ?? [];

    $sourceArray = $evaluator->evaluate($sourcePath, $sourceData);
    if ($sourceArray !== NULL) {
      $mappedFieldsCount++;
    }

    if (!is_array($sourceArray)) {
      return [];
    }

    if (empty($eachMapping) || !is_array($eachMapping)) {
      return $sourceArray;
    }

    $result = [];
    foreach ($sourceArray as $item) {
      $mappedItem = [];
      foreach ($eachMapping as $targetKey => $sourcePath) {
        if (is_array($sourcePath) && isset($sourcePath["_source"])) {
          $mappedItem[$targetKey] = $this->resolveArrayMapping($item, $sourcePath, $evaluator, $mappedFieldsCount);
        }
        elseif (is_string($sourcePath)) {
          $mappedItem[$targetKey] = $this->resolveStringValue($item, $sourcePath, $evaluator, $mappedFieldsCount);
        }
        else {
          $mappedItem[$targetKey] = $sourcePath;
        }
      }
      $result[] = $mappedItem;
    }

    return $result;
  }

  /**
   * Build the result array with optional schema validation.
   *
   * @param mixed $data
   *   The mapped data.
   * @param array<string, mixed> $schema
   *   The JSON Schema for validation.
   * @param string $validationMode
   *   The validation mode (strict or soft).
   * @param int $mappedFieldsCount
   *   The count of successfully mapped fields.
   *
   * @return array{data: mixed, valid: bool, errors: array<int, array{property: string, message: string}>, mappedFields: int}
   *   The result array.
   */
  protected function buildResult(
    mixed $data,
    array $schema,
    string $validationMode,
    int $mappedFieldsCount,
  ): array {
    $validationResult = $this->schemaValidator->validate($data, $schema, $validationMode);

    return [
      "data" => $data,
      "valid" => $validationResult["valid"],
      "errors" => $validationResult["errors"],
      "mappedFields" => $mappedFieldsCount,
    ];
  }

  /**
   * Set a value in a nested array structure using dot notation.
   *
   * @param array<string, mixed> $array
   *   The array to modify (passed by reference).
   * @param string $path
   *   The dot-notation path (e.g., "user.profile.name").
   * @param mixed $value
   *   The value to set.
   */
  protected function setNestedValue(array &$array, string $path, mixed $value): void {
    $keys = explode(".", $path);
    $current = &$array;

    foreach ($keys as $i => $key) {
      if ($i === count($keys) - 1) {
        $current[$key] = $value;
        return;
      }

      if (!isset($current[$key]) || !is_array($current[$key])) {
        $current[$key] = [];
      }

      $current = &$current[$key];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => "mixed",
          "title" => "Input Data",
          "description" => "The source data to reshape (any type: object, array, string, etc.)",
          "required" => TRUE,
        ],
        "mapping" => [
          "type" => "object",
          "format" => "json",
          "title" => "Field Mapping",
          "description" => "Mapping configuration: target path => source expression. Syntax depends on the selected engine. For arrays, use { \"_source\": \"path\", \"_each\": { \"field\": \"subpath\" } }",
          "default" => [],
        ],
        "schema" => [
          "type" => "object",
          "format" => "json",
          "title" => "Output JSON Schema",
          "description" => "Optional JSON Schema to validate the output structure against",
          "default" => [],
        ],
        "validationMode" => [
          "type" => "string",
          "title" => "Validation Mode",
          "description" => "How to handle validation failures: 'strict' throws error, 'soft' continues with errors in output",
          "default" => "soft",
          "enum" => ["strict", "soft"],
        ],
        self::ENGINE_PARAM => [
          "type" => "string",
          "title" => "Expression Engine",
          "description" => "The expression language to use for source path expressions.",
          "default" => self::DEFAULT_ENGINE,
          "enum" => $this->getAvailableEngineIds(),
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => "mixed",
          "description" => "The reshaped output data conforming to the defined structure",
        ],
        "valid" => [
          "type" => "boolean",
          "description" => "Whether the output passes JSON Schema validation (true if no schema provided)",
        ],
        "errors" => [
          "type" => "array",
          "description" => "Array of validation errors (empty if valid or no schema)",
          "items" => [
            "type" => "object",
            "properties" => [
              "property" => [
                "type" => "string",
                "description" => "The property path that failed validation",
              ],
              "message" => [
                "type" => "string",
                "description" => "The validation error message",
              ],
            ],
          ],
        ],
        "mappedFields" => [
          "type" => "integer",
          "description" => "Count of successfully mapped fields",
        ],
      ],
    ];
  }

}
