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

use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;

class ThirdPartyRepository implements ObjectRepository
{
    private readonly EntityRepository $repository;

    public function __construct(EntityManagerInterface $em, private readonly Connection $connection, private readonly PhoneNumberUtil $phonenumberUtil)
    {
        $this->repository = $em->getRepository(ThirdParty::class);
    }

    public function findThirdpartiesWithContacts(string $lang): iterable
    {
        $sql = "
        SELECT
            parent.id AS Id,
            CASE
                WHEN parent.kind = 'company' THEN 'Personne morale'
                ELSE 'Personne physique'
            END AS kind,
            parent_civility.name->>? AS civility,
            parent.firstname AS firstname,
            parent.name AS name,
            parent.profession AS profession,
            parent.telephone AS telephone,
            parent.telephone2 AS telephone2,
            parent.email AS email,
            CONCAT_WS(' ', parent_address.street, parent_address.streetnumber, parent_postal.code, parent_postal.label) AS address,
            parent.comment AS comment,
            parent.parent_id AS parent,
            parent.name_company AS name_company,
            parent.acronym AS acronym,
            CASE WHEN parent.active THEN 'X' ELSE '' END AS active,

            -- Contact (child third-party) details
            contact.id AS contact_id,
            contact.name AS contact_name,
            contact.firstname AS contact_firstname,
            contact.telephone AS contact_phone,
            contact.telephone2 AS contact_phone2,
            contact.email AS contact_email,
            contact.profession AS contact_profession,
            CONCAT_WS(' ', contact_address.street, contact_address.streetnumber, contact_postal.code, contact_postal.label) AS contact_address

        FROM chill_3party.third_party parent
        LEFT JOIN chill_main_civility parent_civility ON parent.civility_id = parent_civility.id
        LEFT JOIN chill_main_address parent_address ON parent.address_id = parent_address.id
        LEFT JOIN chill_main_postal_code parent_postal ON parent_address.postcode_id = parent_postal.id

        -- Join contacts (child third parties)
        LEFT JOIN chill_3party.third_party contact ON contact.parent_id = parent.id
        LEFT JOIN chill_main_address contact_address ON contact.address_id = contact_address.id
        LEFT JOIN chill_main_postal_code contact_postal ON contact_address.postcode_id = contact_postal.id

        -- Exclude contacts from appearing as standalone rows
        WHERE parent.parent_id IS NULL

        -- Sorting order
        ORDER BY parent.name ASC, parent.name_company ASC, parent.id ASC,
                 contact.name ASC, contact.firstname ASC, contact.id ASC
    ";

        return $this->connection->iterateAssociative($sql, [$lang]);
    }

    /**
     * count amongst parties associated to $centers, with $terms parameters.
     */
    public function countByMemberOfCenters(array $centers, array $terms = []): int
    {
        $qb = $this->buildQuery($centers, $terms);
        $qb->select('COUNT(tp)');

        return $qb->getQuery()->getSingleScalarResult();
    }

    public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
    {
        return $this->repository->createQueryBuilder($alias, $indexBy);
    }

    public function find($id): ?ThirdParty
    {
        return $this->repository->find($id);
    }

    /**
     * @return array|ThirdParty[]
     */
    public function findAll(): array
    {
        return $this->repository->findAll();
    }

    /**
     * @param null $limit
     * @param null $offset
     *
     * @return array|ThirdParty[]
     */
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
    }

    /**
     * Finds third-party records by phone number.
     *
     * The search is performed agains every phonenumber field (there are two phonenumber on a thirdParty).
     *
     * @param string|PhoneNumber $phonenumber The phone number to search for. Can be a string or a PhoneNumber object.
     * @param int                $firstResult the index of the first result to retrieve (pagination start)
     * @param int                $maxResults  the maximum number of results to retrieve (pagination limit)
     *
     * @return list<ThirdParty> the result set containing matching third-party records
     */
    public function findByPhonenumber(string|PhoneNumber $phonenumber, int $firstResult = 0, int $maxResults = 20): array
    {
        if ('' === $phonenumber) {
            return [];
        }

        $qb = $this->createQueryBuilder('tp');
        $qb->select('tp');

        $qb->where(
            $qb->expr()->orX(
                $qb->expr()->eq('tp.telephone', ':phonenumber'),
                $qb->expr()->eq('tp.telephone2', ':phonenumber')
            )
        );

        $qb->setParameter(
            'phonenumber',
            is_string($phonenumber) ? $phonenumber : $this->phonenumberUtil->format($phonenumber, PhoneNumberFormat::E164)
        );

        $qb->setFirstResult($firstResult)->setMaxResults($maxResults);

        return $qb->getQuery()->getResult();
    }

    /**
     * Search amongst parties associated to $centers, with $terms parameters.
     *
     * Different format for return:
     * - ['entity']: return the entity hydrated as objects
     * - ['array', [ DQL ]: return objects hydrated as entity, with
     *   an array describing the fields as DQL.
     *
     * supported terms:
     *
     * - name or _default: containing the name (name LIKE %string%)
     * - is_active: is active = true / false
     * - types: an array of types
     *
     * @param int      $firstResult
     * @param int      $maxResults
     * @param array    $terms
     * @param string[] $returnFormat a format for returning
     */
    public function findByMemberOfCenters(array $centers, $firstResult = 0, $maxResults = 20, $terms = [], $returnFormat = ['entity']): array
    {
        $qb = $this->buildQuery($centers, $terms);

        switch ($returnFormat[0]) {
            case 'entity':
                $qb->select('tp');
                $hydrate = Query::HYDRATE_OBJECT;

                break;

            case 'array':
                $hydrate = Query::HYDRATE_ARRAY;
                $first = true;

                foreach ($returnFormat[1] as $dql) {
                    if ($first) {
                        $qb->select($dql);
                        $first = false;
                    } else {
                        $qb->addSelect($dql);
                    }
                }

                break;

            default:
                throw new \DomainException('This return format is invalid');
        }
        $qb->setFirstResult($firstResult)
            ->setMaxResults($maxResults);

        return $qb->getQuery()->getResult();
    }

    public function findOneBy(array $criteria): ?ThirdParty
    {
        return $this->repository->findOneBy($criteria);
    }

    public function getClassName(): string
    {
        return ThirdParty::class;
    }

    private function buildQuery($centers, $terms): QueryBuilder
    {
        $qb = $this->createMemberOfCentersQuery($centers);
        $this->setNameCondition($qb, $terms);
        $this->setTypesCondition($qb, $terms);
        $this->setIsActiveCondition($qb, $terms);

        return $qb;
    }

    private function createMemberOfCentersQuery($centers): QueryBuilder
    {
        $qb = $this->createQueryBuilder('tp');

        $or = $qb->expr()->orX();

        foreach ($centers as $center) {
            $or->add($qb->expr()->isMemberOf(':center_'.$center->getId(), 'tp.centers'));
            $qb->setParameter('center_'.$center->getId(), $center);
        }

        $qb->where($or);

        return $qb;
    }

    private function setIsActiveCondition(QueryBuilder $qb, array $terms)
    {
        if (\array_key_exists('is_active', $terms)) {
            $qb->andWhere(
                $terms['is_active'] ? $qb->expr()->eq('tp.active', "'TRUE'") :
                $qb->expr()->eq('tp.active', "'FALSE'")
            );
        }
    }

    /**
     * Add parameters to filter by containing $terms["name"] or
     * $terms["_default"].
     */
    private function setNameCondition(QueryBuilder $qb, array $terms)
    {
        if (\array_key_exists('name', $terms) || \array_key_exists('_default', $terms)) {
            $term = $terms['name'] ?? $terms['_default'];

            if (null === $term || '' === $term) {
                return;
            }
            $qb->andWhere($qb->expr()->like('UNACCENT(LOWER(tp.name))', 'UNACCENT(LOWER(:name))'));
            $qb->setParameter('name', '%'.$term.'%');
        }
    }

    private function setTypesCondition(QueryBuilder $qb, array $terms)
    {
        if (\array_key_exists('types', $terms)) {
            $orx = $qb->expr()->orX();

            foreach ($terms['types'] as $type) {
                $orx->add('JSONB_EXISTS_IN_ARRAY(tp.type, :type_'.$type.') = \'TRUE\'');
                $qb->setParameter('type_'.$type, $type);
            }
            $qb->andWhere($orx);
        }
    }
}
