<?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\Serializer\Normalizer;

use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Doctrine\Common\Collections\Collection;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;

/**
 * Serialize a Person entity.
 */
class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwareInterface, PersonJsonNormalizerInterface
{
    use DenormalizerAwareTrait;

    use NormalizerAwareTrait;

    use ObjectToPopulateTrait;

    public function __construct(
        private readonly ChillEntityRenderExtension $render,
        /* TODO: replace by PersonRenderInterface, as sthis is the only one required */
        private readonly PersonRepository $repository,
        private readonly CenterResolverManagerInterface $centerResolverManager,
        private readonly ResidentialAddressRepository $residentialAddressRepository,
        private readonly PhoneNumberHelperInterface $phoneNumberHelper,
    ) {}

    public function denormalize($data, $type, $format = null, array $context = [])
    {
        $person = $this->extractObjectToPopulate($type, $context);

        if (\array_key_exists('id', $data) && null === $person) {
            $person = $this->repository->find($data['id']);

            if (null === $person) {
                throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
            }

            // currently, not allowed to update a person through api
            // if instantiated with id
            return $person;
        }

        if (null === $person) {
            $person = new Person();
        }

        $fields = [
            'firstName',
            'lastName',
            'phonenumber',
            'mobilenumber',
            'gender',
            'birthdate',
            'deathdate',
            'center',
            'altNames',
            'email',
            'civility',
        ];

        $fields = array_filter(
            $fields,
            static fn (string $field): bool => \array_key_exists($field, $data)
        );

        foreach ($fields as $item) {
            switch ($item) {
                case 'firstName':
                    $person->setFirstName($data[$item]);

                    break;

                case 'lastName':
                    $person->setLastName($data[$item]);

                    break;

                case 'phonenumber':
                    $person->setPhonenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));

                    break;

                case 'mobilenumber':
                    $person->setMobilenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));

                    break;

                case 'gender':
                    $gender = $this->denormalizer->denormalize($data[$item], Gender::class, $format, []);

                    $person->setGender($gender);

                    break;

                case 'birthdate':
                    $object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);

                    $person->setBirthdate($object);

                    break;

                case 'deathdate':
                    $object = $this->denormalizer->denormalize($data[$item], \DateTimeImmutable::class, $format, $context);

                    $person->setDeathdate($object);

                    break;

                case 'center':
                    $object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context);
                    $person->setCenter($object);

                    break;

                case 'altNames':
                    foreach ($data[$item] as $altName) {
                        $oldAltName = $person
                            ->getAltNames()
                            ->filter(static fn (PersonAltName $n): bool => $n->getKey() === $altName['key'])->first();

                        if (false === $oldAltName) {
                            $newAltName = new PersonAltName();
                            $newAltName->setKey($altName['key']);
                            $newAltName->setLabel($altName['label']);
                            $person->addAltName($newAltName);
                        } else {
                            $oldAltName->setLabel($altName['label']);
                        }
                    }

                    break;

                case 'email':
                    $person->setEmail($data[$item]);

                    break;

                case 'civility':
                    $civility = $this->denormalizer->denormalize($data[$item], Civility::class, $format, []);

                    $person->setCivility($civility);

                    break;
            }
        }

        return $person;
    }

    /**
     * @param Person      $person
     * @param string|null $format
     */
    public function normalize($person, $format = null, array $context = [])
    {
        $groups = $context[AbstractNormalizer::GROUPS] ?? [];

        if (\is_string($groups)) {
            $groups = [$groups];
        }
        $household = $person->getCurrentHousehold();
        $currentResidentialAddresses = $this->residentialAddressRepository->findCurrentResidentialAddressByPerson($person);

        $data = [
            'type' => 'person',
            'id' => $person->getId(),
            'text' => $this->render->renderString($person, ['addAge' => false]),
            'textAge' => $this->render->renderString($person, ['addAge' => true]),
            'firstName' => $person->getFirstName(),
            'lastName' => $person->getLastName(),
            'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress(), $format, $context),
            'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $context),
            'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $context),
            'age' => $this->normalizer->normalize($person->getAge(), $format, $context),
            'phonenumber' => $this->normalizer->normalize($person->getPhonenumber(), $format, $context),
            'mobilenumber' => $this->normalizer->normalize($person->getMobilenumber(), $format, $context),
            'email' => $person->getEmail(),
            'gender' => $this->normalizer->normalize($person->getGender(), $format, $context),
            'civility' => $this->normalizer->normalize($person->getCivility(), $format, $context),
        ];

        if (\in_array('minimal', $groups, true) && 1 === \count($groups)) {
            return $data;
        }

        return [...$data, 'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context), 'altNames' => $this->normalizeAltNames($person->getAltNames()), 'current_household_id' => $household ? $this->normalizer->normalize($household->getId(), $format, $context) : null, 'current_residential_addresses' => $currentResidentialAddresses ?
            $this->normalizer->normalize($currentResidentialAddresses, $format, $context) :
            null];
    }

    public function supportsDenormalization($data, $type, $format = null)
    {
        return Person::class === $type && 'person' === ($data['type'] ?? null);
    }

    public function supportsNormalization($data, $format = null): bool
    {
        return $data instanceof Person && 'json' === $format;
    }

    /**
     * @param Collection<array-key, PersonAltName> $altNames
     *
     * @return array<array-key, array<string, string>>
     */
    protected function normalizeAltNames(Collection $altNames): array
    {
        return $altNames
            ->map(
                static fn (PersonAltName $personAltName): array => [
                    'key' => $personAltName->getKey(),
                    'label' => $personAltName->getLabel(),
                ]
            )
            ->toArray();
    }
}
