<?php

namespace bird\helpers;

class bird_request_validator
{
    public static function validate(mixed $value, array $schema, string $path = '$'): array
    {
        $errors = [];
        self::validateValue($value, $schema, $path, $errors);
        return $errors;
    }

    public static function isUuid(string $value): bool
    {
        return preg_match('/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/', $value) === 1;
    }

    private static function validateValue(mixed $value, array $schema, string $path, array &$errors): void
    {
        if (($schema['nullable'] ?? false) && $value === null) {
            return;
        }

        if (isset($schema['oneOf']) && is_array($schema['oneOf'])) {
            self::validateOneOf($value, $schema['oneOf'], $path, $errors);
            return;
        }

        $type = isset($schema['type']) && is_string($schema['type']) ? $schema['type'] : null;
        if ($type !== null) {
            match ($type) {
                'object' => self::validateObject($value, $schema, $path, $errors),
                'array' => self::validateArray($value, $schema, $path, $errors),
                'string' => self::validateString($value, $schema, $path, $errors),
                'int', 'integer' => self::validateInteger($value, $schema, $path, $errors),
                'number' => self::validateNumber($value, $schema, $path, $errors),
                'bool', 'boolean' => self::validateBoolean($value, $schema, $path, $errors),
                default => $errors[] = $path . ' has unsupported schema type "' . $type . '"',
            };
        }

        if (isset($schema['enum']) && is_array($schema['enum'])) {
            self::validateEnum($value, $schema['enum'], $path, $errors);
        }
    }

    private static function validateOneOf(mixed $value, array $schemas, string $path, array &$errors): void
    {
        $bestErrors = null;
        foreach ($schemas as $schema) {
            if (!is_array($schema)) {
                continue;
            }
            $tmp = [];
            self::validateValue($value, $schema, $path, $tmp);
            if ($tmp === []) {
                return;
            }
            if ($bestErrors === null || count($tmp) < count($bestErrors)) {
                $bestErrors = $tmp;
            }
        }

        $errors[] = $path . ' does not match any of the allowed schemas';
        if (is_array($bestErrors)) {
            foreach ($bestErrors as $err) {
                $errors[] = $err;
            }
        }
    }

    private static function validateObject(mixed $value, array $schema, string $path, array &$errors): void
    {
        if (!is_array($value) || !self::isAssoc($value)) {
            $errors[] = $path . ' must be an object';
            return;
        }

        $required = isset($schema['required']) && is_array($schema['required']) ? $schema['required'] : [];
        foreach ($required as $requiredKey) {
            if (!array_key_exists((string)$requiredKey, $value)) {
                $errors[] = $path . '.' . $requiredKey . ' is required';
            }
        }

        $properties = isset($schema['properties']) && is_array($schema['properties']) ? $schema['properties'] : [];
        $allowAdditional = (bool)($schema['additionalProperties'] ?? true);

        if (!$allowAdditional) {
            foreach ($value as $key => $_unused) {
                if (!array_key_exists((string)$key, $properties)) {
                    $errors[] = $path . '.' . $key . ' is not allowed';
                }
            }
        }

        foreach ($properties as $key => $propertySchema) {
            if (!array_key_exists((string)$key, $value)) {
                continue;
            }
            if (!is_array($propertySchema)) {
                continue;
            }
            self::validateValue($value[(string)$key], $propertySchema, $path . '.' . $key, $errors);
        }
    }

    private static function validateArray(mixed $value, array $schema, string $path, array &$errors): void
    {
        $arrayValue = $value;
        if (is_string($arrayValue) && ($schema['csv'] ?? false)) {
            $parts = array_map('trim', explode(',', $arrayValue));
            $arrayValue = array_values(array_filter($parts, static fn($part) => $part !== ''));
        }

        if (!is_array($arrayValue) || self::isAssoc($arrayValue)) {
            $errors[] = $path . ' must be an array';
            return;
        }

        $count = count($arrayValue);
        if (isset($schema['minItems']) && is_numeric($schema['minItems']) && $count < (int)$schema['minItems']) {
            $errors[] = $path . ' must have at least ' . (int)$schema['minItems'] . ' items';
        }
        if (isset($schema['maxItems']) && is_numeric($schema['maxItems']) && $count > (int)$schema['maxItems']) {
            $errors[] = $path . ' must have at most ' . (int)$schema['maxItems'] . ' items';
        }

        $itemSchema = isset($schema['items']) && is_array($schema['items']) ? $schema['items'] : null;
        if ($itemSchema === null) {
            return;
        }

        foreach ($arrayValue as $index => $item) {
            self::validateValue($item, $itemSchema, $path . '[' . $index . ']', $errors);
        }
    }

    private static function validateString(mixed $value, array $schema, string $path, array &$errors): void
    {
        if (!is_string($value)) {
            $errors[] = $path . ' must be a string';
            return;
        }

        $length = strlen($value);
        if (isset($schema['minLength']) && is_numeric($schema['minLength']) && $length < (int)$schema['minLength']) {
            $errors[] = $path . ' must be at least ' . (int)$schema['minLength'] . ' characters';
        }
        if (isset($schema['maxLength']) && is_numeric($schema['maxLength']) && $length > (int)$schema['maxLength']) {
            $errors[] = $path . ' must be at most ' . (int)$schema['maxLength'] . ' characters';
        }

        if (isset($schema['pattern']) && is_string($schema['pattern']) && @preg_match('/' . $schema['pattern'] . '/', $value) !== 1) {
            $errors[] = $path . ' has invalid format';
        }

        if (isset($schema['format']) && is_string($schema['format'])) {
            if ($schema['format'] === 'uuid' && !self::isUuid($value)) {
                $errors[] = $path . ' must be a UUID';
            }
            if ($schema['format'] === 'date-time' && strtotime($value) === false) {
                $errors[] = $path . ' must be a valid date-time';
            }
        }
    }

    private static function validateInteger(mixed $value, array $schema, string $path, array &$errors): void
    {
        $intValue = self::toInt($value);
        if ($intValue === null) {
            $errors[] = $path . ' must be an integer';
            return;
        }

        if (isset($schema['min']) && is_numeric($schema['min']) && $intValue < (int)$schema['min']) {
            $errors[] = $path . ' must be at least ' . (int)$schema['min'];
        }
        if (isset($schema['max']) && is_numeric($schema['max']) && $intValue > (int)$schema['max']) {
            $errors[] = $path . ' must be at most ' . (int)$schema['max'];
        }
    }

    private static function validateNumber(mixed $value, array $schema, string $path, array &$errors): void
    {
        $numberValue = self::toFloat($value);
        if ($numberValue === null) {
            $errors[] = $path . ' must be a number';
            return;
        }

        if (isset($schema['min']) && is_numeric($schema['min']) && $numberValue < (float)$schema['min']) {
            $errors[] = $path . ' must be at least ' . (float)$schema['min'];
        }
        if (isset($schema['max']) && is_numeric($schema['max']) && $numberValue > (float)$schema['max']) {
            $errors[] = $path . ' must be at most ' . (float)$schema['max'];
        }
    }

    private static function validateBoolean(mixed $value, array $schema, string $path, array &$errors): void
    {
        if (self::toBool($value) === null) {
            $errors[] = $path . ' must be a boolean';
        }
    }

    private static function validateEnum(mixed $value, array $allowedValues, string $path, array &$errors): void
    {
        if (in_array($value, $allowedValues, true)) {
            return;
        }

        if (is_scalar($value)) {
            foreach ($allowedValues as $allowed) {
                if (is_scalar($allowed) && (string)$allowed === (string)$value) {
                    return;
                }
            }
        }

        $errors[] = $path . ' must be one of: ' . implode(', ', array_map(static fn($item) => (string)$item, $allowedValues));
    }

    private static function toInt(mixed $value): ?int
    {
        if (is_int($value)) {
            return $value;
        }
        if (is_string($value) && preg_match('/^-?[0-9]+$/', $value) === 1) {
            return (int)$value;
        }
        return null;
    }

    private static function toFloat(mixed $value): ?float
    {
        if (is_int($value) || is_float($value)) {
            return (float)$value;
        }
        if (is_string($value) && is_numeric($value)) {
            return (float)$value;
        }
        return null;
    }

    private static function toBool(mixed $value): ?bool
    {
        if (is_bool($value)) {
            return $value;
        }
        if (is_int($value) && ($value === 0 || $value === 1)) {
            return $value === 1;
        }
        if (is_string($value)) {
            $normalized = strtolower(trim($value));
            if ($normalized === 'true' || $normalized === '1') {
                return true;
            }
            if ($normalized === 'false' || $normalized === '0') {
                return false;
            }
        }
        return null;
    }

    private static function isAssoc(array $array): bool
    {
        return array_keys($array) !== range(0, count($array) - 1);
    }
}

