<?php

declare(strict_types=1);

namespace NoahVet\Reef\Domain\Validation\V2;

final class OpenApiValidator implements OpenApiValidatorInterface
{
    /**
     * @param iterable<OpenApiPropertyValidatorInterface> $propertyValidators
     */
    public function __construct(
        private readonly iterable $propertyValidators,
    ) {
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<string, mixed>     $rules
     */
    public function validate(array $payload, array $rules): bool
    {
        if (!$this->validateRequired($payload, $rules)) {
            return false;
        }

        return $this->validatePayload($payload, $rules);
    }

    /**
     * @param array<string, mixed> $value
     * @param array<string, mixed> $rule
     */
    public function validatePayloadArray(array $value, array $rule, string $fullPropertyName): bool
    {
        $childRules = $this->getObjectChildRules($rule);

        if (null !== $childRules) {
            if (!$this->validatePayload($value, $childRules, $fullPropertyName.'.')) {
                return false;
            }
        }

        // Recurse through arrays of objects
        if (isset($rule['items']['properties']) && \is_array($rule['items']['properties'])) {
            if (!$this->validateArrayItems($value, $rule['items'], $fullPropertyName)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<string, mixed>     $rules
     */
    private function validatePayload(array $payload, array $rules, string $nodeName = ''): bool
    {
        foreach ($payload as $propertyName => $value) {
            $rule = \is_string($propertyName) && \is_array($rules[$propertyName] ?? null) ? $rules[$propertyName] : [];
            $fullPropertyName = $nodeName.$propertyName;

            foreach ($this->propertyValidators as $validator) {
                if (!$validator->validate($fullPropertyName, $value, $rule)) {
                    return false;
                }
            }

            if (\is_array($value)) {
                if (!$this->validatePayloadArray($value, $rule, $fullPropertyName)) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * @param array<string|int, mixed> $value
     * @param array<string, mixed>     $itemsRule
     */
    private function validateArrayItems(array $value, array $itemsRule, string $fullPropertyName): bool
    {
        if (!\array_is_list($value)) {
            if (!$this->validateRequired($value, $itemsRule)) {
                return false;
            }

            return $this->validatePayload($value, $itemsRule['properties'], $fullPropertyName.'.');
        }

        foreach ($value as $item) {
            if (!\is_array($item)) {
                return false;
            }

            if (!$this->validateRequired($item, $itemsRule)) {
                return false;
            }

            if (!$this->validatePayload($item, $itemsRule['properties'], $fullPropertyName.'.')) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param array<string, mixed> $rule
     *
     * @return array<string, mixed>|null
     */
    private function getObjectChildRules(array $rule): ?array
    {
        if (isset($rule['properties']) && \is_array($rule['properties'])) {
            return $rule['properties'];
        }

        $knownKeys = ['type', 'nullable', 'enum', 'items', 'required'];
        $objectLikeKeys = \array_diff(\array_keys($rule), $knownKeys);

        if ([] !== $objectLikeKeys) {
            return $rule;
        }

        return null;
    }

    /**
     * @param array<int|string, mixed> $payload
     * @param array<string, mixed>     $rules
     */
    private function validateRequired(array $payload, array $rules): bool
    {
        $requiredKeys = $this->extractRequired($rules);

        foreach ($requiredKeys as $requiredKey) {
            if (!\array_key_exists($requiredKey, $payload)) {
                return false;
            }
        }

        $properties = $rules['properties'] ?? $this->getObjectChildRules($rules) ?? [];

        foreach ($properties as $propertyName => $propertyRules) {
            if ('required' === $propertyName || !\is_array($propertyRules)) {
                continue;
            }

            if (!\array_key_exists($propertyName, $payload)) {
                $isNullable = isset($propertyRules['nullable']) && true === $propertyRules['nullable'];

                if ($isNullable) {
                    continue;
                }

                return false;
            }

            // Nested objects
            if (isset($propertyRules['properties']) || $this->hasRequired($propertyRules)) {
                $childPayload = $payload[$propertyName] ?? [];

                if (!\is_array($childPayload)) {
                    $childPayload = [];
                }

                if (!$this->validateRequired($childPayload, $propertyRules)) {
                    return false;
                }
            }

            // Arrays of objects
            if (isset($propertyRules['items']) && \is_array($propertyRules['items'])) {
                $itemsRules = $propertyRules['items'];
                $listPayload = $payload[$propertyName] ?? [];

                if (!\is_array($listPayload)) {
                    if ($this->hasRequired($itemsRules)) {
                        return false;
                    }

                    continue;
                }

                if (!\array_is_list($listPayload)) {
                    if (!$this->validateRequired($listPayload, $itemsRules)) {
                        return false;
                    }

                    continue;
                }

                foreach ($listPayload as $itemPayload) {
                    if (!\is_array($itemPayload)) {
                        if ($this->hasRequired($itemsRules)) {
                            return false;
                        }

                        continue;
                    }

                    if (!$this->validateRequired($itemPayload, $itemsRules)) {
                        return false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * @param array<string, mixed> $rules
     *
     * @return array<int, string>
     */
    private function extractRequired(array $rules): array
    {
        $required = $rules['required'] ?? [];

        return \is_array($required) ? $required : [];
    }

    /**
     * @param array<string, mixed> $rules
     */
    private function hasRequired(array $rules): bool
    {
        return isset($rules['required']) && \is_array($rules['required']) && [] !== $rules['required'];
    }
}
