<?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 Regex Extractor nodes.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "regex_extractor",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Regex Extractor"),
  description: "Extract data using regular expressions",
  version: "1.0.0"
)]
class RegexExtractor extends AbstractFlowDropNodeProcessor {

  /**
   * Maximum allowed regex pattern length.
   */
  private const MAX_PATTERN_LENGTH = 500;

  /**
   * Allowed regex flags.
   *
   * @var string[]
   */
  private const ALLOWED_FLAGS = ['i', 'm', 's', 'u', 'x'];

  /**
   * {@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 {
    $pattern = $params["pattern"] ?? "";
    if (!empty($pattern) && strlen($pattern) > self::MAX_PATTERN_LENGTH) {
      return ValidationResult::error(
        "pattern",
        "Regex pattern exceeds maximum length of " . self::MAX_PATTERN_LENGTH . " characters.",
        "PATTERN_TOO_LONG"
      );
    }

    $flags = $params["flags"] ?? "";
    if (!empty($flags)) {
      $flagChars = str_split($flags);
      $invalidFlags = array_diff($flagChars, self::ALLOWED_FLAGS);
      if (!empty($invalidFlags)) {
        return ValidationResult::error(
          "flags",
          "Invalid regex flags: " . implode(", ", $invalidFlags) . ". Allowed: " . implode(", ", self::ALLOWED_FLAGS),
          "INVALID_FLAGS"
        );
      }
    }

    // Validate the regex pattern compiles without errors.
    if (!empty($pattern)) {
      $testRegex = "/" . $pattern . "/" . $flags;
      if (@preg_match($testRegex, "") === FALSE) {
        return ValidationResult::error(
          "pattern",
          "Invalid regex pattern: the pattern could not be compiled.",
          "INVALID_PATTERN"
        );
      }
    }

    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $pattern = $params->get("pattern", "");
    $flags = $params->get("flags", "");
    $matchAll = $params->get("matchAll", FALSE);
    $text = $params->get("text") ?: "";

    $matches = [];
    $error = NULL;
    if (!empty($pattern) && !empty($text)) {
      $regex = "/" . $pattern . "/" . $flags;

      // Use a reduced backtrack limit to prevent ReDoS.
      $previousLimit = (int) ini_get("pcre.backtrack_limit");
      ini_set("pcre.backtrack_limit", (string) min($previousLimit, 100000));

      try {
        if ($matchAll) {
          $result = @preg_match_all($regex, $text, $matches, PREG_SET_ORDER);
        }
        else {
          $result = @preg_match($regex, $text, $matches);
        }

        if ($result === FALSE) {
          $pcreError = preg_last_error();
          $matches = [];
          $error = match ($pcreError) {
            PREG_BACKTRACK_LIMIT_ERROR => "Regex backtrack limit exceeded. Simplify the pattern or reduce input size.",
            PREG_RECURSION_LIMIT_ERROR => "Regex recursion limit exceeded.",
            PREG_INTERNAL_ERROR => "Internal regex error occurred.",
            default => "Regex execution failed.",
          };
        }
      }
      finally {
        ini_set("pcre.backtrack_limit", (string) $previousLimit);
      }
    }

    $output = [
      "matches" => $matches,
      "pattern" => $pattern,
      "flags" => $flags,
      "match_all" => $matchAll,
      "total_matches" => count($matches),
    ];

    if ($error !== NULL) {
      $output["error"] = $error;
    }

    return $output;
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "text" => [
          "type" => "string",
          "title" => "Text",
          "description" => "The text to extract from",
          "required" => FALSE,
        ],
        "pattern" => [
          "type" => "string",
          "title" => "Pattern",
          "description" => "The regex pattern to match",
          "default" => "",
        ],
        "flags" => [
          "type" => "string",
          "title" => "Flags",
          "description" => "Regex flags (i, m, s, x, etc.)",
          "default" => "",
        ],
        "matchAll" => [
          "type" => "boolean",
          "title" => "Match All",
          "description" => "Whether to match all occurrences",
          "default" => FALSE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "matches" => [
          "type" => "array",
          "description" => "The regex matches",
        ],
        "pattern" => [
          "type" => "string",
          "description" => "The regex pattern used",
        ],
        "flags" => [
          "type" => "string",
          "description" => "The regex flags used",
        ],
        "match_all" => [
          "type" => "boolean",
          "description" => "Whether to match all occurrences",
        ],
        "total_matches" => [
          "type" => "integer",
          "description" => "The total number of matches",
        ],
      ],
    ];
  }

}
