<?php
declare(strict_types=1);

namespace FCA\StockApi\Collection\Filter\Builder;

use FCA\StockApi\Collection\Sort\Sort;
use FCA\StockApi\Exception\ApiException;

class Validator
{
    public static $operators = ['$lte', '$lt', '$gte', '$gt', '$in', '!$in', '$ne', '$all'];

    public static function isValidValue(string $field, $value): bool
    {
        if ($value === 'geometry') {
            if (in_array('geometry', ValueTypes::getValueMap()[$field])) {
                return true;
            }
            return false;
        }

        if (in_array(gettype($value), ValueTypes::getValueMap()[$field])) {
            return true;
        }

        return false;
    }

    public static function isValidField(string $field): bool
    {
        $constants = Fields::getAllFields();

        foreach ($constants as $constant) {
            if ($constant === $field) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param $filter
     * @return bool
     * @throws ApiException
     */
    public static function validateFilter($filter): bool
    {
        if (is_array($filter) and isset($filter['$and']) and count($filter) === 1) {
            foreach ($filter['$and'] as $f) {
                self::validateFilter($f);
            }
        } elseif (is_array($filter) and isset($filter['$and']) and count($filter) !== 1) {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        } elseif (is_array($filter) and isset($filter['$or']) and count($filter) === 1) {
            foreach ($filter['$or'] as $f) {
                self::validateFilter($f);
            }
        } elseif (is_array($filter) and isset($filter['$or']) and count($filter) !== 1) {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        } else {
            self::validateExpression($filter);
        }

        return true;
    }

    public static function validateExpression($filter): void
    {
        if (!is_array($filter) and $filter != []) {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        }

        foreach ($filter as $key => $value) {
            if (is_int($key)) {
                self::validateFilter($value);
                continue;
            }

            if (!self::isValidField($key)) {
                throw new ApiException('Invalid field "' . $key . '"');
            }

            if (!is_array($value) && !self::isValidValue($key, $value)) {
                throw new ApiException('Invalid value type ("' . $key . '")');
            }

            if (is_array($value)) {
                if (count($value) === 1) {
                    if (isset($value['$not'])) {
                        self::validateExpression([$key => $value['$not']]);
                    } elseif (isset($value['$near'])) {
                        if (self::isValidValue($key, 'geometry')) {
                            self::validateNearFilter($value['$near']);
                        } else {
                            throw new ApiException('Invalid filter: ' . json_encode($filter));
                        }
                    } else {
                        self::validatePartialExpression($value, $key);
                    }
                } else {
                    throw new ApiException('Invalid filter: ' . json_encode($value));
                }
            }
        }
    }

    public static function validatePartialExpression(array $filter, string $key): void
    {
        $operator = array_keys($filter)[0];
        $val = $filter[$operator];

        if (!in_array($operator, self::$operators)) {
            throw new ApiException('Invalid operator: ' . $operator);
        }

        if (in_array($operator, ['$in', '$nin', '$all'], true)) {
            if (!is_array($val)) {
                throw new ApiException('Value must be an array!');
            }

            foreach ($val as $val2) {
                if (!self::isValidValue($key, $val2)) {
                    throw new ApiException('Invalid value type ("' . $key . '")');
                }
            }
        } else {
            if (!self::isValidValue($key, $val)) {
                throw new ApiException('Invalid value type for "' . $key . '": ' . gettype($val));
            }
        }
    }

    protected static function validateNearFilter(array $filter): void
    {
        if (count($filter) === 2
            and isset($filter['$geometry'])
            and isset($filter['$maxDistance'])
        ) {
            if (!is_int($filter['$maxDistance'])) {
                throw new ApiException('Invalid filter: minDistance must be integer');
            }
            if (!is_array($filter['$geometry'])) {
                throw new ApiException('Invalid filter: $geometry must be array');
            }
            $geo = $filter['$geometry'];
            if (!isset($geo['type']) or $geo['type'] != 'Point') {
                throw new ApiException('Unsupported geometryType: ' . $filter['$near']['$geometry']['type']);
            }
            if (!isset($geo['coordinates']) or !is_array($geo['coordinates'])) {
                throw new ApiException('Undefined Geo Coordinates');
            }
            if (!is_float($geo['coordinates'][0])
                or !is_float($geo['coordinates'][1])
                or count($geo['coordinates']) > 2
            ) {
                throw new ApiException('Invalid Geo Coordinates');
            }
        } else {
            throw new ApiException('Invalid filter: ' . json_encode($filter));
        }
    }

    public static function isValidSortArray(array $sort): bool
    {
        if ($sort === ['random' => true]) {
            return true;
        }

        foreach ($sort as $key => $value) {
            if (!self::isValidField($key) or ($value !== Sort::ASC and $value !== Sort::DESC)) {
                return false;
            }
        }

        return true;
    }
}
