<?php

namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\ClassMetadataInfo;

#[ApiFilter(CustomJsonFilter::class)]
class CustomJsonFilter extends AbstractFilter
{
    private const FILTER_NAME = 'custom_json_filter';

    protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
    {
        if ($property !== self::FILTER_NAME) {
            return;
        }

        $filters = json_decode($value, true);
        if (!is_array($filters)) {
            return;
        }

        $this->applyFilters($queryBuilder, $queryNameGenerator, $resourceClass, $filters);
    }

    private function applyFilters(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $filters, string $alias = 'o', string $prefix = ''): void
    {
        $metadata = $this->getClassMetadata($resourceClass);

        foreach ($filters as $field => $fieldValue) {
            $propertyName = $prefix ? $prefix . '.' . $field : $field;

            if (!$this->isValidProperty($metadata, $field)) {
                continue;
            }

            if ($this->isAssociation($metadata, $field)) {
                $this->handleAssociation($queryBuilder, $queryNameGenerator, $resourceClass, $field, $fieldValue, $alias, $propertyName);
            } elseif (is_bool($fieldValue) || (is_string($fieldValue) && in_array(strtolower($fieldValue), ['true', 'false', '0', '1']))) {
                $this->addBooleanFilter($queryBuilder, $queryNameGenerator, $field, $fieldValue, $alias);
            } elseif (is_array($fieldValue)) {
                if (isset($fieldValue['start']) || isset($fieldValue['end'])) {
                    $this->addFlexibleDateRangeFilter($queryBuilder, $queryNameGenerator, $field, $fieldValue, $alias);
                } elseif (isset($fieldValue['min']) || isset($fieldValue['max'])) {
                    $this->addNumericRangeFilter($queryBuilder, $queryNameGenerator, $field, $fieldValue, $alias);
                } else {
                    $this->addInFilter($queryBuilder, $queryNameGenerator, $field, $fieldValue, $alias);
                }
            } elseif (is_string($fieldValue)) {
                $this->addLikeFilter($queryBuilder, $queryNameGenerator, $field, $fieldValue, $alias);
            }
        }
    }

    private function addLikeFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $field, string $value, string $alias): void
    {
        $parameterName = $queryNameGenerator->generateParameterName($field);
        $queryBuilder
            ->andWhere(sprintf('%s.%s LIKE :%s', $alias, $field, $parameterName))
            ->setParameter($parameterName, '%' . $value . '%');
    }

    private function isValidProperty(ClassMetadata $metadata, string $property): bool
    {
        return $metadata->hasField($property) || $metadata->hasAssociation($property);
    }

    private function isAssociation(ClassMetadata $metadata, string $property): bool
    {
        return $metadata->hasAssociation($property);
    }

    private function handleAssociation(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $field, $fieldValue, string $alias, string $propertyName): void
    {
        $associationAlias = $queryNameGenerator->generateJoinAlias($field);
        $queryBuilder->leftJoin($alias . '.' . $field, $associationAlias);

        if (is_array($fieldValue)) {
            $associationClass = $this->getClassMetadata($resourceClass)->getAssociationTargetClass($field);
            $this->applyFilters($queryBuilder, $queryNameGenerator, $associationClass, $fieldValue, $associationAlias, $propertyName);
        }
    }

    private function addBooleanFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $field, $value, string $alias): void
    {
        $parameterName = $queryNameGenerator->generateParameterName($field);
        $boolValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
        if ($boolValue !== null) {
            $queryBuilder
                ->andWhere(sprintf('%s.%s = :%s', $alias, $field, $parameterName))
                ->setParameter($parameterName, $boolValue);
        }
    }

    private function addFlexibleDateRangeFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $field, array $dateRange, string $alias): void
    {
        $conditions = [];
        $parameters = [];

        if (!empty($dateRange['start'])) {
            $startParamName = $queryNameGenerator->generateParameterName($field . '_start');
            $conditions[] = sprintf('%s.%s >= :%s', $alias, $field, $startParamName);
            $parameters[$startParamName] = new \DateTime($dateRange['start']);
        }

        if (!empty($dateRange['end'])) {
            $endParamName = $queryNameGenerator->generateParameterName($field . '_end');
            $conditions[] = sprintf('%s.%s <= :%s', $alias, $field, $endParamName);
            $parameters[$endParamName] = new \DateTime($dateRange['end']);
        }

        if (!empty($conditions)) {
            $queryBuilder->andWhere(implode(' AND ', $conditions));
            foreach ($parameters as $name => $value) {
                $queryBuilder->setParameter($name, $value, Types::DATETIME_MUTABLE);
            }
        }
    }

    private function addNumericRangeFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $field, array $range, string $alias): void
    {
        $conditions = [];
        $parameters = [];

        if (isset($range['min']) && is_numeric($range['min'])) {
            $minParamName = $queryNameGenerator->generateParameterName($field . '_min');
            $conditions[] = sprintf('%s.%s >= :%s', $alias, $field, $minParamName);
            $parameters[$minParamName] = $range['min'];
        }

        if (isset($range['max']) && is_numeric($range['max'])) {
            $maxParamName = $queryNameGenerator->generateParameterName($field . '_max');
            $conditions[] = sprintf('%s.%s <= :%s', $alias, $field, $maxParamName);
            $parameters[$maxParamName] = $range['max'];
        }

        if (!empty($conditions)) {
            $queryBuilder->andWhere(implode(' AND ', $conditions));
            foreach ($parameters as $name => $value) {
                $queryBuilder->setParameter($name, $value);
            }
        }
    }

    private function addInFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $field, array $values, string $alias): void
    {
        if (empty($values)) {
            return;
        }

        $parameterName = $queryNameGenerator->generateParameterName($field);
        $queryBuilder
            ->andWhere(sprintf('%s.%s IN (:%s)', $alias, $field, $parameterName))
            ->setParameter($parameterName, $values);
    }

    public function getDescription(string $resourceClass): array
    {
        return [
            self::FILTER_NAME => [
                'property' => null,
                'type' => 'string',
                'required' => false,
                'swagger' => [
                    'description' => 'Filter items using a JSON object. Supports boolean values, flexible date ranges, numeric ranges, IN operations, and subresource filtering. Example: {"buy": true, "candidateStat": {"rev": {"min": 10000, "max": 50000}}, "logType": ["alert", "info", "preview_pack"]}',
                    'name' => self::FILTER_NAME,
                    'type' => 'string',
                ],
            ],
        ];
    }
}