<?php

declare(strict_types=1);

namespace Core\General\Validation\Service;

use Carbon\Carbon;
use Core\General\Helper\DateTimeHelper;
use Core\General\Helper\RegexHelper;
use Core\General\Translation\ValueObject\TranslationTuple;
use Core\General\Validation\Enum\ArrayValueType;
use Core\General\Validation\ValueObject\RegexCondition;

class PayloadValidator
{
    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, mixed>|null - Returns null if early return should take effect
     */
    public function validateRequired(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
    ): ?array {
        if ($validIfNotSet && !\array_key_exists($key, $data)) {
            return null;
        }

        if (!\array_key_exists($key, $data)) {
            return [$key => 'field_error.required'];
        }

        if ($validIfNull && $data[$key] === null) {
            return null;
        }

        if ($data[$key] === null) {
            return [$key => 'field_error.required'];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, string>
     */
    protected function validateBoolean(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_bool($data[$key])) {
            return [$key => 'field_error.boolean.type'];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, mixed>
     */
    protected function validateString(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        ?int $minLength = null,
        ?int $maxLength = 255,
        array $validValues = [],
        ?RegexCondition $regexCondition = null,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_string($data[$key])) {
            return [$key => 'field_error.string.type'];
        }

        if (!$validIfEmpty && trim($data[$key]) === '') {
            return [$key => 'field_error.string.non_empty'];
        }

        if ($validValues !== []) {
            if (\in_array($data[$key], $validValues)) {
                return []; // Means all values are valid, thus no additional checks are required
            }

            return [$key => new TranslationTuple(
                'field_error.invalid_value',
                [
                    '%validValues%' => implode(', ', $validValues),
                ]
            )];
        }

        $lengthError = $this->validateStringLength($data, $key, $minLength, $maxLength);
        if (!empty($lengthError)) {
            return $lengthError;
        }

        if ($regexCondition !== null && !@preg_match($regexCondition->getExpression(), $data[$key])) {
            return [$key => $regexCondition->getErrorMessage()];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, mixed>
     */
    private function validateStringLength(
        array $data,
        string $key,
        ?int $minLength,
        ?int $maxLength,
    ): array {
        if ($minLength !== null && $maxLength !== null) {
            if (\strlen($data[$key]) < $minLength || \strlen($data[$key]) > $maxLength) {
                return [$key => new TranslationTuple(
                    'field_error.string.length.between',
                    [
                        '%minLength%' => $minLength,
                        '%maxLength%' => $maxLength,
                    ]
                )];
            }
        }

        if ($minLength !== null && \strlen($data[$key]) < $minLength) {
            return [$key => new TranslationTuple(
                'field_error.string.length.min',
                ['%minLength%' => $minLength]
            )];
        }

        if ($maxLength !== null && \strlen($data[$key]) > $maxLength) {
            return [$key => new TranslationTuple(
                'field_error.string.length.max',
                ['%maxLength%' => $maxLength]
            )];
        }

        return [];
    }

    /**
     * @param int|float|numeric-string $value
     *
     * @return array<string, mixed>
     */
    private function validateNumberValue(
        int|float|string $value,
        string $key,
        ?float $minValue,
        ?float $maxValue,
    ): array {
        $value = (float) $value;

        if ($minValue !== null && $maxValue !== null) {
            if ($value < $minValue || $value > $maxValue) {
                return [$key => new TranslationTuple(
                    'field_error.numeric.value.between',
                    [
                        '%minLength%' => $minValue,
                        '%maxLength%' => $maxValue,
                    ]
                )];
            }
        }

        if ($minValue !== null && $value < $minValue) {
            return [$key => new TranslationTuple(
                'field_error.numeric.value.min',
                ['%minLength%' => $minValue]
            )];
        }

        if ($maxValue !== null && $value > $maxValue) {
            return [$key => new TranslationTuple(
                'field_error.numeric.value.max',
                ['%maxLength%' => $maxValue]
            )];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param int[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateInteger(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        array $validValues = [],
        ?int $minValue = null,
        ?int $maxValue = null,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_int($data[$key])) {
            return [$key => 'field_error.integer.type'];
        }

        if ($validValues !== []) {
            if (\in_array($data[$key], $validValues)) {
                return []; // Means all values are valid, thus no additional checks are required
            }

            return [$key => new TranslationTuple(
                'field_error.invalid_value',
                [
                    '%validValues%' => implode(', ', $validValues),
                ]
            )];
        }

        $valueErrors = $this->validateNumberValue(
            $data[$key],
            $key,
            $minValue,
            $maxValue,
        );

        if (!empty($valueErrors)) {
            return $valueErrors;
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param float[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateFloat(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        array $validValues = [],
        ?float $minValue = null,
        ?float $maxValue = null,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_float($data[$key]) && !\is_int($data[$key])) {
            return [$key => 'field_error.float.type'];
        }

        if ($validValues !== []) {
            if (\in_array($data[$key], $validValues)) {
                return []; // Means all values are valid, thus no additional checks are required
            }

            return [$key => new TranslationTuple(
                'field_error.invalid_value',
                [
                    '%validValues%' => implode(', ', $validValues),
                ]
            )];
        }

        $valueErrors = $this->validateNumberValue(
            $data[$key],
            $key,
            $minValue,
            $maxValue,
        );

        if (!empty($valueErrors)) {
            return $valueErrors;
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param numeric-string[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateNumeric(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        array $validValues = [],
        ?int $minValue = null,
        ?int $maxValue = null,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!is_numeric($data[$key])) {
            return [$key => 'field_error.numeric.type'];
        }

        if ($validValues !== []) {
            if (\in_array($data[$key], $validValues)) {
                return []; // Means all values are valid, thus no additional checks are required
            }

            return [$key => new TranslationTuple(
                'field_error.invalid_value',
                [
                    '%validValues%' => implode(', ', $validValues),
                ]
            )];
        }

        $valueErrors = $this->validateNumberValue(
            $data[$key],
            $key,
            $minValue,
            $maxValue,
        );

        if (!empty($valueErrors)) {
            return $valueErrors;
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param value-of<DateTimeHelper::VALID_FORMATS> $format
     *
     * @return array<string, string>
     */
    protected function validateDateTime(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        string $format = DateTimeHelper::DEFAULT_FORMAT,
        ?Carbon $minDate = null,
        ?Carbon $maxDate = null,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        $dateTime = DateTimeHelper::buildCarbonObject($data[$key], $format);

        if (!\is_string($data[$key]) || $dateTime === false) {
            return match ($format) {
                DateTimeHelper::YMDHIS => [$key => 'field_error.datetime.format.ymd'],
                DateTimeHelper::YMD => [$key => 'field_error.datetime.format.ymdhis'],
                DateTimeHelper::HIS => [$key => 'field_error.datetime.format.his'],
                default => [$key => 'field_error.datetime.format.iso8601'],
            };
        }

        $valueErrors = $this->validateDateTimeValue(
            $dateTime,
            $key,
            $format,
            $minDate,
            $maxDate,
        );

        if (!empty($valueErrors)) {
            return $valueErrors;
        }

        return [];
    }

    /**
     * @param value-of<DateTimeHelper::VALID_FORMATS> $format
     *
     * @return array<string, mixed>
     */
    private function validateDateTimeValue(
        Carbon $value,
        string $key,
        string $format,
        ?Carbon $minDate,
        ?Carbon $maxDate,
    ): array {
        if ($minDate !== null && $maxDate !== null) {
            if ($value->isBefore($minDate) || $value->isAfter($maxDate)) {
                return [$key => new TranslationTuple(
                    'field_error.datetime.value.between',
                    [
                        '%startDateTime%' => $minDate->format($format),
                        '%endDateTime%' => $maxDate->format($format),
                    ]
                )];
            }
        }

        if ($minDate !== null && $value->isBefore($minDate)) {
            return [$key => new TranslationTuple(
                'field_error.datetime.value.min',
                ['%dateTime%' => $minDate->format($format)]
            )];
        }

        if ($maxDate !== null && $value->isAfter($maxDate)) {
            return [$key => new TranslationTuple(
                'field_error.datetime.value.max',
                ['%dateTime%' => $maxDate->format($format)]
            )];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, string>
     */
    protected function validateUuid(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_string($data[$key]) || !preg_match(RegexHelper::getUuidExpression(), $data[$key])) {
            return [$key => 'field_error.uuid.type'];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, mixed>
     */
    protected function validateEmail(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        array $validValues = [],
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_string($data[$key]) || !filter_var($data[$key], \FILTER_VALIDATE_EMAIL)) {
            return [$key => 'email.not.valid'];
        }

        if ($validValues !== [] && !\in_array($data[$key], $validValues)) {
            return [$key => new TranslationTuple(
                'field_error.invalid_value',
                [
                    '%validValues%' => implode(', ', $validValues),
                ]
            )];
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param mixed[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArray(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
        ArrayValueType $expectedValueType = ArrayValueType::ANY,
    ): array {
        $errors = $this->validateRequired(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
        );

        if ($errors === null) {
            return [];
        }

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

        if (!\is_array($data[$key])) {
            return [$key => \sprintf('field_error.array.value_type.%s', $expectedValueType->value)];
        }

        if (!$validIfEmpty && $data[$key] === []) {
            return [$key => \sprintf('field_error.array.value_type.%s', $expectedValueType->value)];
        }

        if ($validValues !== []) {
            if (empty(array_diff($data[$key], $validValues))) {
                return []; // Means all values are valid, thus no additional checks are required
            }

            return [$key => new TranslationTuple(
                'field_error.array.invalid_values',
                [
                    '%validValues%' => implode(', ', $validValues),
                ]
            )];
        }

        foreach ($data[$key] as $value) {
            switch (true) {
                case $expectedValueType === ArrayValueType::STRING && !\is_string($value):
                    return [$key => 'field_error.array.value_type.string'];
                case $expectedValueType === ArrayValueType::INTEGER && !\is_int($value):
                    return [$key => 'field_error.array.value_type.integer'];
                case $expectedValueType === ArrayValueType::NUMERIC && !is_numeric($value):
                    return [$key => 'field_error.array.value_type.numeric'];
                case $expectedValueType === ArrayValueType::UUID && (!\is_string($value) || !preg_match(RegexHelper::getUuidExpression(), $value)):
                    return [$key => 'field_error.array.value_type.uuid'];
                case $expectedValueType === ArrayValueType::EMAIL && (!\is_string($value) || !filter_var($value, \FILTER_VALIDATE_EMAIL)):
                    return [$key => 'field_error.array.value_type.email'];
                case $expectedValueType === ArrayValueType::OBJECT && !\is_object($value):
                    return [$key => 'field_error.array.value_type.object'];
                case $expectedValueType === ArrayValueType::ARRAY && !\is_array($value):
                    return [$key => 'field_error.array.value_type.array'];
            }
        }

        return [];
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsStringValuesOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::STRING,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param int[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsIntegerValuesOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::INTEGER,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param int[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsNumericValuesOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::NUMERIC,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsUuidsOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::UUID,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsEmailValuesOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::EMAIL,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsObjectValuesOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::OBJECT,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateArrayContainsArrayValuesOnly(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        array $validValues = [],
    ): array {
        return $this->validateArray(
            $data,
            $key,
            $validIfNull,
            $validIfNotSet,
            $validIfEmpty,
            $validValues,
            ArrayValueType::ARRAY,
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, mixed>
     */
    protected function validateCamelCase(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        ?int $minLength = null,
        ?int $maxLength = null,
        array $validValues = [],
    ): array {
        return $this->validateString(
            data: $data,
            key: $key,
            validIfNull: $validIfNull,
            validIfNotSet: $validIfNotSet,
            validIfEmpty: $validIfEmpty,
            minLength: $minLength,
            maxLength: $maxLength,
            validValues: $validValues,
            regexCondition: new RegexCondition(
                RegexHelper::CAMEL_CASE_STRING,
                'field_error.array.camel_case'
            ),
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, mixed>
     */
    protected function validateKebabCase(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        ?int $minLength = null,
        ?int $maxLength = null,
        array $validValues = [],
    ): array {
        return $this->validateString(
            data: $data,
            key: $key,
            validIfNull: $validIfNull,
            validIfNotSet: $validIfNotSet,
            validIfEmpty: $validIfEmpty,
            minLength: $minLength,
            maxLength: $maxLength,
            validValues: $validValues,
            regexCondition: new RegexCondition(
                RegexHelper::KEBAB_CASE_STRING,
                'field_error.array.kebab_case'
            ),
        );
    }

    /**
     * @param array<string, mixed> $data
     * @param string[] $validValues
     *
     * @return array<string, mixed>
     */
    protected function validateSnakeCase(
        array $data,
        string $key,
        bool $validIfNull = false,
        bool $validIfNotSet = false,
        bool $validIfEmpty = true,
        ?int $minLength = null,
        ?int $maxLength = null,
        array $validValues = [],
    ): array {
        return $this->validateString(
            data: $data,
            key: $key,
            validIfNull: $validIfNull,
            validIfNotSet: $validIfNotSet,
            validIfEmpty: $validIfEmpty,
            minLength: $minLength,
            maxLength: $maxLength,
            validValues: $validValues,
            regexCondition: new RegexCondition(
                RegexHelper::SNAKE_CASE_STRING,
                'field_error.array.snake_case'
            ),
        );
    }

    /**
     * @param array<string, mixed> $data
     *
     * @return array<string, string|TranslationTuple>
     */
    protected function validateLatitudeAndLongitude(
        array $data,
        string $latitudeKey = 'latitude',
        string $longitudeKey = 'longitude',
        bool $validIfNull = false,
        bool $validIfNotSet = false,
    ): array {
        $latitudeErrors = $this->validateNumeric(
            data: $data,
            key: $latitudeKey,
            validIfNull: $validIfNull,
            validIfNotSet: $validIfNotSet,
            minValue: -90,
            maxValue: 90,
        );

        $longitudeErrors = $this->validateNumeric(
            data: $data,
            key: $longitudeKey,
            validIfNull: $validIfNull,
            validIfNotSet: $validIfNotSet,
            minValue: -180,
            maxValue: 180,
        );

        return array_merge(
            [],
            $latitudeErrors,
            $longitudeErrors,
        );
    }
}
