<?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\Repository;

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use Symfony\Component\Security\Core\Security;

final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
{
    public function __construct(
        private Security $security,
        private EntityManagerInterface $em,
        private CountryRepository $countryRepository,
        private AuthorizationHelperInterface $authorizationHelper,
        private PersonIdentifierManagerInterface $personIdentifierManager,
    ) {}

    public function buildAuthorizedQuery(
        ?string $default = null,
        ?string $firstname = null,
        ?string $lastname = null,
        ?\DateTimeInterface $birthdate = null,
        ?\DateTimeInterface $birthdateBefore = null,
        ?\DateTimeInterface $birthdateAfter = null,
        ?int $gender = null,
        ?string $countryCode = null,
        ?string $phonenumber = null,
        ?string $city = null,
    ): SearchApiQuery {
        $query = $this->createSearchQuery(
            $default,
            $firstname,
            $lastname,
            $birthdate,
            $birthdateBefore,
            $birthdateAfter,
            $gender,
            $countryCode,
            $phonenumber,
            $city
        );

        return $this->addAuthorizations($query);
    }

    public function countBySearchCriteria(
        ?string $default = null,
        ?string $firstname = null,
        ?string $lastname = null,
        ?\DateTimeInterface $birthdate = null,
        ?\DateTimeInterface $birthdateBefore = null,
        ?\DateTimeInterface $birthdateAfter = null,
        ?int $gender = null,
        ?string $countryCode = null,
        ?string $phonenumber = null,
        ?string $city = null,
    ): int {
        $query = $this->buildAuthorizedQuery(
            $default,
            $firstname,
            $lastname,
            $birthdate,
            $birthdateBefore,
            $birthdateAfter,
            $gender,
            $countryCode,
            $phonenumber,
            $city
        );

        return $this->fetchQueryCount($query);
    }

    /**
     * Create a search query without ACL.
     *
     * @throws NonUniqueResultException
     * @throws ParsingException
     */
    public function createSearchQuery(
        ?string $default = null,
        ?string $firstname = null,
        ?string $lastname = null,
        ?\DateTimeInterface $birthdate = null,
        ?\DateTimeInterface $birthdateBefore = null,
        ?\DateTimeInterface $birthdateAfter = null,
        ?int $gender = null,
        ?string $countryCode = null,
        ?string $phonenumber = null,
        ?string $city = null,
    ): SearchApiQuery {
        $query = new SearchApiQuery();
        $query
            ->setFromClause('chill_person_person AS person');

        $idDefinitionWorkers = array_map(
            fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->getId(),
            array_filter(
                $this->personIdentifierManager->getWorkers(),
                fn (PersonIdentifierWorker $worker) => $worker->getDefinition()->isSearchable()
            )
        );
        $idDefinitionWorkerQuestionMarks = implode(', ', array_fill(0, count($idDefinitionWorkers), '?'));

        $pertinence = [];
        $pertinenceArgs = [];
        $andWhereSearchClause = [];
        $andWhereSearchClauseArgs = [];

        if ('' !== trim((string) $default)) {
            foreach (\explode(' ', (string) $default) as $str) {
                if ('' === trim($str)) {
                    continue;
                }
                $pertinence[] =
                    'STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) + '.
                    "(person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%')::int + ".
                    "(EXISTS (SELECT 1 FROM unnest(string_to_array(fullnamecanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int + ".
                    '(starts_with(LOWER(UNACCENT(lastname)), UNACCENT(LOWER(?))))::int';
                \array_push($pertinenceArgs, $str, $str, $str, $str);

                $q = [
                    'LOWER(UNACCENT(?)) <<% person.fullnamecanonical',
                    "person.fullnamecanonical LIKE '%' || LOWER(UNACCENT(?)) || '%' ",
                ];
                $qArguments = [$str, $str];

                if (count($idDefinitionWorkers) > 0) {
                    $q[] = $mq = "EXISTS (
                        SELECT 1 FROM chill_person_identifier AS identifier
                        WHERE identifier.canonical LIKE LOWER(UNACCENT(?)) || '%' AND identifier.definition_id IN ({$idDefinitionWorkerQuestionMarks})
                        AND person.id = identifier.person_id
                    )";
                    $pertinence[] = "({$mq})::int * 1000000";
                    $qArguments = [...$qArguments, $str, ...$idDefinitionWorkers];
                    $pertinenceArgs = [...$pertinenceArgs, $str, ...$idDefinitionWorkers];
                }

                $andWhereSearchClause[] = '('.implode(' OR ', $q).')';
                $andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, ...$qArguments];
            }

        } else {
            $pertinence = ['1'];
            $pertinenceArgs = [];
        }

        if (null !== $phonenumber) {
            $personPhoneClause = "person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%')";
            if (count($andWhereSearchClauseArgs) > 0) {
                $initialSearchClause = '(('.\implode(' AND ', $andWhereSearchClause).') OR '.$personPhoneClause.')';
            }
            $andWhereSearchClauseArgs = [...$andWhereSearchClauseArgs, $phonenumber, $phonenumber, $phonenumber];

            // drastically increase pertinence
            $pertinence[] = "(person.phonenumber LIKE '%' || ? || '%' OR person.mobilenumber LIKE '%' || ? || '%' OR EXISTS (SELECT 1 FROM chill_person_phone where person_id = person.id AND phonenumber LIKE '%' || ? || '%'))::int * 1000000";
            $pertinenceArgs = [...$pertinenceArgs, $phonenumber, $phonenumber, $phonenumber];
        } else {
            $initialSearchClause = \implode(' AND ', $andWhereSearchClause);
        }

        if (isset($initialSearchClause)) {
            $query->andWhereClause(
                $initialSearchClause,
                $andWhereSearchClauseArgs
            );
        }

        $query
            ->setSelectPertinence(\implode(' + ', $pertinence), $pertinenceArgs);

        if (null !== $birthdate) {
            $query->andWhereClause(
                'person.birthdate = ?::date',
                [$birthdate->format('Y-m-d')]
            );
        }

        if (null !== $firstname) {
            $query->andWhereClause(
                "UNACCENT(LOWER(person.firstname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
                [$firstname]
            );
        }

        if (null !== $lastname) {
            $query->andWhereClause(
                "UNACCENT(LOWER(person.lastname)) LIKE '%' || UNACCENT(LOWER(?)) || '%'",
                [$lastname]
            );
        }

        if (null !== $birthdateBefore) {
            $query->andWhereClause(
                'person.birthdate <= ?::date',
                [$birthdateBefore->format('Y-m-d')]
            );
        }

        if (null !== $birthdateAfter) {
            $query->andWhereClause(
                'person.birthdate >= ?::date',
                [$birthdateAfter->format('Y-m-d')]
            );
        }

        if (null !== $city) {
            $query->setFromClause($query->getFromClause().' '.
                'JOIN view_chill_person_current_address vcpca ON vcpca.person_id = person.id '.
                'JOIN chill_main_address cma ON vcpca.address_id = cma.id '.
                'JOIN chill_main_postal_code cmpc ON cma.postcode_id = cmpc.id');

            foreach (\explode(' ', $city) as $cityStr) {
                $query->andWhereClause(
                    "(UNACCENT(LOWER(cmpc.label)) LIKE '%' || UNACCENT(LOWER(?)) || '%' OR cmpc.code LIKE '%' || UNACCENT(LOWER(?)) || '%')",
                    [$cityStr, $city]
                );
            }
        }

        if (null !== $countryCode) {
            $query->setFromClause($query->getFromClause().' JOIN country ON person.nationality_id = country.id');
            $query->andWhereClause('country.countrycode = UPPER(?)', [$countryCode]);
        }

        if (null !== $gender) {
            $query->andWhereClause('person.gender_id = ?', [$gender]);
        }

        return $query;
    }

    public function fetchQueryCount(SearchApiQuery $query): int
    {
        $rsm = new Query\ResultSetMapping();
        $rsm->addScalarResult('c', 'c');

        $nql = $this->em->createNativeQuery($query->buildQuery(true), $rsm);
        $nql->setParameters($query->buildParameters(true));

        return $nql->getSingleScalarResult();
    }

    /**
     * @return array|Person[]
     */
    public function fetchQueryPerson(SearchApiQuery $query, ?int $start = 0, ?int $limit = 50): array
    {
        $rsm = new Query\ResultSetMappingBuilder($this->em);
        $rsm->addRootEntityFromClassMetadata(Person::class, 'person');

        $query->addSelectClause($rsm->generateSelectClause());

        $nql = $this->em->createNativeQuery(
            $query->buildQuery().' ORDER BY pertinence DESC OFFSET ? LIMIT ?',
            $rsm
        )->setParameters(\array_merge($query->buildParameters(), [$start, $limit]));

        return $nql->getResult();
    }

    /**
     * @return array|Person[]
     *
     * @throws NonUniqueResultException
     * @throws ParsingException
     */
    public function findBySearchCriteria(
        int $start,
        int $limit,
        bool $simplify = false,
        ?string $default = null,
        ?string $firstname = null,
        ?string $lastname = null,
        ?\DateTimeInterface $birthdate = null,
        ?\DateTimeInterface $birthdateBefore = null,
        ?\DateTimeInterface $birthdateAfter = null,
        ?int $gender = null,
        ?string $countryCode = null,
        ?string $phonenumber = null,
        ?string $city = null,
    ): array {
        $query = $this->buildAuthorizedQuery(
            $default,
            $firstname,
            $lastname,
            $birthdate,
            $birthdateBefore,
            $birthdateAfter,
            $gender,
            $countryCode,
            $phonenumber,
            $city
        );

        return $this->fetchQueryPerson($query);
    }

    private function addAuthorizations(SearchApiQuery $query): SearchApiQuery
    {
        $authorizedCenters = $this->authorizationHelper
            ->getReachableCenters($this->security->getUser(), PersonVoter::SEE);

        if ([] === $authorizedCenters) {
            return $query->andWhereClause('FALSE = TRUE', []);
        }

        return $query
            ->setFromClause($query->getFromClause().' JOIN view_chill_person_person_center_history_current vcppchc ON vcppchc.person_id = person.id', $query->getFromParams())
            ->andWhereClause(
                strtr(
                    'vcppchc.center_id IN ({{ center_ids }})',
                    [
                        '{{ center_ids }}' => \implode(
                            ', ',
                            \array_fill(0, \count($authorizedCenters), '?')
                        ),
                    ]
                ),
                \array_map(static fn (Center $c) => $c->getId(), $authorizedCenters)
            );
    }

    public function findByPhone(PhoneNumber $phoneNumber, int $start = 0, int $limit = 20): array
    {
        $authorizedCenters = $this->authorizationHelper
            ->getReachableCenters($this->security->getUser(), PersonVoter::SEE);

        if ([] === $authorizedCenters) {
            return [];
        }

        $util = \libphonenumber\PhoneNumberUtil::getInstance();

        return $this->em->createQuery(
            'SELECT p FROM '.Person::class.' p LEFT JOIN p.otherPhoneNumbers opn JOIN p.centerCurrent pcc '.
            'WHERE (p.phonenumber LIKE :phone OR p.mobilenumber LIKE :phone OR opn.phonenumber LIKE :phone) '.
            'AND pcc.center IN (:centers)'
        )
            ->setMaxResults($limit)
            ->setFirstResult($start)
            ->setParameter('phone', $util->format($phoneNumber, PhoneNumberFormat::E164))
            ->setParameter('centers', $authorizedCenters)
            ->getResult();
    }
}
