<?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\BudgetBundle\Service\Summary\SummaryBudgetInterface;
use Chill\DocGeneratorBundle\Serializer\Helper\NormalizeNullValueHelper;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Repository\Relationships\RelationshipRepository;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\DataFixtures\Exception\CircularReferenceException;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;

class PersonDocGenNormalizer implements
    ContextAwareNormalizerInterface,
    NormalizerAwareInterface
{
    use NormalizerAwareTrait;

    private const CIRCULAR_KEY = 'person:circular';

    public function __construct(private readonly PersonRenderInterface $personRender, private readonly RelationshipRepository $relationshipRepository, private readonly TranslatableStringHelper $translatableStringHelper, private readonly SummaryBudgetInterface $summaryBudget) {}

    public function normalize($person, $format = null, array $context = [])
    {
        try {
            $context = $this->addCircularToContext($person, $context);
        } catch (CircularReferenceException) {
            return [
                'isNull' => true,
                'isCircular' => true,
                'text' => '',
            ];
        }

        /** @var Person $person */
        $dateContext = $context;
        $dateContext['docgen:expects'] = \DateTimeInterface::class;
        $addressContext = array_merge($context, ['docgen:expects' => Address::class]);
        $phonenumberContext = array_merge($context, ['docgen:expects' => PhoneNumber::class]);
        $centerContext = array_merge($context, ['docgen:expects' => Center::class]);
        $personResourceContext = array_merge($context, [
            'docgen:expects' => Person\PersonResource::class,
            // we simplify the list of attributes for the embedded persons
            AbstractNormalizer::GROUPS => ['docgen:read'],
            // when a person reference the same person... take care of circular references
            AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => fn ($object, $format, $context) => $this->normalizer->normalize(null, $format, $context),
        ]);
        $genderContext = array_merge($context, ['docgen:expects' => Gender::class]);

        if (null === $person) {
            return $this->normalizeNullValue($format, $context);
        }

        if (!$person instanceof Person) {
            throw new UnexpectedValueException();
        }

        $data = [
            'type' => 'person',
            'id' => $person->getId(),
            'isNull' => false,
            'civility' => $this->normalizer->normalize($person->getCivility(), $format, array_merge($context, ['docgen:expects' => Civility::class])),
            'firstName' => $person->getFirstName(),
            'lastName' => $person->getLastName(),
            'altNames' => \implode(
                ', ',
                \array_map(
                    static fn (PersonAltName $altName) => $altName->getLabel(),
                    $person->getAltNames()->toArray()
                )
            ),
            'text' => $this->personRender->renderString($person, []),
            'age' => (int) $person->getAge(),
            'birthdate' => $this->normalizer->normalize($person->getBirthdate(), $format, $dateContext),
            'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $dateContext),
            'gender' => null !== ($g = $person->getGender()) ? $this->translatableStringHelper->localize($g->getLabel()) : '',
            'genderEntity' => $this->normalizer->normalize($person->getGender(), $format, $genderContext),
            'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '',
            'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext),
            'maritalStatusComment' => $this->normalizer->normalize($person->getMaritalStatusComment(), $format, $dateContext),
            'email' => $person->getEmail(),
            'firstPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber() ?? $person->getMobilenumber(), $format, $phonenumberContext),
            'fixPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber(), $format, $phonenumberContext),
            'mobilePhoneNumber' => $this->normalizer->normalize($person->getMobilenumber(), $format, $phonenumberContext),
            'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '',
            'placeOfBirth' => $person->getPlaceOfBirth(),
            'memo' => $person->getMemo(),
            'numberOfChildren' => (string) $person->getNumberOfChildren(),
            'address' => $this->normalizer->normalize($person->getCurrentPersonAddress(), $format, $addressContext),
            'resources' => $this->normalizer->normalize($person->getResources(), $format, $personResourceContext),
            'center' => $this->normalizer->normalize($person->getCenter(), $format, $centerContext),
        ];

        if ($context['docgen:person:with-household'] ?? false) {
            $data['household'] = $this->normalizer->normalize(
                $person->getCurrentHousehold(),
                $format,
                array_merge($context, [
                    'docgen:expects' => Household::class,
                    'docgen:person:with-household' => false,
                    'docgen:person:with-relations' => false,
                    'docgen:person:with-budget' => false,
                ])
            );
        }

        if ($context['docgen:person:with-relations'] ?? false) {
            $data['relations'] = $this->normalizer->normalize(
                new ArrayCollection($this->relationshipRepository->findByPerson($person)),
                $format,
                array_merge($context, [
                    'docgen:person:with-household' => false,
                    'docgen:person:with-relation' => false,
                    'docgen:relationship:counterpart' => $person,
                    'docgen:person:with-budget' => false,
                ])
            );
        }

        if ($context['docgen:person:with-budget'] ?? false) {
            $data['budget']['person'] = $this->summaryBudget->getSummaryForPerson($person);

            if ($context['docgen:person:with-household'] ?? false) {
                $data['budget']['household'] = $this->summaryBudget->getSummaryForHousehold($person->getCurrentHousehold());
            }
        }

        return $data;
    }

    public function supportsNormalization($data, $format = null, array $context = [])
    {
        if ('docgen' !== $format) {
            return false;
        }

        return
            $data instanceof Person
            || (
                null === $data
                && Person::class === ($context['docgen:expects'] ?? null)
            );
    }

    private function addCircularToContext($person, $context)
    {
        if (null === $person) {
            $key = 'n';
        } else {
            $key = spl_object_hash($person);
        }

        if (!\array_key_exists(self::CIRCULAR_KEY, $context)) {
            $context[self::CIRCULAR_KEY] = [$key];

            return $context;
        }

        $occurences = array_reduce($context[self::CIRCULAR_KEY], static function ($carry, $item) use ($key) {
            if ($key === $item) {
                ++$carry;
            }

            return $carry;
        }, 0);

        if (2 <= $occurences) {
            throw new CircularReferenceException();
        }

        $context[self::CIRCULAR_KEY][] = $key;

        return $context;
    }

    private function hasGroup($context, string $group): bool
    {
        $groups = $context[AbstractNormalizer::GROUPS] ?? [];

        if (\is_string($groups)) {
            $groups = [$groups];
        }

        return \in_array($group, $groups, true);
    }

    private function normalizeNullValue(string $format, array $context)
    {
        $normalizer = new NormalizeNullValueHelper($this->normalizer, 'type', 'person');

        $attributes = [
            'id', 'firstName', 'lastName', 'age', 'altNames', 'text',
            'center' => Center::class,
            'civility' => Civility::class,
            'birthdate' => \DateTimeInterface::class,
            'deathdate' => \DateTimeInterface::class,
            'genderEntity' => Gender::class,
            'gender', 'maritalStatus',
            'maritalStatusDate' => \DateTimeInterface::class,
            'maritalStatusComment',
            'email', 'firstPhoneNumber', 'fixPhoneNumber', 'mobilePhoneNumber', 'nationality',
            'placeOfBirth', 'memo', 'numberOfChildren',
            'address' => Address::class,
        ];

        if ($context['docgen:person:with-household'] ?? false) {
            $attributes['household'] = Household::class;
        }

        $data = $normalizer->normalize($attributes, $format, $context);

        $data['resources'] = [];

        if ($context['docgen:person:with-relations'] ?? false) {
            $data['relations'] = [];
        }

        if ($context['docgen:person:with-budget'] ?? false) {
            $data['budget']['person'] = $this->summaryBudget->getSummaryForPerson(null);

            if ($context['docgen:person:with-household'] ?? false) {
                $data['budget']['household'] = $this->summaryBudget->getSummaryForHousehold(null);
            }
        }

        return $data;
    }
}
