<?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\Export\Helper;

use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Export\Helper\ExportAddressHelper;
use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\CivilityRepositoryInterface;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\GenderRepository;
use Chill\MainBundle\Repository\LanguageRepositoryInterface;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\MaritalStatusRepositoryInterface;
use DateTimeImmutable;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Helper for list person: provide:.
 *
 * * the labels;
 * * add select statement in query
 *
 * for some fields
 */
final readonly class ListPersonHelper
{
    private const FIELDS = [
        'personId',
        'civility',
        'firstName',
        'lastName',
        'birthdate',
        'center',
        'deathdate',
        'placeOfBirth',
        'gender',
        'genderComment',
        'maritalStatus',
        'maritalStatusComment',
        'maritalStatusDate',
        'memo',
        'email',
        'phonenumber',
        'mobilenumber',
        'numberOfChildren',
        'contactInfo',
        'countryOfBirth',
        'nationality',
        // add full addresses
        // 'address_fields',
        // add a list of spoken languages
        'spokenLanguages',
        // add household id
        'household_id',
        // add created at, created by, updated at, and updated by
        // 'lifecycleUpdate',
        'createdAt', 'createdBy', 'updatedAt', 'updatedBy',
    ];

    public function __construct(
        private ExportAddressHelper $addressHelper,
        private CenterRepositoryInterface $centerRepository,
        private CivilityRepositoryInterface $civilityRepository,
        private CountryRepository $countryRepository,
        private LanguageRepositoryInterface $languageRepository,
        private MaritalStatusRepositoryInterface $maritalStatusRepository,
        private TranslatableStringHelper $translatableStringHelper,
        private TranslatorInterface $translator,
        private UserRepositoryInterface $userRepository,
        private GenderRepository $genderRepository,
        /**
         * @var iterable<CustomizeListPersonHelperInterface>
         */
        private iterable $customPersonHelpers,
    ) {}

    /**
     * Those keys are the "direct" keys, which are created when we decide to use to list all the keys.
     *
     * This method must be used in `getKeys` instead of the `self::FIELDS`
     *
     * @return array<string>
     */
    public function getAllKeys(): array
    {
        $keys = [
            ...ListPersonHelper::FIELDS,
            ...$this->addressHelper->getKeys(ExportAddressHelper::F_ALL, 'address_fields'),
        ];

        foreach ($this->customPersonHelpers as $customize) {
            $keys = $customize->alterKeys($keys);
        }

        return $keys;
    }

    public function addSelect(QueryBuilder $qb, \DateTimeImmutable $computedDate): void
    {
        // we first add all the fields which are handled by the
        $focusedFieldKeys = [
            'personId', 'countryOfBirth', 'nationality', // 'address_fields',
            'spokenLanguages', 'household_id', 'center', // 'lifecycleUpdate',
            'genderComment', 'maritalStatus', 'maritalStatusComment', 'civility',
            'createdAt', 'createdBy', 'updatedAt', 'updatedBy',
        ];

        $filteredFields = array_filter(
            ListPersonHelper::FIELDS,
            fn ($field) => !in_array($field, $focusedFieldKeys, true)
        );

        foreach ($this->getAllKeys() as $f) {
            switch ($f) {
                case 'personId':
                    $qb->addSelect('person.id AS personId');

                    break;

                case 'countryOfBirth':
                case 'nationality':
                    $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f));

                    break;

                case 'spokenLanguages':
                    $qb->addSelect('(SELECT AGGREGATE(language.id) FROM '.Language::class.' language WHERE language MEMBER OF person.spokenLanguages) AS spokenLanguages');

                    break;

                case 'household_id':
                    $qb
                        ->addSelect('IDENTITY(personHouseholdAddress.household) AS household_id');

                    $this->addCurrentAddressAt($qb, $computedDate);

                    break;

                case 'center':
                    $qb
                        ->addSelect('IDENTITY(centerHistory.center) AS center')
                        ->leftJoin('person.centerHistory', 'centerHistory')
                        ->andWhere(
                            $qb->expr()->orX(
                                $qb->expr()->isNull('centerHistory'),
                                $qb->expr()->andX(
                                    $qb->expr()->lte('centerHistory.startDate', ':address_date'),
                                    $qb->expr()->orX(
                                        $qb->expr()->isNull('centerHistory.endDate'),
                                        $qb->expr()->gte('centerHistory.endDate', ':address_date')
                                    )
                                )
                            )
                        )
                        ->setParameter('address_date', $computedDate);

                    break;

                case 'genderComment':
                    $qb->addSelect('person.genderComment.comment AS genderComment');

                    break;

                case 'gender':
                    $qb->addSelect('IDENTITY(person.gender) AS gender');

                    break;

                case 'maritalStatus':
                    $qb->addSelect('IDENTITY(person.maritalStatus) AS maritalStatus');

                    break;

                case 'maritalStatusComment':
                    $qb->addSelect('person.maritalStatusComment.comment AS maritalStatusComment');

                    break;

                case 'civility':
                    $qb->addSelect('IDENTITY(person.civility) AS civility');

                    break;

                default:
                    if (in_array($f, $filteredFields, true)) {
                        $qb->addSelect(sprintf('person.%s as %s', $f, $f));
                    }
            }
        }

        // address
        $this->addCurrentAddressAt($qb, $computedDate);
        $qb->leftJoin('personHouseholdAddress.address', 'personAddress');
        $this->addressHelper->addSelectClauses(ExportAddressHelper::F_ALL, $qb, 'personAddress', 'address_fields');

        // lifecycle update
        $qb
            ->addSelect('person.createdAt AS createdAt')
            ->addSelect('IDENTITY(person.createdBy) AS createdBy')
            ->addSelect('person.updatedAt AS updatedAt')
            ->addSelect('IDENTITY(person.updatedBy) AS updatedBy');

        foreach ($this->customPersonHelpers as $customPersonHelper) {
            $customPersonHelper->alterSelect($qb, $computedDate);
        }
    }

    /**
     * @return array|string[]
     *
     * @deprecated
     */
    public function getAllPossibleFields(): array
    {
        return $this->getAllKeys();
    }

    public function getLabels($key, array $values, $data): callable
    {
        foreach ($this->customPersonHelpers as $customPersonHelper) {
            if (null !== $callable = $customPersonHelper->getLabels($key, $values, $data)) {
                return $callable;
            }
        }

        if (str_starts_with((string) $key, 'address_fields')) {
            return $this->addressHelper->getLabel($key, $values, $data, 'address_fields');
        }

        switch ($key) {
            case 'center':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value || null === $center = $this->centerRepository->find($value)) {
                        return '';
                    }

                    return $center->getName();
                };

            case 'birthdate':
            case 'deathdate':
            case 'maritalStatusDate':
            case 'createdAt':
            case 'updatedAt':
                // for birthdate, we have to transform the string into a date
                // to format the date correctly.
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    // warning: won't work with DateTimeImmutable as we reset time a few lines later
                    $date = \DateTime::createFromFormat('Y-m-d', $value);
                    $hasTime = false;

                    if (false === $date) {
                        $date = \DateTime::createFromFormat('Y-m-d H:i:s', $value);
                        $hasTime = true;
                    }

                    // check that the creation could occurs.
                    if (false === $date) {
                        throw new \Exception(sprintf('The value %s could not be converted to %s', $value, \DateTime::class));
                    }

                    if (!$hasTime) {
                        $date->setTime(0, 0, 0);
                    }

                    return $date;
                };

            case 'createdBy':
            case 'updatedBy':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    return $this->userRepository->find($value)->getLabel();
                };

            case 'civility':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    $civility = $this->civilityRepository->find($value);

                    if (null === $civility) {
                        return '';
                    }

                    return $this->translatableStringHelper->localize($civility->getName());
                };

            case 'gender':
                // for gender, we have to translate men/women statement
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    $gender = $this->genderRepository->find($value);

                    return $this->translatableStringHelper->localize($gender->getLabel());
                };

            case 'maritalStatus':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    $maritalStatus = $this->maritalStatusRepository->find($value);

                    return $this->translatableStringHelper->localize($maritalStatus->getName());
                };

            case 'spokenLanguages':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    $ids = json_decode((string) $value, null, 512, JSON_THROW_ON_ERROR);

                    return
                        implode(
                            '|',
                            array_map(function ($id) {
                                if (null === $id) {
                                    return '';
                                }

                                $lang = $this->languageRepository->find($id);

                                if (null === $lang) {
                                    return null;
                                }

                                return $this->translatableStringHelper->localize($lang->getName());
                            }, $ids)
                        );
                };

            case 'countryOfBirth':
            case 'nationality':
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    if (null === $value) {
                        return '';
                    }

                    $country = $this->countryRepository->find($value);

                    return $this->translatableStringHelper->localize(
                        $country->getName()
                    );
                };

            default:
                if (!\in_array($key, self::getAllKeys(), true)) {
                    throw new \RuntimeException("this key is not supported by this helper: {$key}");
                }

                // for fields which are associated with person
                return function ($value) use ($key) {
                    if ('_header' === $value) {
                        return $this->translator->trans($key);
                    }

                    return $value;
                };
        }
    }

    private function addCurrentAddressAt(QueryBuilder $qb, \DateTimeImmutable $date): void
    {
        if (!\in_array('personHouseholdAddress', $qb->getAllAliases(), true)) {
            $qb
                ->leftJoin('person.householdAddresses', 'personHouseholdAddress')
                ->andWhere(
                    $qb->expr()->orX(
                        // no address at this time
                        $qb->expr()->isNull('personHouseholdAddress'),
                        // there is one address...
                        $qb->expr()->andX(
                            $qb->expr()->lte('personHouseholdAddress.validFrom', ':address_date'),
                            $qb->expr()->orX(
                                $qb->expr()->isNull('personHouseholdAddress.validTo'),
                                $qb->expr()->gt('personHouseholdAddress.validTo', ':address_date')
                            )
                        )
                    )
                )
                ->setParameter('address_date', $date);
        }
    }
}
