<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service;

use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop_runtime\Exception\MissingParameterException;
use Drupal\flowdrop_runtime\Exception\ParameterValidationException;
use Psr\Log\LoggerInterface;

/**
 * Service for resolving parameters from multiple sources.
 *
 * This service implements the core parameter resolution algorithm
 * for the Unified Parameter System. It takes parameter definitions
 * from the plugin schema, entity configuration, workflow values,
 * and runtime inputs, and resolves them into a single ParameterBag.
 *
 * Resolution Priority (highest to lowest):
 * 1. Runtime input (from upstream node connection) - if connectable=TRUE
 * 2. Workflow value (set by user in workflow editor) - if configurable=TRUE
 * 3. Entity default (from config entity YAML)
 * 4. Schema default (from plugin's getParameterSchema())
 *
 * Execution Context Access:
 * For processors that need access to workflow execution context (initial
 * data, workflow ID, etc.), implement ExecutionContextAwareInterface.
 * The runtime will automatically inject an ExecutionContextDTO with
 * type-safe access to context data.
 *
 * Flag Resolution (entity config → system defaults):
 * - connectable: entity config, else FALSE
 * - configurable: entity config, else FALSE
 * - required: entity config, else FALSE
 *
 * @see \Drupal\flowdrop_runtime\Service\ParameterResolverInterface
 * @see \Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ExecutionContextAwareInterface
 * @see docs/development/flowdrop-node-processor.md
 */
class ParameterResolver implements ParameterResolverInterface {

  /**
   * The logger service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private LoggerInterface $logger;

  /**
   * Constructs a ParameterResolver.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   */
  public function __construct(LoggerInterface $logger) {
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public function resolve(
    array $parameterSchema,
    array $entityConfig,
    array $workflowValues,
    array $runtimeInputs,
  ): ParameterBagInterface {
    $resolved = [];

    $properties = $parameterSchema["properties"] ?? [];

    foreach ($properties as $name => $schema) {
      $config = $entityConfig[$name] ?? [];

      // Get flags from entity config with system defaults.
      // Plugins no longer declare these - entity has full control.
      $configurable = $config["configurable"] ?? FALSE;
      $connectable = $config["connectable"] ?? FALSE;
      $required = $config["required"] ?? FALSE;

      // Get default value (entity overrides schema).
      $schemaDefault = $schema["default"] ?? NULL;
      $entityDefault = $config["default"] ?? NULL;
      $default = $entityDefault ?? $schemaDefault;

      // Resolve value based on priority.
      $value = $this->resolveValue(
            $name,
            $connectable,
            $configurable,
            $runtimeInputs,
            $workflowValues,
            $default
        );

      // Check required constraint.
      if ($required && $value === NULL) {
        throw new MissingParameterException(
          $name,
          "Required parameter '{$name}' is missing. " .
          "Ensure it is either connected (connectable: true) or has a default value."
          );
      }

      // Validate value against schema (strict).
      if ($value !== NULL) {
        $this->validateValue($name, $value, $schema);
      }

      $resolved[$name] = $value;

      $this->logger->debug("Resolved parameter '@name': @value", [
        "@name" => $name,
        "@value" => is_scalar($value) ? (string) $value : json_encode($value),
      ]);
    }

    return new ParameterBag($resolved);
  }

  /**
   * Resolve a single parameter value.
   *
   * @param string $name
   *   Parameter name.
   * @param bool $connectable
   *   Whether parameter accepts connections.
   * @param bool $configurable
   *   Whether parameter is configurable.
   * @param array<string, mixed> $runtimeInputs
   *   Runtime input values.
   * @param array<string, mixed> $workflowValues
   *   Workflow configuration values.
   * @param mixed $default
   *   Default value.
   *
   * @return mixed
   *   The resolved value.
   */
  private function resolveValue(
    string $name,
    bool $connectable,
    bool $configurable,
    array $runtimeInputs,
    array $workflowValues,
    mixed $default,
  ): mixed {
    // Priority 1: Runtime input (if connectable and provided).
    if ($connectable && array_key_exists($name, $runtimeInputs)) {
      $this->logger->debug("Parameter '@name' resolved from runtime input", ["@name" => $name]);
      return $runtimeInputs[$name];
    }

    // Priority 2: Workflow value (if configurable and provided).
    if ($configurable && array_key_exists($name, $workflowValues)) {
      $this->logger->debug("Parameter '@name' resolved from workflow value", ["@name" => $name]);
      return $workflowValues[$name];
    }

    // Priority 3: Default value.
    $this->logger->debug("Parameter '@name' resolved from default", ["@name" => $name]);
    return $default;
  }

  /**
   * Validate a parameter value against its schema.
   *
   * @param string $name
   *   Parameter name.
   * @param mixed $value
   *   Parameter value.
   * @param array<string, mixed> $schema
   *   Parameter schema.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\ParameterValidationException
   *   When validation fails.
   */
  private function validateValue(string $name, mixed $value, array $schema): void {
    // Type validation.
    $expectedType = $schema["type"] ?? "mixed";
    if ($expectedType !== "mixed" && !$this->isValidType($value, $expectedType)) {
      $actualType = gettype($value);
      throw new ParameterValidationException(
            $name,
            $value,
            "type",
            "Parameter '{$name}' expects type '{$expectedType}', got '{$actualType}'"
        );
    }

    // Enum validation.
    if (isset($schema["enum"])) {
      if (!in_array($value, $schema["enum"], TRUE)) {
        $allowed = implode(", ", array_map(
              fn($v): string => is_string($v) ? "'{$v}'" : (string) $v,
              $schema["enum"]
          ));
        throw new ParameterValidationException(
              $name,
              $value,
              "enum",
              "Parameter '{$name}' received invalid value. Allowed values: {$allowed}"
          );
      }
    }

    // Numeric constraints.
    if (is_numeric($value)) {
      $numericValue = (float) $value;

      if (isset($schema["minimum"]) && $numericValue < $schema["minimum"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "minimum",
              "Parameter '{$name}' value {$value} is below minimum {$schema["minimum"]}"
          );
      }

      if (isset($schema["maximum"]) && $numericValue > $schema["maximum"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "maximum",
              "Parameter '{$name}' value {$value} exceeds maximum {$schema["maximum"]}"
          );
      }

      if (isset($schema["exclusiveMinimum"]) && $numericValue <= $schema["exclusiveMinimum"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "exclusiveMinimum",
              "Parameter '{$name}' value {$value} must be greater than {$schema["exclusiveMinimum"]}"
          );
      }

      if (isset($schema["exclusiveMaximum"]) && $numericValue >= $schema["exclusiveMaximum"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "exclusiveMaximum",
              "Parameter '{$name}' value {$value} must be less than {$schema["exclusiveMaximum"]}"
          );
      }

      if (isset($schema["multipleOf"]) && fmod($numericValue, $schema["multipleOf"]) !== 0.0) {
        throw new ParameterValidationException(
              $name,
              $value,
              "multipleOf",
              "Parameter '{$name}' value {$value} must be a multiple of {$schema["multipleOf"]}"
          );
      }
    }

    // String constraints.
    if (is_string($value)) {
      $length = mb_strlen($value);

      if (isset($schema["minLength"]) && $length < $schema["minLength"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "minLength",
              "Parameter '{$name}' length {$length} is below minimum {$schema["minLength"]}"
          );
      }

      if (isset($schema["maxLength"]) && $length > $schema["maxLength"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "maxLength",
              "Parameter '{$name}' length {$length} exceeds maximum {$schema["maxLength"]}"
          );
      }

      if (isset($schema["pattern"])) {
        $pattern = "/" . $schema["pattern"] . "/u";
        if (!preg_match($pattern, $value)) {
          throw new ParameterValidationException(
                $name,
                $value,
                "pattern",
                "Parameter '{$name}' does not match required pattern '{$schema["pattern"]}'"
            );
        }
      }

      // Format validation (common formats).
      if (isset($schema["format"])) {
        $this->validateFormat($name, $value, $schema["format"]);
      }
    }

    // Array constraints.
    if (is_array($value)) {
      $count = count($value);

      if (isset($schema["minItems"]) && $count < $schema["minItems"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "minItems",
              "Parameter '{$name}' has {$count} items, minimum is {$schema["minItems"]}"
          );
      }

      if (isset($schema["maxItems"]) && $count > $schema["maxItems"]) {
        throw new ParameterValidationException(
              $name,
              $value,
              "maxItems",
              "Parameter '{$name}' has {$count} items, maximum is {$schema["maxItems"]}"
          );
      }

      if (isset($schema["uniqueItems"]) && $schema["uniqueItems"]) {
        $unique = array_unique($value, SORT_REGULAR);
        if (count($unique) !== $count) {
          throw new ParameterValidationException(
                $name,
                $value,
                "uniqueItems",
                "Parameter '{$name}' must have unique items"
            );
        }
      }
    }
  }

  /**
   * Validate a string value against a format.
   *
   * @param string $name
   *   Parameter name.
   * @param string $value
   *   The string value.
   * @param string $format
   *   The expected format.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\ParameterValidationException
   *   When format validation fails.
   */
  private function validateFormat(string $name, string $value, string $format): void {
    $valid = match ($format) {
      "email" => filter_var($value, FILTER_VALIDATE_EMAIL) !== FALSE,
            "uri", "url" => filter_var($value, FILTER_VALIDATE_URL) !== FALSE,
            "date" => $this->isValidDate($value, "Y-m-d"),
            "date-time" => $this->isValidDate($value, \DateTimeInterface::RFC3339) ||
                           $this->isValidDate($value, \DateTimeInterface::ATOM) ||
                           $this->isValidDate($value, "Y-m-d\TH:i:s.u\Z") ||
                           $this->isValidDate($value, "Y-m-d\TH:i:sP"),
            "time" => $this->isValidDate($value, "H:i:s") ||
                      $this->isValidDate($value, "H:i"),
            "ipv4" => filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== FALSE,
            "ipv6" => filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== FALSE,
            "uuid" => preg_match(
            "/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i",
            $value
        ) === 1,
            "hostname" => preg_match(
            "/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*$/",
            $value
        ) === 1,
      // Unknown formats pass validation.
            default => TRUE,
    };

    if (!$valid) {
      throw new ParameterValidationException(
            $name,
            $value,
            "format",
            "Parameter '{$name}' is not a valid {$format}"
        );
    }
  }

  /**
   * Check if a date string is valid.
   *
   * @param string $value
   *   The date string.
   * @param string $format
   *   The expected format.
   *
   * @return bool
   *   TRUE if valid.
   */
  private function isValidDate(string $value, string $format): bool {
    $date = \DateTimeImmutable::createFromFormat($format, $value);
    return $date !== FALSE && $date->format($format) === $value;
  }

  /**
   * Check if a value matches the expected type.
   *
   * @param mixed $value
   *   The value to check.
   * @param string $expectedType
   *   The expected type.
   *
   * @return bool
   *   TRUE if type matches.
   */
  private function isValidType(mixed $value, string $expectedType): bool {
    return match ($expectedType) {
      "string" => is_string($value),
            "integer", "int" => is_int($value),
            "number" => is_int($value) || is_float($value),
            "boolean", "bool" => is_bool($value),
            "array" => is_array($value),
            "object" => is_array($value) || is_object($value),
            "null" => $value === NULL,
            "mixed" => TRUE,
            default => TRUE,
    };
  }

}
