<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\ValueObjects;

use App\Framework\Core\ValueObjects\ClientIdentifier;
use App\Framework\Core\ValueObjects\IdempotencyKey;
use App\Framework\Security\CsrfToken;
use InvalidArgumentException;

/**
 * Value Object representing action method parameters
 *
 * Provides type-safe wrapper around action parameters with validation,
 * type coercion, and convenient access methods.
 */
final readonly class ActionParameters
{
    /**
     * @param array<string, mixed> $parameters Action parameters
     * @param CsrfToken|null $csrfToken CSRF token for security validation
     * @param ClientIdentifier|null $clientIdentifier Client identifier for rate limiting and tracking
     * @param IdempotencyKey|null $idempotencyKey Idempotency key for duplicate request prevention
     */
    private function __construct(
        private array $parameters,
        private ?CsrfToken $csrfToken = null,
        private ?ClientIdentifier $clientIdentifier = null,
        private ?IdempotencyKey $idempotencyKey = null
    ) {
        $this->validate();
    }

    /**
     * Create ActionParameters from array
     *
     * @param array<string, mixed> $parameters
     * @param CsrfToken|null $csrfToken CSRF token
     * @param ClientIdentifier|null $clientIdentifier Client identifier
     * @param IdempotencyKey|null $idempotencyKey Idempotency key
     */
    public static function fromArray(
        array $parameters,
        ?CsrfToken $csrfToken = null,
        ?ClientIdentifier $clientIdentifier = null,
        ?IdempotencyKey $idempotencyKey = null
    ): self {
        return new self($parameters, $csrfToken, $clientIdentifier, $idempotencyKey);
    }

    /**
     * Create empty ActionParameters
     */
    public static function empty(): self
    {
        return new self([], null);
    }

    /**
     * Get CSRF token
     */
    public function getCsrfToken(): ?CsrfToken
    {
        return $this->csrfToken;
    }

    /**
     * Check if CSRF token is present
     */
    public function hasCsrfToken(): bool
    {
        return $this->csrfToken !== null;
    }

    /**
     * Get client identifier
     */
    public function getClientIdentifier(): ?ClientIdentifier
    {
        return $this->clientIdentifier;
    }

    /**
     * Check if client identifier is present
     */
    public function hasClientIdentifier(): bool
    {
        return $this->clientIdentifier !== null;
    }

    /**
     * Get idempotency key
     */
    public function getIdempotencyKey(): ?IdempotencyKey
    {
        return $this->idempotencyKey;
    }

    /**
     * Check if idempotency key is present
     */
    public function hasIdempotencyKey(): bool
    {
        return $this->idempotencyKey !== null;
    }

    /**
     * Get parameter value
     *
     * @template T
     * @param string $key
     * @param T|null $default
     * @return T|mixed
     */
    public function get(string $key, mixed $default = null): mixed
    {
        return $this->parameters[$key] ?? $default;
    }

    /**
     * Get string parameter
     *
     * @throws InvalidArgumentException if parameter is not string
     */
    public function getString(string $key, ?string $default = null): ?string
    {
        $value = $this->get($key, $default);

        if ($value === null) {
            return null;
        }

        if (! is_string($value)) {
            throw new InvalidArgumentException(
                "Parameter '{$key}' must be string, got: " . gettype($value)
            );
        }

        return $value;
    }

    /**
     * Get required string parameter
     *
     * @throws InvalidArgumentException if parameter is missing or not string
     */
    public function requireString(string $key): string
    {
        if (! $this->has($key)) {
            throw new InvalidArgumentException("Required parameter '{$key}' is missing");
        }

        $value = $this->getString($key);

        if ($value === null) {
            throw new InvalidArgumentException("Required parameter '{$key}' cannot be null");
        }

        return $value;
    }

    /**
     * Get integer parameter
     *
     * @throws InvalidArgumentException if parameter is not int
     */
    public function getInt(string $key, ?int $default = null): ?int
    {
        $value = $this->get($key, $default);

        if ($value === null) {
            return null;
        }

        if (! is_int($value) && ! is_numeric($value)) {
            throw new InvalidArgumentException(
                "Parameter '{$key}' must be integer, got: " . gettype($value)
            );
        }

        return (int) $value;
    }

    /**
     * Get required integer parameter
     *
     * @throws InvalidArgumentException if parameter is missing or not int
     */
    public function requireInt(string $key): int
    {
        if (! $this->has($key)) {
            throw new InvalidArgumentException("Required parameter '{$key}' is missing");
        }

        $value = $this->getInt($key);

        if ($value === null) {
            throw new InvalidArgumentException("Required parameter '{$key}' cannot be null");
        }

        return $value;
    }

    /**
     * Get float parameter
     *
     * @throws InvalidArgumentException if parameter is not float
     */
    public function getFloat(string $key, ?float $default = null): ?float
    {
        $value = $this->get($key, $default);

        if ($value === null) {
            return null;
        }

        if (! is_float($value) && ! is_numeric($value)) {
            throw new InvalidArgumentException(
                "Parameter '{$key}' must be float, got: " . gettype($value)
            );
        }

        return (float) $value;
    }

    /**
     * Get boolean parameter
     *
     * @throws InvalidArgumentException if parameter is not bool
     */
    public function getBool(string $key, ?bool $default = null): ?bool
    {
        $value = $this->get($key, $default);

        if ($value === null) {
            return null;
        }

        // Type coercion for boolean
        if (is_bool($value)) {
            return $value;
        }

        // Convert common string representations
        if (is_string($value)) {
            $lower = strtolower($value);
            if (in_array($lower, ['true', '1', 'yes', 'on'], true)) {
                return true;
            }
            if (in_array($lower, ['false', '0', 'no', 'off', ''], true)) {
                return false;
            }
        }

        // Convert numeric values
        if (is_numeric($value)) {
            return (bool) $value;
        }

        return (bool) $value;
    }

    /**
     * Get array parameter
     *
     * @throws InvalidArgumentException if parameter is not array
     * @return array<mixed>|null
     */
    public function getArray(string $key, ?array $default = null): ?array
    {
        $value = $this->get($key, $default);

        if ($value === null) {
            return null;
        }

        if (! is_array($value)) {
            throw new InvalidArgumentException(
                "Parameter '{$key}' must be array, got: " . gettype($value)
            );
        }

        return $value;
    }

    /**
     * Check if parameter exists
     */
    public function has(string $key): bool
    {
        return array_key_exists($key, $this->parameters);
    }

    /**
     * Get all parameters as array
     *
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        return $this->parameters;
    }

    /**
     * Check if parameters are empty
     */
    public function isEmpty(): bool
    {
        return empty($this->parameters);
    }

    /**
     * Get number of parameters
     */
    public function count(): int
    {
        return count($this->parameters);
    }

    /**
     * Get all parameter keys
     *
     * @return string[]
     */
    public function keys(): array
    {
        return array_keys($this->parameters);
    }

    /**
     * Filter parameters by keys
     *
     * @param string[] $keys
     */
    public function only(array $keys): self
    {
        $filtered = array_intersect_key(
            $this->parameters,
            array_flip($keys)
        );

        return new self($filtered);
    }

    /**
     * Create new instance without keys
     *
     * @param string[] $keys
     */
    public function except(array $keys): self
    {
        $filtered = array_diff_key(
            $this->parameters,
            array_flip($keys)
        );

        return new self($filtered);
    }

    /**
     * Check if parameters equal another ActionParameters
     */
    public function equals(self $other): bool
    {
        return $this->parameters === $other->parameters;
    }

    /**
     * Validate parameters structure
     *
     * @throws InvalidArgumentException
     */
    private function validate(): void
    {
        // Validate all keys are strings
        foreach ($this->parameters as $key => $value) {
            if (! is_string($key)) {
                throw new InvalidArgumentException(
                    'ActionParameters keys must be strings, got: ' . gettype($key)
                );
            }
        }
    }
}
