<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\PersonBundle\Search;

use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\PickGenderType;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use libphonenumber\PhoneNumber;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
{
    final public const NAME = 'person_regular';

    private const POSSIBLE_KEYS = [
        '_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
        'birthdate-after', 'gender', 'nationality', 'phonenumber', 'city',
    ];

    public function __construct(
        private readonly \Twig\Environment $templating,
        private readonly ExtractDateFromPattern $extractDateFromPattern,
        private readonly ExtractPhonenumberFromPattern $extractPhonenumberFromPattern,
        private readonly PaginatorFactory $paginatorFactory,
        private readonly PersonACLAwareRepositoryInterface $personACLAwareRepository,
        private readonly GenderRepository $genderRepository,
    ) {}

    public function buildForm(FormBuilderInterface $builder)
    {
        $builder
            ->add('_default', TextType::class, [
                'label' => 'First name or Last name',
                'required' => false,
            ])
            ->add('firstname', TextType::class, [
                'label' => 'First name',
                'required' => false,
            ])
            ->add('lastname', TextType::class, [
                'label' => 'Last name',
                'required' => false,
            ])
            ->add('birthdate', ChillDateType::class, [
                'label' => 'Birthdate',
                'required' => false,
            ])
            ->add('birthdate-after', ChillDateType::class, [
                'label' => 'Birthdate after',
                'required' => false,
            ])
            ->add('birthdate-before', ChillDateType::class, [
                'label' => 'Birthdate before',
                'required' => false,
            ])
            ->add('phonenumber', ChillPhoneNumberType::class, [
                'required' => false,
                'label' => 'Part of the phonenumber',
            ])
            ->add('gender', PickGenderType::class, [
                'label' => 'Gender',
                'required' => false,
                'expanded' => false,
                'placeholder' => 'All genders',
            ])
            ->add('city', TextType::class, [
                'required' => false,
                'label' => 'City or postal code',
            ]);
    }

    public function convertFormDataToQuery(array $data)
    {
        $string = '@person ';

        $string .= !isset($data['_default']) ? '' : $data['_default'].' ';

        foreach (['firstname', 'lastname', 'city'] as $key) {
            $string .= !isset($data[$key]) ? '' : $key.':'.
                // add quote if contains spaces
                (str_contains((string) $data[$key], ' ') ? '"'.$data[$key].'"' : $data[$key])
                .' ';
        }

        foreach (['birthdate', 'birthdate-before', 'birthdate-after'] as $key) {
            $string .= !isset($data[$key]) ?
                ''
                :
                $key.':'.$data[$key]->format('Y-m-d').' ';
        }

        $string .= !isset($data['phonenumber']) ? '' : 'phonenumber:'.$data['phonenumber']->getNationalNumber();

        $string .= !isset($data['gender']) ? '' : 'gender:"'.$data['gender']->getId().'"';

        return $string;
    }

    public function convertTermsToFormData(array $terms)
    {
        $data = [];

        foreach (['firstname', 'lastname', '_default', 'phonenumber', 'city'] as $key) {
            $data[$key] = $terms[$key] ?? null;
        }

        // parse dates
        foreach (['birthdate', 'birthdate-before', 'birthdate-after'] as $key) {
            if (\array_key_exists($key, $terms)) {
                try {
                    $date = new \DateTime($terms[$key]);
                } catch (\Exception $ex) {
                    throw new ParsingException("The date for {$key} is ".'not parsable', 0, $ex);
                }
            }
            $data[$key] = $date ?? null;
        }

        if (\array_key_exists('phonenumber', $terms)) {
            try {
                $phonenumber = new PhoneNumber();
                $phonenumber->setNationalNumber($terms['phonenumber']);
            } catch (\Exception $ex) {
                throw new ParsingException('The data for phonenumber is not parsable', 0, $ex);
            }

            $data['phonenumber'] = $phonenumber;
        }

        if (array_key_exists('gender', $terms)) {
            $data['gender'] = $this->genderRepository->find((int) $terms['gender']);
        }

        return $data;
    }

    public function getAdvancedSearchTitle(): string
    {
        return 'Search within persons';
    }

    public static function getAlias(): string
    {
        return self::NAME;
    }

    /**
     * (non-PHPdoc).
     *
     * @see \Chill\MainBundle\Search\SearchInterface::getOrder()
     */
    public function getOrder(): int
    {
        return 100;
    }

    /**
     * (non-PHPdoc).
     *
     * @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
     */
    public function isActiveByDefault()
    {
        return true;
    }

    public function renderResult(array $terms, $start = 0, $limit = 50, array $options = [], $format = 'html')
    {
        $terms = $this->findAdditionnalInDefault($terms);
        $total = $this->count($terms);
        $paginator = $this->paginatorFactory->create($total);

        if ('html' === $format) {
            return $this->templating->render(
                '@ChillPerson/Person/list_with_period.html.twig',
                [
                    'persons' => $this->search($terms, $start, $limit, $options),
                    'pattern' => $this->recomposePattern(
                        $terms,
                        \array_filter(self::POSSIBLE_KEYS, static fn ($item) => '_default' !== $item),
                        $terms['_domain']
                    ),
                    'total' => $total,
                    'start' => $start,
                    'search_name' => self::NAME,
                    'preview' => $options[SearchInterface::SEARCH_PREVIEW_OPTION],
                    'paginator' => $paginator,
                ]
            );
        }

        // format is "json"
        return [
            'results' => $this->search($terms, $start, $limit, \array_merge($options, ['simplify' => true])),
            'pagination' => [
                'more' => $paginator->hasNextPage(),
            ],
        ];
    }

    public function supports($domain, $format)
    {
        return 'person' === $domain;
    }

    protected function count(array $terms): int
    {
        [
            '_default' => $default,
            'firstname' => $firstname,
            'lastname' => $lastname,
            'birthdate' => $birthdate,
            'birthdate-before' => $birthdateBefore,
            'birthdate-after' => $birthdateAfter,
            'gender' => $gender,
            'nationality' => $countryCode,
            'phonenumber' => $phonenumber,
            'city' => $city,
        ] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);

        if (null !== $birthdate) {
            try {
                $birthdate = new \DateTime($birthdate);
            } catch (\Exception $e) {
                throw new ParsingException('The date is not parsable', 0, $e);
            }
        }

        if (null !== $birthdateBefore) {
            try {
                $birthdateBefore = new \DateTime($birthdateBefore);
            } catch (\Exception $e) {
                throw new ParsingException('The date is not parsable', 0, $e);
            }
        }

        if (null !== $birthdateAfter) {
            try {
                $birthdateAfter = new \DateTime($birthdateAfter);
            } catch (\Exception $e) {
                throw new ParsingException('The date is not parsable', 0, $e);
            }
        }

        return $this->personACLAwareRepository
            ->countBySearchCriteria(
                $default,
                $firstname,
                $lastname,
                $birthdate,
                $birthdateBefore,
                $birthdateAfter,
                null !== $gender ? (int) $gender : null,
                $countryCode,
                $phonenumber,
                $city
            );
    }

    /**
     * @return Person[]
     */
    protected function search(array $terms, int $start, int $limit, array $options = [])
    {
        [
            '_default' => $default,
            'firstname' => $firstname,
            'lastname' => $lastname,
            'birthdate' => $birthdate,
            'birthdate-before' => $birthdateBefore,
            'birthdate-after' => $birthdateAfter,
            'gender' => $gender,
            'nationality' => $countryCode,
            'phonenumber' => $phonenumber,
            'city' => $city,
        ] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);

        if (null !== $birthdate) {
            try {
                $birthdate = new \DateTime($birthdate);
            } catch (\Exception $e) {
                throw new ParsingException('The date is not parsable', 0, $e);
            }
        }

        if (null !== $birthdateBefore) {
            try {
                $birthdateBefore = new \DateTime($birthdateBefore);
            } catch (\Exception $e) {
                throw new ParsingException('The date is not parsable', 0, $e);
            }
        }

        if (null !== $birthdateAfter) {
            try {
                $birthdateAfter = new \DateTime($birthdateAfter);
            } catch (\Exception $e) {
                throw new ParsingException('The date is not parsable', 0, $e);
            }
        }

        return $this->personACLAwareRepository
            ->findBySearchCriteria(
                $start,
                $limit,
                $options['simplify'] ?? false,
                $default,
                $firstname,
                $lastname,
                $birthdate,
                $birthdateBefore,
                $birthdateAfter,
                null !== $gender ? (int) $gender : null,
                $countryCode,
                $phonenumber,
                $city
            );
    }

    private function findAdditionnalInDefault(array $terms): array
    {
        // chaining some extractor
        $datesResults = $this->extractDateFromPattern->extractDates($terms['_default']);
        $phoneResults = $this->extractPhonenumberFromPattern->extractPhonenumber($datesResults->getFilteredSubject());
        $terms['_default'] = $phoneResults->getFilteredSubject();

        if (
            $datesResults->hasResult() && (!\array_key_exists('birthdate', $terms)
            || null !== $terms['birthdate'])
        ) {
            $terms['birthdate'] = $datesResults->getFound()[0]->format('Y-m-d');
        }

        if (
            $phoneResults->hasResult() && (!\array_key_exists('phonenumber', $terms)
            || null !== $terms['phonenumber'])
        ) {
            $terms['phonenumber'] = $phoneResults->getFound()[0];
        }

        return $terms;
    }
}
