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

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Entity\AddresseeHistory;
use Chill\TicketBundle\Entity\CallerHistory;
use Chill\TicketBundle\Entity\Comment;
use Chill\TicketBundle\Entity\EmergencyStatusHistory;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\PersonHistory;
use Chill\TicketBundle\Entity\StateHistory;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Security\Voter\CommentVoter;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
    use NormalizerAwareTrait;

    public function __construct(private Security $security) {}

    public function normalize($object, ?string $format = null, array $context = [])
    {
        if (!$object instanceof Ticket) {
            throw new UnexpectedValueException();
        }

        $data = [
            'type' => 'ticket_ticket',
            'id' => $object->getId(),
            'externalRef' => $object->getExternalRef(),
            'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
            'currentPersons' => $this->normalizer->normalize($object->getPersons(), $format, [
                'groups' => 'read',
            ]),
            'currentAddressees' => $this->normalizer->normalize($object->getCurrentAddressee(), $format, ['groups' => 'read']),
            'currentInputs' => $this->normalizer->normalize($object->getCurrentInputs(), $format, ['groups' => 'read']),
            'currentMotive' => $this->normalizer->normalize($object->getMotive(), $format, ['groups' => ['read', MotiveNormalizer::GROUP_CHILDREN_TO_PARENT]]),
            'currentState' => $object->getState()?->value ?? 'open',
            'emergency' => $object->getEmergencyStatus()?->value ?? 'no',
            'caller' => $this->normalizer->normalize($object->getCaller(), $format, ['groups' => 'read']),
        ];

        if ('read:simple' === $context['groups']) {
            $data += ['type_extended' => 'ticket_ticket:simple'];

            return $data;
        }

        $data += [
            'type_extended' => 'ticket_ticket:extended',
            'history' => array_values($this->serializeHistory($object, $format, ['groups' => 'read'])),
            'updatedAt' => $this->normalizer->normalize($object->getUpdatedAt(), $format, $context),
            'updatedBy' => $this->normalizer->normalize($object->getUpdatedBy(), $format, $context),
            'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
        ];

        return $data;
    }

    public function supportsNormalization($data, ?string $format = null)
    {
        return 'json' === $format && $data instanceof Ticket;
    }

    private function serializeHistory(Ticket $ticket, string $format, array $context): array
    {
        $events = [
            ...array_map(
                fn (StateHistory $stateHistory) => [
                    'event_type' => 'state_change',
                    'at' => $stateHistory->getStartDate(),
                    'by' => $stateHistory->getCreatedBy(),
                    'data' => [
                        'new_state' => $stateHistory->getState()->value,
                    ],
                ],
                $ticket->getStateHistories()->toArray(),
            ),
            ...array_map(
                fn (MotiveHistory $motiveHistory) => [
                    'event_type' => 'set_motive',
                    'at' => $motiveHistory->getStartDate(),
                    'by' => $motiveHistory->getCreatedBy(),
                    'data' => $motiveHistory,
                ],
                $ticket->getMotiveHistories()->toArray()
            ),
            ...array_map(
                fn (PersonHistory $personHistory) =>  [
                    'event_type' => 'add_person',
                    'at' => $personHistory->getStartDate(),
                    'by' => $personHistory->getCreatedBy(),
                    'data' => $personHistory,
                ],
                $ticket->getPersonHistories()->toArray(),
            ),
            ...array_map(
                fn (Comment $comment) => [
                    'event_type' => 'add_comment',
                    'at' => $comment->getCreatedAt(),
                    'by' => $comment->getCreatedBy(),
                    'data' => $comment,
                ],
                $ticket->getComments()->filter(fn (Comment $comment) => $this->security->isGranted(CommentVoter::READ, $comment))->toArray(),
            ),
            ...$this->addresseesStates($ticket),
            ...$this->personStates($ticket),
            ...array_map(
                fn (EmergencyStatusHistory $stateHistory) => [
                    'event_type' => 'emergency_change',
                    'at' => $stateHistory->getStartDate(),
                    'by' => $stateHistory->getCreatedBy(),
                    'data' => [
                        'new_emergency' => $stateHistory->getEmergencyStatus()->value,
                    ],
                ],
                $ticket->getEmergencyStatusHistories()->toArray(),
            ),
            ...array_map(
                fn (CallerHistory $stateHistory) => [
                    'event_type' => 'set_caller',
                    'at' => $stateHistory->getStartDate(),
                    'by' => $stateHistory->getCreatedBy(),
                    'data' => [
                        'new_caller' => $this->normalizer->normalize($ticket->getCaller(), $format, ['groups' => ['read']]),
                    ],
                ],
                $ticket->getCallerHistories()->toArray(),
            ),
        ];

        if (null !== $ticket->getCreatedBy() && null !== $ticket->getCreatedAt()) {
            $events[] =
                [
                    'event_type' => 'create_ticket',
                    'at' => \DateTimeImmutable::createFromInterface($ticket->getCreatedAt())
                        ->sub(new \DateInterval('PT1S')), // TODO hack to avoid collision with creation of the ticket event,
                    'by' => $ticket->getCreatedBy(),
                    'data' => [],
                ];
        }

        usort(
            $events,
            static fn (array $a, array $b): int => $a['at'] <=> $b['at']
        );

        return array_map(
            fn ($data) => [
                'event_type' => $data['event_type'],
                'at' => $this->normalizer->normalize($data['at'], $format, $context),
                'by' => $this->normalizer->normalize($data['by'], $format, $context),
                'data' => $this->normalizer->normalize($data['data'], $format, $this->contextByEventType($data['event_type'], $context)),
            ],
            $events
        );
    }

    private function contextByEventType(string $eventType, array $context): array
    {
        return match($eventType) {
            'set_motive' => array_merge($context, ['groups' => ['read', MotiveNormalizer::GROUP_CHILDREN_TO_PARENT]]),
            default => $context,
        };
    }

    private function addresseesStates(Ticket $ticket): array
    {
        /** @var array{string, array{added: list<AddresseeHistory>, removed: list<AddresseeHistory>}} $changes */
        $changes = [];
        $dateFormat = 'Y-m-d-m-Y-H-i-s';

        foreach ($ticket->getAddresseeHistories() as $history) {
            $changes[$history->getStartDate()->format($dateFormat)]['added'][] = $history;
            if (null !== $history->getEndDate()) {
                $changes[$history->getEndDate()->format($dateFormat)]['removed'][] = $history;
            }
        }

        ksort($changes);

        $currents = [];
        $steps = [];
        foreach ($changes as $change) {
            $historiesAdded = $change['added'] ?? [];
            $historiesRemoved = $change['removed'] ?? [];

            if (0 < count($historiesAdded)) {
                $at = $historiesAdded[0]->getStartDate();
                $by = $historiesAdded[0]->getCreatedBy();
            } elseif (0 < count($historiesRemoved)) {
                $at = $historiesRemoved[0]->getEndDate();
                $by = $historiesRemoved[0]->getRemovedBy();
            } else {
                throw new \LogicException('it should have at least one history');
            }

            $removed = array_map(fn (AddresseeHistory $history) => $history->getAddressee(), $historiesRemoved);
            $currents = array_filter($currents, fn (User|UserGroup $a) => !in_array($a, $removed, true));
            foreach ($historiesAdded as $history) {
                $currents[] = $history->getAddressee();
            }

            $steps[] = [
                'event_type' => 'addressees_state',
                'at' => $at,
                'by' => $by,
                'data' => [
                    'addressees' => array_values($currents),
                ],
            ];
        }

        return $steps;
    }

    private function personStates(Ticket $ticket): array
    {
        /** @var array{string, array{added: list<PersonHistory>, removed: list<PersonHistory>}} $changes */
        $changes = [];
        $dateFormat = 'Y-m-d-m-Y-H-i-s';

        foreach ($ticket->getPersonHistories() as $history) {
            $changes[$history->getStartDate()->format($dateFormat)]['added'][] = $history;
            if (null !== $history->getEndDate()) {
                $changes[$history->getEndDate()->format($dateFormat)]['removed'][] = $history;
            }
        }

        ksort($changes);

        $currents = [];
        $steps = [];
        foreach ($changes as $change) {
            $historiesAdded = $change['added'] ?? [];
            $historiesRemoved = $change['removed'] ?? [];

            if (0 < count($historiesAdded)) {
                $at = $historiesAdded[0]->getStartDate();
                $by = $historiesAdded[0]->getCreatedBy();
            } elseif (0 < count($historiesRemoved)) {
                $at = $historiesRemoved[0]->getEndDate();
                $by = $historiesRemoved[0]->getRemovedBy();
            } else {
                throw new \LogicException('it should have at least one history');
            }

            $removed = array_map(fn (PersonHistory $history) => $history->getPerson(), $historiesRemoved);
            $currents = array_filter($currents, fn (Person $a) => !in_array($a, $removed, true));
            foreach ($historiesAdded as $history) {
                $currents[] = $history->getPerson();
            }

            $steps[] = [
                'event_type' => 'persons_state',
                'at' => $at,
                'by' => $by,
                'data' => [
                    'persons' => array_values($currents),
                ],
            ];
        }

        return $steps;
    }
}
