<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO;

/**
 * Parameter bag containing resolved parameters for node execution.
 *
 * This class provides a clean, type-safe interface for accessing
 * parameter values within a plugin's process() method.
 *
 * Part of the Unified Parameter System - all parameters (whether they
 * came from config, workflow values, or runtime inputs) are resolved
 * and validated before being passed to the plugin.
 *
 * Note: This class intentionally does NOT extend Symfony's
 * HttpFoundation\ParameterBag for the following reasons:
 * - Immutability: Symfony's ParameterBag is mutable
 *   (E.g. set(), remove(), replace())
 *   which would break our functional, side-effect-free design. Workflow nodes
 *   should not be able to mutate parameters passed to them.
 * - Sealed design: We use 'final' to prevent extension and ensure consistent
 *   behavior across all usages.
 * - Type-safe getters: We provide additional typed accessors (getTyped(),
 *   getString(), getFloat(), getArray()) not available in Symfony.
 * - JsonSerializable: Native JSON serialization for logging and debugging.
 *
 * @see \Drupal\flowdrop\DTO\ParameterBagInterface
 * @see \Symfony\Component\HttpFoundation\ParameterBag
 * @see docs/development/unified-parameter-system-spec.md
 */
final class ParameterBag implements ParameterBagInterface, \JsonSerializable {

  /**
   * Resolved parameter values.
   *
   * @var array<string, mixed>
   */
  private array $parameters;

  /**
   * Constructs a ParameterBag.
   *
   * @param array<string, mixed> $parameters
   *   The resolved parameters.
   */
  public function __construct(array $parameters = []) {
    $this->parameters = $parameters;
  }

  /**
   * {@inheritdoc}
   */
  public function get(string $name, mixed $default = NULL): mixed {
    return $this->parameters[$name] ?? $default;
  }

  /**
   * {@inheritdoc}
   */
  public function has(string $name): bool {
    return array_key_exists($name, $this->parameters);
  }

  /**
   * {@inheritdoc}
   */
  public function all(): array {
    return $this->parameters;
  }

  /**
   * {@inheritdoc}
   */
  public function keys(): array {
    return array_keys($this->parameters);
  }

  /**
   * {@inheritdoc}
   */
  public function isEmpty(): bool {
    return empty($this->parameters);
  }

  /**
   * {@inheritdoc}
   */
  public function getTyped(string $name, string $type, mixed $default = NULL): mixed {
    $value = $this->get($name, $default);

    if ($value === NULL) {
      return $default;
    }

    $actualType = gettype($value);
    $expectedType = match ($type) {
      "string" => "string",
            "int", "integer" => "integer",
            "float", "double" => "double",
            "bool", "boolean" => "boolean",
            "array" => "array",
            default => $type,
    };

    // Allow int to be treated as float.
    if ($expectedType === "double" && $actualType === "integer") {
      return (float) $value;
    }

    if ($actualType !== $expectedType) {
      throw new \InvalidArgumentException(
            "Parameter '{$name}' expected type '{$type}', got '{$actualType}'"
        );
    }

    return $value;
  }

  /**
   * {@inheritdoc}
   */
  public function getString(string $name, string $default = ""): string {
    $value = $this->get($name, $default);

    if ($value === NULL) {
      return $default;
    }

    if (is_string($value)) {
      return $value;
    }

    // Attempt to convert scalar values to string.
    if (is_scalar($value)) {
      return (string) $value;
    }

    return $default;
  }

  /**
   * {@inheritdoc}
   */
  public function getInt(string $name, int $default = 0): int {
    $value = $this->get($name, $default);

    if ($value === NULL) {
      return $default;
    }

    if (is_int($value)) {
      return $value;
    }

    // Attempt to convert numeric strings and floats.
    if (is_numeric($value)) {
      return (int) $value;
    }

    return $default;
  }

  /**
   * {@inheritdoc}
   */
  public function getFloat(string $name, float $default = 0.0): float {
    $value = $this->get($name, $default);

    if ($value === NULL) {
      return $default;
    }

    if (is_float($value)) {
      return $value;
    }

    if (is_int($value)) {
      return (float) $value;
    }

    // Attempt to convert numeric strings.
    if (is_numeric($value)) {
      return (float) $value;
    }

    return $default;
  }

  /**
   * {@inheritdoc}
   */
  public function getBool(string $name, bool $default = FALSE): bool {
    $value = $this->get($name, $default);

    if ($value === NULL) {
      return $default;
    }

    if (is_bool($value)) {
      return $value;
    }

    // Handle common truthy/falsy values.
    if ($value === 1 || $value === "1" || $value === "true") {
      return TRUE;
    }

    if ($value === 0 || $value === "0" || $value === "false" || $value === "") {
      return FALSE;
    }

    return $default;
  }

  /**
   * {@inheritdoc}
   */
  public function getArray(string $name, array $default = []): array {
    $value = $this->get($name, $default);

    if ($value === NULL) {
      return $default;
    }

    if (is_array($value)) {
      return $value;
    }

    return $default;
  }

  /**
   * Create a ParameterBag from an array.
   *
   * @param array<string, mixed> $data
   *   The parameter data.
   *
   * @return self
   *   New ParameterBag instance.
   */
  public static function fromArray(array $data): self {
    return new self($data);
  }

  /**
   * Create an empty ParameterBag.
   *
   * @return self
   *   New empty ParameterBag instance.
   */
  public static function empty(): self {
    return new self([]);
  }

  /**
   * Create a new ParameterBag with additional parameters.
   *
   * This method is immutable - it returns a new instance.
   *
   * @param array<string, mixed> $parameters
   *   Additional parameters to merge.
   *
   * @return self
   *   New ParameterBag with merged parameters.
   */
  public function with(array $parameters): self {
    return new self(array_merge($this->parameters, $parameters));
  }

  /**
   * Create a new ParameterBag without specified parameters.
   *
   * This method is immutable - it returns a new instance.
   *
   * @param array<string> $names
   *   Parameter names to remove.
   *
   * @return self
   *   New ParameterBag without specified parameters.
   */
  public function without(array $names): self {
    $filtered = array_diff_key($this->parameters, array_flip($names));
    return new self($filtered);
  }

  /**
   * Serialize the ParameterBag to JSON.
   *
   * Implements \JsonSerializable interface to allow JSON encoding
   * of the ParameterBag instance via json_encode().
   *
   * @return array<string, mixed>
   *   The parameters array suitable for JSON serialization.
   */
  public function jsonSerialize(): array {
    return $this->parameters;
  }

}
