<?php

declare(strict_types=1);

namespace App\Framework\Router\ValueObjects;

use App\Framework\Router\Exceptions\ParameterResolutionException;

/**
 * Value Object für Route-Parameter mit Type Safety und Validation
 */
final readonly class RouteParameters
{
    private array $parameters;

    /**
     * @param array<string, mixed> $parameters
     */
    public function __construct(array $parameters = [])
    {
        $this->parameters = $this->validateAndNormalize($parameters);
    }

    /**
     * Factory method aus Array
     */
    public static function fromArray(array $parameters): self
    {
        return new self($parameters);
    }

    /**
     * Factory method für leere Parameter
     */
    public static function empty(): self
    {
        return new self([]);
    }

    /**
     * Prüft ob Parameter existiert
     */
    public function has(string $name): bool
    {
        return array_key_exists($name, $this->parameters);
    }

    /**
     * Holt Parameter-Wert mit Type Safety
     */
    public function get(string $name, mixed $default = null): mixed
    {
        return $this->parameters[$name] ?? $default;
    }

    /**
     * Holt String-Parameter mit Validation
     */
    public function getString(string $name, ?string $default = null): string
    {
        $value = $this->get($name, $default);

        if ($value === null) {
            throw new ParameterResolutionException("Parameter '{$name}' is required but not provided");
        }

        return (string) $value;
    }

    /**
     * Holt Integer-Parameter mit Validation
     */
    public function getInt(string $name, ?int $default = null): int
    {
        $value = $this->get($name, $default);

        if ($value === null) {
            throw new ParameterResolutionException("Parameter '{$name}' is required but not provided");
        }

        if (! is_numeric($value)) {
            throw new ParameterResolutionException("Parameter '{$name}' must be numeric, got: " . gettype($value));
        }

        return (int) $value;
    }

    /**
     * Holt Boolean-Parameter mit Validation
     */
    public function getBool(string $name, ?bool $default = null): bool
    {
        $value = $this->get($name, $default);

        if ($value === null) {
            throw new ParameterResolutionException("Parameter '{$name}' is required but not provided");
        }

        return filter_var($value, FILTER_VALIDATE_BOOLEAN);
    }

    /**
     * Holt Float-Parameter mit Validation
     */
    public function getFloat(string $name, ?float $default = null): float
    {
        $value = $this->get($name, $default);

        if ($value === null) {
            throw new ParameterResolutionException("Parameter '{$name}' is required but not provided");
        }

        if (! is_numeric($value)) {
            throw new ParameterResolutionException("Parameter '{$name}' must be numeric, got: " . gettype($value));
        }

        return (float) $value;
    }

    /**
     * Holt Array-Parameter mit Validation
     */
    public function getArray(string $name, ?array $default = null): array
    {
        $value = $this->get($name, $default);

        if ($value === null) {
            throw new ParameterResolutionException("Parameter '{$name}' is required but not provided");
        }

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

        return $value;
    }

    /**
     * Alle Parameter als Array
     */
    public function all(): array
    {
        return $this->parameters;
    }

    /**
     * Anzahl der Parameter
     */
    public function count(): int
    {
        return count($this->parameters);
    }

    /**
     * Prüft ob Parameter leer sind
     */
    public function isEmpty(): bool
    {
        return empty($this->parameters);
    }

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

    /**
     * Parameter-Werte
     */
    public function values(): array
    {
        return array_values($this->parameters);
    }

    /**
     * Erstellt neue Instanz mit zusätzlichen Parametern
     */
    public function with(string $name, mixed $value): self
    {
        $parameters = $this->parameters;
        $parameters[$name] = $value;

        return new self($parameters);
    }

    /**
     * Erstellt neue Instanz ohne bestimmten Parameter
     */
    public function without(string $name): self
    {
        $parameters = $this->parameters;
        unset($parameters[$name]);

        return new self($parameters);
    }

    /**
     * Erstellt neue Instanz mit mehreren Parametern
     */
    public function merge(array $parameters): self
    {
        return new self(array_merge($this->parameters, $parameters));
    }

    /**
     * Filtert Parameter nach Callback
     */
    public function filter(callable $callback): self
    {
        return new self(array_filter($this->parameters, $callback, ARRAY_FILTER_USE_BOTH));
    }

    /**
     * Nur bestimmte Parameter
     */
    public function only(array $keys): self
    {
        return new self(array_intersect_key($this->parameters, array_flip($keys)));
    }

    /**
     * Alle Parameter außer bestimmten
     */
    public function except(array $keys): self
    {
        return new self(array_diff_key($this->parameters, array_flip($keys)));
    }

    /**
     * Parameter mit Constraints validieren
     */
    public function validate(ParameterConstraints $constraints): self
    {
        foreach ($constraints->toArray() as $name => $constraint) {
            if (! $this->has($name)) {
                if ($constraint['required'] ?? false) {
                    throw new ParameterResolutionException("Required parameter '{$name}' is missing");
                }

                continue;
            }

            $value = $this->get($name);

            // Type validation
            if (isset($constraint['type'])) {
                $this->validateType($name, $value, $constraint['type']);
            }

            // Range validation for numbers
            if (isset($constraint['min']) && is_numeric($value) && $value < $constraint['min']) {
                throw new ParameterResolutionException("Parameter '{$name}' must be >= {$constraint['min']}, got: {$value}");
            }

            if (isset($constraint['max']) && is_numeric($value) && $value > $constraint['max']) {
                throw new ParameterResolutionException("Parameter '{$name}' must be <= {$constraint['max']}, got: {$value}");
            }

            // Length validation for strings
            if (isset($constraint['minLength']) && is_string($value) && strlen($value) < $constraint['minLength']) {
                throw new ParameterResolutionException("Parameter '{$name}' must be at least {$constraint['minLength']} characters long");
            }

            if (isset($constraint['maxLength']) && is_string($value) && strlen($value) > $constraint['maxLength']) {
                throw new ParameterResolutionException("Parameter '{$name}' must be at most {$constraint['maxLength']} characters long");
            }

            // Pattern validation
            if (isset($constraint['pattern']) && is_string($value) && ! preg_match($constraint['pattern'], $value)) {
                throw new ParameterResolutionException("Parameter '{$name}' does not match required pattern");
            }

            // Enum validation
            if (isset($constraint['enum']) && ! in_array($value, $constraint['enum'], true)) {
                throw new ParameterResolutionException("Parameter '{$name}' must be one of: " . implode(', ', $constraint['enum']));
            }
        }

        return $this;
    }

    /**
     * Debug-Informationen
     */
    public function getDebugInfo(): array
    {
        return [
            'count' => $this->count(),
            'keys' => $this->keys(),
            'types' => array_map('gettype', $this->parameters),
            'isEmpty' => $this->isEmpty(),
        ];
    }

    /**
     * Validiert und normalisiert Parameter
     */
    private function validateAndNormalize(array $parameters): array
    {
        $normalized = [];

        foreach ($parameters as $name => $value) {
            if (! is_string($name)) {
                throw new ParameterResolutionException("Parameter names must be strings, got: " . gettype($name));
            }

            if (trim($name) === '') {
                throw new ParameterResolutionException("Parameter names cannot be empty");
            }

            $normalized[trim($name)] = $value;
        }

        return $normalized;
    }

    /**
     * Validiert Parameter-Typ
     */
    private function validateType(string $name, mixed $value, string $expectedType): void
    {
        $actualType = gettype($value);

        $isValid = match($expectedType) {
            'string' => is_string($value),
            'int', 'integer' => is_int($value) || (is_numeric($value) && (int) $value == $value),
            'float', 'double' => is_float($value) || is_numeric($value),
            'bool', 'boolean' => is_bool($value) || in_array(strtolower((string) $value), ['true', 'false', '1', '0'], true),
            'array' => is_array($value),
            'null' => $value === null,
            default => true // Unknown types pass through
        };

        if (! $isValid) {
            throw new ParameterResolutionException("Parameter '{$name}' must be of type '{$expectedType}', got: {$actualType}");
        }
    }

    /**
     * Konvertiert zu JSON
     */
    public function toJson(): string
    {
        return json_encode($this->parameters, JSON_THROW_ON_ERROR);
    }

    /**
     * Factory method aus JSON
     */
    public static function fromJson(string $json): self
    {
        $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

        if (! is_array($data)) {
            throw new ParameterResolutionException("JSON must decode to an array");
        }

        return new self($data);
    }
}
