<?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\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;

/**
 * Denormalize a Person entity from a JSON-like array structure, creating or updating an existing instance.
 *
 * To find an existing instance by his id, see the @see{PersonJsonReadDenormalizer}.
 */
final class PersonJsonDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
    use DenormalizerAwareTrait;
    use ObjectToPopulateTrait;

    public function __construct(private readonly PersonIdentifierManagerInterface $personIdentifierManager) {}

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

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

        // Setters applied directly per known field for readability
        if (\array_key_exists('firstName', $data)) {
            $person->setFirstName($data['firstName']);
        }

        if (\array_key_exists('lastName', $data)) {
            $person->setLastName($data['lastName']);
        }

        if (\array_key_exists('phonenumber', $data)) {
            $person->setPhonenumber($this->denormalizer->denormalize($data['phonenumber'], PhoneNumber::class, $format, $context));
        }

        if (\array_key_exists('mobilenumber', $data)) {
            $person->setMobilenumber($this->denormalizer->denormalize($data['mobilenumber'], PhoneNumber::class, $format, $context));
        }

        if (\array_key_exists('gender', $data) && null !== $data['gender']) {
            $gender = $this->denormalizer->denormalize($data['gender'], Gender::class, $format, []);
            $person->setGender($gender);
        }

        if (\array_key_exists('birthdate', $data)) {
            $object = $this->denormalizer->denormalize($data['birthdate'], \DateTime::class, $format, $context);
            $person->setBirthdate($object);
        }

        if (\array_key_exists('deathdate', $data)) {
            $object = $this->denormalizer->denormalize($data['deathdate'], \DateTimeImmutable::class, $format, $context);
            $person->setDeathdate($object);
        }

        if (\array_key_exists('center', $data)) {
            $object = $this->denormalizer->denormalize($data['center'], Center::class, $format, $context);
            $person->setCenter($object);
        }

        if (\array_key_exists('altNames', $data)) {
            foreach ($data['altNames'] as $altNameData) {
                if (!array_key_exists('key', $altNameData)
                    || !array_key_exists('value', $altNameData)
                    || '' === trim((string) $altNameData['key'])
                ) {
                    throw new UnexpectedValueException('format for alt name is not correct');
                }
                $altNameKey = $altNameData['key'];
                $altNameValue = $altNameData['value'];

                $altName = $person->getAltNames()->findFirst(fn (int $key, PersonAltName $personAltName) => $personAltName->getKey() === $altNameKey);
                if (null === $altName) {
                    $altName = new PersonAltName();
                    $person->addAltName($altName);
                }
                $altName->setKey($altNameKey)->setLabel($altNameValue);
            }
        }

        if (\array_key_exists('identifiers', $data)) {
            foreach ($data['identifiers'] as $identifierData) {
                if (!array_key_exists('definition_id', $identifierData)
                    || !array_key_exists('value', $identifierData)
                    || !is_int($identifierData['definition_id'])
                    || !is_array($identifierData['value'])
                ) {
                    throw new UnexpectedValueException('format for identifiers is not correct');
                }

                $definitionId = $identifierData['definition_id'];
                $value = $identifierData['value'];

                $worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definitionId);

                if (!$worker->getDefinition()->isEditableByUsers()) {
                    continue;
                }

                $personIdentifier = $person->getIdentifiers()->findFirst(fn (int $key, PersonIdentifier $personIdentifier) => $personIdentifier->getDefinition()->getId() === $definitionId);
                if (null === $personIdentifier) {
                    $personIdentifier = new PersonIdentifier($worker->getDefinition());
                    $person->addIdentifier($personIdentifier);
                }

                $personIdentifier->setValue($value);
                $personIdentifier->setCanonical($worker->canonicalizeValue($value));

                if ($worker->isEmpty($personIdentifier)) {
                    $person->removeIdentifier($personIdentifier);
                }
            }
        }

        if (\array_key_exists('email', $data)) {
            $person->setEmail($data['email']);
        }

        if (\array_key_exists('civility', $data) && null !== $data['civility']) {
            $civility = $this->denormalizer->denormalize($data['civility'], Civility::class, $format, []);
            $person->setCivility($civility);
        }

        return $person;
    }

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