<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\Comment;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Application\AttendanceLinkGenerator;
use Ferienpass\CoreBundle\Entity\ActivityNotification;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Message\CommentPosted;
use Ferienpass\CoreBundle\Repository\ConversationItemRepository;
use Ferienpass\CoreBundle\Repository\UserRepository;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * Fans a freshly posted comment out to its audiences: in-app bell notifications
 * for admin colleagues, and emails for the notified hosts/parents. Email replies
 * coming back in are fanned out to the colleagues taking part in the thread.
 */
final readonly class CommentNotifier
{
    public function __construct(
        private EntityManagerInterface $em,
        private CommentAudienceResolver $resolver,
        private ConversationItemRepository $comments,
        private UserRepository $users,
        private ConversationItemUrlGenerator $urls,
        private AttendanceLinkGenerator $attendanceLinks,
        #[Autowire(service: 'event.bus')]
        private MessageBusInterface $eventBus,
    ) {
    }

    /**
     * Notify everyone the author addressed in an app comment. Persists the
     * in-app rows and flushes, then dispatches the emails.
     */
    public function notify(ConversationItem $comment, bool $withAttendanceDetails = false): void
    {
        $resolved = $this->resolver->resolve($comment);
        $mailTargets = $resolved['emails'];

        foreach ($resolved['inApp'] as $entry) {
            $user = $entry['user'];
            if ($this->isAuthor($comment, $user)) {
                continue;
            }

            $this->em->persist(new ActivityNotification($comment, $user, $entry['reason']));

            if ($this->subscribesToEmail($user)) {
                $mailTargets[] = ['email' => $user->getEmail(), 'name' => $user->getName(), 'kind' => 'admin', 'reason' => $entry['reason']];
            }
        }

        $this->em->flush();

        $this->dispatchEmails($comment, $mailTargets, $withAttendanceDetails);
    }

    /**
     * A reply that arrived by email: alert the colleagues taking part in the
     * subject's conversation (or all admins, if none yet).
     */
    public function notifyEmailReply(ConversationItem $reply): void
    {
        $participants = $this->comments->findThreadParticipants($reply->getSubject());
        if ([] === $participants) {
            $participants = $this->users->findAdmins();
        }

        $mailTargets = [];
        foreach ($participants as $user) {
            if ($this->isAuthor($reply, $user)) {
                continue;
            }

            $this->em->persist(new ActivityNotification($reply, $user, ActivityNotification::REASON_REPLY));

            if ($this->subscribesToEmail($user)) {
                $mailTargets[] = ['email' => $user->getEmail(), 'name' => $user->getName(), 'kind' => 'admin', 'reason' => ActivityNotification::REASON_REPLY];
            }
        }

        $this->em->flush();

        $this->dispatchEmails($reply, $mailTargets);
    }

    /**
     * Dispatch the comment e-mail to each target, de-duplicated by address so a
     * colleague who matches two audiences is not mailed twice. The subject and
     * settings URLs are resolved here — at dispatch time we still have the
     * request host; the async worker would not (only admin/host recipients ever
     * receive them, gated in the handler).
     *
     * @param array<int, array{email: ?string, name: ?string, kind: string, reason?: ?string}> $targets
     */
    private function dispatchEmails(ConversationItem $comment, array $targets, bool $withAttendanceDetails = false): void
    {
        $subjectUrl = $this->urls->adminUrl($comment, UrlGeneratorInterface::ABSOLUTE_URL);
        $settingsUrl = $this->urls->settingsUrl(UrlGeneratorInterface::ABSOLUTE_URL);

        // Signed here, not in the handler: the worker has no request host, and a
        // signature covers the host it was made for.
        $attendance = $comment->getSubject();
        $attendanceUrl = $withAttendanceDetails && $attendance instanceof Attendance
            ? $this->attendanceLinks->signedShowUrl($attendance)
            : null;

        $seen = [];
        foreach ($targets as $target) {
            $key = strtolower(trim((string) $target['email']));
            if ('' === $key || isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;

            $this->eventBus->dispatch(new CommentPosted(
                $comment->getUuid()->toRfc4122(),
                (string) $target['email'],
                $target['name'],
                $target['kind'],
                $subjectUrl,
                $settingsUrl,
                $target['reason'] ?? null,
                $attendanceUrl,
            ));
        }
    }

    private function subscribesToEmail(User $user): bool
    {
        return $user->isNotifyComments() && '' !== trim((string) $user->getEmail());
    }

    private function isAuthor(ConversationItem $comment, User $user): bool
    {
        return null !== $comment->getAuthor() && $comment->getAuthor()->getId() === $user->getId();
    }
}
