<?php

namespace AutoAdmin\Bundle\Admin\Model;

use AutoAdmin\Bundle\Admin\Search\EntitySearchHelper;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;
use Exception;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;

class EntityFilterModel
{
    protected int $page;
    protected int $items;
    protected mixed $globalSearch = null;
    protected array $fieldSorts = [];
    protected array $sortingOrder = [];
    protected array $fieldSearches = [];
    protected array $qbPrepareCallbacks = [];
    protected array $qbAlterateCallbacks = [];
    protected array $qbOptions = [];

    public function __construct(Request $request, array $searchFormConfig = [])
    {
        $queryParameters = $request->query->all();

        $this->page = !empty($queryParameters['page']) ? (max(1, (int)$queryParameters['page'])) : 1;
        $this->items = !empty($queryParameters['items']) ? (int)$queryParameters['items'] : 25;
        $this->globalSearch = !empty($queryParameters['global_search']) ? $queryParameters['global_search'] : null;

        // sort & search fields
        foreach ($queryParameters as $key => $value) {
            if (strpos($key, 'sort_') === 0 && !empty($value)) {
                $fieldName = explode('_', $key, 2)[1];
                if (substr($fieldName, 0, 4) === '_tr_') {
                    $fieldName = '_tr.' . substr($fieldName, 4);
                } else {
                    $fieldName = '_.' . $fieldName;
                }
                $this->fieldSorts[$fieldName] = $value;
            }

            if (strpos($key, 'search_') === 0 && !empty($value)) {
                $fieldName = explode('_', $key, 2)[1];
                if (substr($fieldName, 0, 4) === '_tr_') {
                    $fieldName = '_tr.' . substr($fieldName, 4);
                } else {
                    $fieldName = '_.' . $fieldName;
                }
                $this->fieldSearches[$fieldName] = $value;
            }
        }

        // sorting order
        if (!empty($queryParameters['sorting_order'])) {
            $sortingOrder = explode(',', $queryParameters['sorting_order']);
            // only use order for fields that are sorted
            foreach ($sortingOrder as $k => $sortIndex) {
                $shortIndex = str_replace('sort_', '', $sortIndex);
                if (substr($shortIndex, 0, 4) === '_tr_') {
                    $shortIndex = '_tr.' . substr($shortIndex, 4);
                }
                $sortingOrder[$k] = $shortIndex;
                if (!array_key_exists($shortIndex, $this->fieldSorts)) {
                    unset($sortingOrder[$k]);
                }
            }
            $this->sortingOrder = $sortingOrder;

            $this->fieldSorts = array_merge(array_flip($this->sortingOrder), $this->fieldSorts);
        }

        // filter params
        $filteredFormData = [];
        if (!empty($searchFormConfig)) {
            foreach ($searchFormConfig as $prop => $config) {
                /** @var FormInterface $form */
                if ($form = $config['form'] ?? null) {
                    $form->handleRequest($request);
                    $data = $form->getData() ?? [];
                    $filteredFormData = array_merge_recursive($filteredFormData, $data);
                }
            }

            // Remove normal filter if filtered by empty
            foreach ($filteredFormData as $key => $value) {
                if (preg_match('#(.*)_empty$#', $key, $matches)) {
                    if ($filteredFormData[$matches[0]]) {
                        unset($filteredFormData[$matches[1]]);
                    } else {
                        unset($filteredFormData[$matches[0]]);
                    }
                }
            }

            foreach ($filteredFormData as $prefixedField => $fieldData) {
                $field = str_replace('filter_', '', $prefixedField);
                $value = $filteredFormData[$field];

                if (preg_match('#(.*)_empty$#', $field, $matches)) {
                    if ($value) {
                        $this->addQbPrepareCallback(
                            function (QueryBuilder $qb) use ($field, $matches) {
                                $qb->leftJoin('_.' . $matches[1], $matches[1]);
                            }
                        );

                        $this->addQbAlterateCallback(
                            function (QueryBuilder $qb) use ($field, $matches, $value) {
                                $qb->andWhere($matches[1] . '.id IS NULL OR ' . $matches[1] . '.id = \'\'');
                            }
                        );
                    }

                    continue;
                }

                $config = $searchFormConfig[$field];
                $mapping = $config['mapping'];
                $type = $mapping['type'];
                if ($type === ClassMetadataInfo::MANY_TO_ONE) {
                    $this->addQbPrepareCallback(
                        function (QueryBuilder $qb) use ($field) {
                            $qb->leftJoin('_.' . $field, $field);
                        }
                    );

                    $this->addQbAlterateCallback(
                        function (QueryBuilder $qb) use ($field, $value) {
                            $filterParamName = $field . '_filter_param';
                            $qb->andWhere($field . '.id = :' . $filterParamName)
                                ->setParameter($filterParamName, $value);
                        }
                    );
                } elseif (!is_int($type)) {
                    if (array_key_exists($type, EntitySearchHelper::getFilterOptions())) {
                        switch ($type) {
                            case 'boolean':
                                $this->addQbAlterateCallback(
                                    function (QueryBuilder $qb) use ($field, $value) {
                                        $filterParamName = $field . '_filter_param';
                                        $qb->andWhere('_.' . $field . ' = :' . $filterParamName)
                                            ->setParameter($filterParamName, $value);
                                    }
                                );
                                break;
                            default:
                                throw new Exception('Unsupported field type encountered "' . $type . '"');
                        }
                    }
                }
            }
        }
    }

    public function addQbPrepareCallback(callable $cb): self
    {
        $this->qbPrepareCallbacks[] = $cb;

        return $this;
    }

    public function addQbAlterateCallback(callable $cb): self
    {
        $this->qbAlterateCallbacks[] = $cb;

        return $this;
    }

    public function isSearching(): bool
    {
        return
            $this->globalSearch != null
            || !empty($this->fieldSearches)
            || !empty($this->qbPrepareCallbacks)
            || !empty($this->qbAlterateCallbacks);
    }

    public function getCurrentPage(): int
    {
        return $this->page;
    }

    public function getMaxItems(): int
    {
        return $this->items;
    }

    public function getGlobalSearch(): mixed
    {
        return trim($this->globalSearch);
    }

    public function getFieldsOrders(): array
    {
        return $this->fieldSorts;
    }

    public function getFieldsSearch(): array
    {
        return $this->fieldSearches;
    }

    public function getQbPrepareCallbacks(): array
    {
        return $this->qbPrepareCallbacks;
    }

    public function getQbAlterateCallbacks(): array
    {
        return $this->qbAlterateCallbacks;
    }

    public function getQbOptions(): array
    {
        return $this->qbOptions;
    }

    public function setQbOptions(array $qbOptions): self
    {
        $this->qbOptions = $qbOptions;

        return $this;
    }
}
