<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

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 Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
 * Executor for Get Workflow Data nodes.
 *
 * This processor retrieves the initial data passed when the workflow
 * execution was started. It implements ExecutionContextAwareInterface
 * to receive type-safe access to the execution context.
 *
 * The execution context provides:
 * - initial_data: The data passed when workflow execution started
 * - workflow_id: The current workflow ID
 * - pipeline_id: The current pipeline ID
 * - execution_id: The unique execution identifier
 * - metadata: Additional execution metadata
 *
 * @see \Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ExecutionContextAwareInterface
 * @see \Drupal\flowdrop\DTO\ExecutionContextDTO
 */
#[FlowDropNodeProcessor(
  id: "get_workflow_data",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Get Workflow Data"),
  description: "Retrieve data passed to the workflow at execution time.",
  version: "1.0.0"
)]
class GetWorkflowData extends AbstractFlowDropNodeProcessor implements ExecutionContextAwareInterface {

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

  /**
   * The property accessor service.
   *
   * @var \Symfony\Component\PropertyAccess\PropertyAccessorInterface
   */
  protected PropertyAccessorInterface $propertyAccessor;

  /**
   * Constructs a GetWorkflowData object.
   *
   * @param array<string, mixed> $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin ID.
   * @param array<string, mixed> $plugin_definition
   *   The plugin definition.
   */
  public function __construct(
    array $configuration,
    string $plugin_id,
    array $plugin_definition,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
  }

  /**
   * {@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 {
    // This node works with execution context, not regular parameters.
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    // Check if execution context was injected.
    if ($this->executionContext === NULL) {
      // Return empty result when not in workflow context.
      return [
        "data" => NULL,
        "path" => "",
        "exists" => FALSE,
        "workflow_id" => "",
        "pipeline_id" => "",
        "execution_id" => "",
      ];
    }

    // Get the initial data from the type-safe context.
    $initialData = $this->executionContext->getInitialData();

    // Get optional path for extracting specific value from initial_data.
    $path = $params->getString("path", "");

    // Get optional default value if path extraction fails.
    $defaultValue = $params->get("default_value");

    // If a path is specified, extract that specific value from initial_data.
    if (!empty($path)) {
      $extractedValue = $this->extractDataByPath($initialData, $path);

      // Use default value if extraction returned NULL and default is provided.
      if ($extractedValue === NULL && $defaultValue !== NULL) {
        $extractedValue = $defaultValue;
      }

      return [
        "data" => $extractedValue,
        "path" => $path,
        "exists" => $extractedValue !== NULL || $defaultValue !== NULL,
        "initial_data" => $initialData,
        "workflow_id" => $this->executionContext->getWorkflowId(),
        "pipeline_id" => $this->executionContext->getPipelineId(),
        "execution_id" => $this->executionContext->getExecutionId(),
      ];
    }

    // No path specified, return all initial data.
    return [
      "data" => $initialData,
      "path" => "",
      "exists" => TRUE,
      "initial_data" => $initialData,
      "workflow_id" => $this->executionContext->getWorkflowId(),
      "pipeline_id" => $this->executionContext->getPipelineId(),
      "execution_id" => $this->executionContext->getExecutionId(),
    ];
  }

  /**
   * Extracts data from the workflow data using a property path.
   *
   * Supports both array notation [key] and dot notation (key.subkey).
   *
   * @param mixed $data
   *   The data structure to extract from.
   * @param string $path
   *   The property path (e.g., "[user][name]" or "user.name").
   *
   * @return mixed
   *   The extracted value or NULL if path doesn't exist.
   */
  private function extractDataByPath(mixed $data, string $path): mixed {
    if (empty($path)) {
      return $data;
    }

    // Handle empty or non-array data.
    if (!is_array($data) && !is_object($data)) {
      return NULL;
    }

    try {
      // Normalize dot notation to array notation for PropertyAccess.
      $normalizedPath = $this->normalizePropertyPath($path);

      if ($this->propertyAccessor->isReadable($data, $normalizedPath)) {
        return $this->propertyAccessor->getValue($data, $normalizedPath);
      }

      return NULL;
    }
    catch (\Exception $e) {
      return NULL;
    }
  }

  /**
   * Normalizes a property path to PropertyAccess format.
   *
   * Converts dot notation (user.name) to array notation ([user][name]).
   *
   * @param string $path
   *   The property path.
   *
   * @return string
   *   The normalized path in PropertyAccess format.
   */
  private function normalizePropertyPath(string $path): string {
    // If already in bracket notation, return as-is.
    if (str_starts_with($path, "[")) {
      return $path;
    }

    // Convert dot notation to bracket notation.
    $parts = explode(".", $path);
    $normalizedParts = array_map(
      static fn(string $part): string => "[{$part}]",
      $parts
    );

    return implode("", $normalizedParts);
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "path" => [
          "type" => "string",
          "title" => "Property Path",
          "description" => "Optional path to extract specific value from initial_data (e.g., 'user.email' or '[items][0][name]').",
          "default" => "",
        ],
        "default_value" => [
          "type" => "mixed",
          "title" => "Default Value",
          "description" => "Value to return if the path extraction fails or returns NULL.",
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => "mixed",
          "description" => "The extracted data (specific value if path provided, otherwise all initial data).",
        ],
        "path" => [
          "type" => "string",
          "description" => "The property path used for extraction (empty if no path specified).",
        ],
        "exists" => [
          "type" => "boolean",
          "description" => "Whether the data exists (TRUE if found or default value provided).",
        ],
        "initial_data" => [
          "type" => "object",
          "description" => "The complete workflow initial data for reference.",
        ],
        "workflow_id" => [
          "type" => "string",
          "description" => "The workflow ID of the current execution.",
        ],
        "pipeline_id" => [
          "type" => "string",
          "description" => "The pipeline ID of the current execution.",
        ],
        "execution_id" => [
          "type" => "string",
          "description" => "The unique execution ID of the current run.",
        ],
      ],
    ];
  }

}
