<?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\Notifier;

use Contao\CoreBundle\Filesystem\FilesystemItem;
use Contao\CoreBundle\Filesystem\VirtualFilesystemInterface;
use Ferienpass\CoreBundle\Entity;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\ConversationItem;
use Ferienpass\CoreBundle\Entity\DbafsAttachment;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Notification\AbstractNotification;
use Ferienpass\CoreBundle\Notification\AccountActivatedNotification;
use Ferienpass\CoreBundle\Notification\AccountCreatedNotification;
use Ferienpass\CoreBundle\Notification\AccountRegistrationHelpNotification;
use Ferienpass\CoreBundle\Notification\ApplicationsClosedNotification;
use Ferienpass\CoreBundle\Notification\AttendanceChangesNotification;
use Ferienpass\CoreBundle\Notification\AttendanceCompletionNotification;
use Ferienpass\CoreBundle\Notification\AttendanceConfirmedNotification;
use Ferienpass\CoreBundle\Notification\AttendanceDecisions;
use Ferienpass\CoreBundle\Notification\AttendanceEmailConfirmationNotification;
use Ferienpass\CoreBundle\Notification\AttendanceNewlyConfirmedNotification;
use Ferienpass\CoreBundle\Notification\AttendanceRejectedNotification;
use Ferienpass\CoreBundle\Notification\AttendanceWithdrawConfirmationNotification;
use Ferienpass\CoreBundle\Notification\CommentNotification;
use Ferienpass\CoreBundle\Notification\EmailToAwareNotificationInterface;
use Ferienpass\CoreBundle\Notification\HostCreatedNotification;
use Ferienpass\CoreBundle\Notification\OfferCancelledNotification;
use Ferienpass\CoreBundle\Notification\OfferCompletedNotification;
use Ferienpass\CoreBundle\Notification\OfferCompleteNotification;
use Ferienpass\CoreBundle\Notification\OfferFinalizedNotification;
use Ferienpass\CoreBundle\Notification\OfferRelaunchedNotification;
use Ferienpass\CoreBundle\Notification\ParticipantListNotification;
use Ferienpass\CoreBundle\Notification\PaymentAwareNotificationInterface;
use Ferienpass\CoreBundle\Notification\PaymentCreatedNotification;
use Ferienpass\CoreBundle\Notification\RemindAttendanceNotification;
use Ferienpass\CoreBundle\Notification\UserInvitationNotification;
use Ferienpass\CoreBundle\Notification\UserPasswordlessNotification;
use Ferienpass\CoreBundle\Notification\UserPasswordNotification;
use Ferienpass\CoreBundle\Notifier\Message\Attachment;
use Ferienpass\CoreBundle\Payments\PaymentSystem;
use Ferienpass\CoreBundle\Repository\NotificationRepository;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;

class Notifier implements NotifierInterface
{
    /**
     * @var array<string, AbstractNotification>
     */
    private array $notifications;

    public function __construct(#[AutowireIterator('ferienpass.notification', defaultIndexMethod: 'getName')] iterable $notifications, private readonly NotifierInterface $notifier, private readonly NotificationRepository $notificationRepository, #[Autowire(param: 'ferienpass.reply_address')] private readonly ?string $replyTo, private readonly VirtualFilesystemInterface $attachmentsStorage, private readonly PaymentSystem $paymentSystem, #[Autowire(param: 'ferienpass.model.offer.class')] private readonly string $offerClass, #[Autowire(param: 'ferienpass.model.participant.class')] private readonly string $participantClass)
    {
        $this->notifications = $notifications instanceof \Traversable ? iterator_to_array($notifications) : $notifications;
    }

    public function accountActivated(User $user): ?AccountActivatedNotification
    {
        $notification = $this->get(AccountActivatedNotification::getName());
        if (!$notification instanceof AccountActivatedNotification) {
            return null;
        }

        return $notification->user($user);
    }

    public function accountCreated(User $user): ?AccountCreatedNotification
    {
        $notification = $this->get(AccountCreatedNotification::getName());
        if (!$notification instanceof AccountCreatedNotification) {
            return null;
        }

        return $notification->user($user);
    }

    public function accountRegistrationHelp(User $user): ?AccountRegistrationHelpNotification
    {
        $notification = $this->get(AccountRegistrationHelpNotification::getName());
        if (!$notification instanceof AccountRegistrationHelpNotification) {
            return null;
        }

        return $notification->user($user);
    }

    public function hostCreated(Host $host, User $user): ?HostCreatedNotification
    {
        $notification = $this->get(HostCreatedNotification::getName());
        if (!$notification instanceof HostCreatedNotification) {
            return null;
        }

        return $notification->host($host)->user($user);
    }

    public function comment(ConversationItem $comment): ?CommentNotification
    {
        $notification = $this->get(CommentNotification::getName());
        if (!$notification instanceof CommentNotification) {
            // No admin-configured template row: fall back to the registered
            // default so comment emails work out of the box (the type still
            // appears in the notification editor for optional customisation).
            $prototype = $this->notifications[CommentNotification::getName()] ?? null;
            if (!$prototype instanceof CommentNotification) {
                return null;
            }

            $notification = clone $prototype;
            if (null !== $this->replyTo) {
                $notification->replyTo($this->replyTo);
            }
        }

        return $notification->comment($comment);
    }

    public function userPassword(string $token, User $user): ?UserPasswordNotification
    {
        $notification = $this->get(UserPasswordNotification::getName());
        if (!$notification instanceof UserPasswordNotification) {
            return null;
        }

        return $notification->token($token)->user($user);
    }

    public function userPasswordless(User $user): ?UserPasswordlessNotification
    {
        $notification = $this->get(UserPasswordlessNotification::getName());
        if (!$notification instanceof UserPasswordlessNotification) {
            return null;
        }

        return $notification->user($user);
    }

    public function attendanceNewlyConfirmed(Attendance $attendance): ?AttendanceNewlyConfirmedNotification
    {
        $notification = $this->get(AttendanceNewlyConfirmedNotification::getName(), $attendance->getOffer()->getEdition() ?? null);
        if (!$notification instanceof AttendanceNewlyConfirmedNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function applicationsClosed(OfferInterface $offer): ?ApplicationsClosedNotification
    {
        $notification = $this->get(ApplicationsClosedNotification::getName(), $offer->getEdition());
        if (!$notification instanceof ApplicationsClosedNotification) {
            return null;
        }

        return $notification->offer($offer);
    }

    public function attendanceConfirmed(Attendance $attendance): ?AttendanceConfirmedNotification
    {
        $notification = $this->get(AttendanceConfirmedNotification::getName(), $attendance->getOffer()->getEdition());
        if (!$notification instanceof AttendanceConfirmedNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function attendanceEmailConfirmation(Attendance $attendance): ?AttendanceEmailConfirmationNotification
    {
        $notification = $this->get(AttendanceEmailConfirmationNotification::getName(), $attendance->getOffer()->getEdition());
        if (!$notification instanceof AttendanceEmailConfirmationNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function attendanceRejected(Attendance $attendance): ?AttendanceRejectedNotification
    {
        $notification = $this->get(AttendanceRejectedNotification::getName(), $attendance->getOffer()->getEdition());
        if (!$notification instanceof AttendanceRejectedNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function attendanceCompletion(Attendance $attendance): ?AttendanceCompletionNotification
    {
        $notification = $this->get(AttendanceCompletionNotification::getName(), $attendance->getOffer()->getEdition());
        if (!$notification instanceof AttendanceCompletionNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function attendanceWithdrawConfirmation(Attendance $attendance): ?AttendanceWithdrawConfirmationNotification
    {
        $notification = $this->get(AttendanceWithdrawConfirmationNotification::getName(), $attendance->getOffer()->getEdition());
        if (!$notification instanceof AttendanceWithdrawConfirmationNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function attendanceDecisions($attendances, ?Edition $edition = null): ?AttendanceDecisions
    {
        $notification = $this->get(AttendanceDecisions::getName(), $edition);
        if (!$notification instanceof AttendanceDecisions) {
            return null;
        }

        foreach ($attendances as $attendance) {
            $notification->attendance($attendance);
        }

        return $notification;
    }

    public function offerCancelled(Attendance $attendance): ?OfferCancelledNotification
    {
        $notification = $this->get(OfferCancelledNotification::getName(), $attendance->getoffer()->getEdition());
        if (!$notification instanceof OfferCancelledNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function offerFinalized(OfferInterface $offer): ?OfferFinalizedNotification
    {
        $notification = $this->get(OfferFinalizedNotification::getName(), $offer->getEdition());
        if (!$notification instanceof OfferFinalizedNotification) {
            return null;
        }

        return $notification->offer($offer);
    }

    public function offerCompleted(OfferInterface $offer): ?OfferCompletedNotification
    {
        $notification = $this->get(OfferCompletedNotification::getName(), $offer->getEdition());
        if (!$notification instanceof OfferCompletedNotification) {
            return null;
        }

        return $notification->offer($offer);
    }

    public function offerComplete(OfferInterface $offer): ?OfferCompleteNotification
    {
        $notification = $this->get(OfferCompleteNotification::getName(), $offer->getEdition());
        if (!$notification instanceof OfferCompleteNotification) {
            return null;
        }

        return $notification->offer($offer);
    }

    public function participantList(OfferInterface $offer): ?ParticipantListNotification
    {
        $notification = $this->get(ParticipantListNotification::getName(), $offer->getEdition());
        if (!$notification instanceof ParticipantListNotification) {
            return null;
        }

        return $notification->offer($offer);
    }

    public function attendanceChanges(OfferInterface $offer): ?AttendanceChangesNotification
    {
        $notification = $this->get(AttendanceChangesNotification::getName(), $offer->getEdition());
        if (!$notification instanceof AttendanceChangesNotification) {
            return null;
        }

        return $notification->offer($offer);
    }

    public function offerRelaunched(Attendance $attendance): ?OfferRelaunchedNotification
    {
        $notification = $this->get(OfferRelaunchedNotification::getName(), $attendance->getOffer()->getEdition());
        if (!$notification instanceof OfferRelaunchedNotification) {
            return null;
        }

        return $notification->attendance($attendance);
    }

    public function paymentCreated(Payment $payment): ?PaymentCreatedNotification
    {
        $notification = $this->get(PaymentCreatedNotification::getName());
        if (!$notification instanceof PaymentCreatedNotification) {
            return null;
        }

        return $notification->payment($payment);
    }

    public function remindAttendance(Attendance ...$attendances): ?RemindAttendanceNotification
    {
        $first = $attendances[0] ?? null;
        if (null === $first) {
            return null;
        }

        $notification = $this->get(RemindAttendanceNotification::getName(), $first->getOffer()->getEdition());
        if (!$notification instanceof RemindAttendanceNotification) {
            return null;
        }

        foreach ($attendances as $attendance) {
            $notification->attendance($attendance);
        }

        return $notification;
    }

    public function userInvitation(User $user, Host $host, string $inviteeEmail): ?UserInvitationNotification
    {
        $notification = $this->get(UserInvitationNotification::getName());
        if (!$notification instanceof UserInvitationNotification) {
            return null;
        }

        return $notification->user($user)->host($host)->inviteeEmail($inviteeEmail);
    }

    public function send(Notification $notification, RecipientInterface ...$recipients): void
    {
        $this->notifier->send($notification, ...$recipients);
    }

    public function types(): array
    {
        $paymentsEnabled = $this->paymentSystem->isEnabled();

        return array_keys(array_filter(
            $this->notifications,
            static fn (AbstractNotification $notification): bool => $paymentsEnabled || !$notification instanceof PaymentAwareNotificationInterface,
        ));
    }

    public function getClass(string $key): ?string
    {
        if (!\array_key_exists($key, $this->notifications)) {
            return null;
        }

        return $this->notifications[$key]::class;
    }

    public function createMock(string $key, string $subject, string $content): ?Notification
    {
        if (!\array_key_exists($key, $this->notifications)) {
            return null;
        }

        $notification = $this->notifications[$key];

        $notification = $notification->createMock($this->offerClass, $this->participantClass);

        $notification
            ->subject($subject)
            ->content($content);

        return $notification;
    }

    public function get(string $key, ?Edition $edition = null, bool $strict = false): ?Notification
    {
        if (!\array_key_exists($key, $this->notifications)) {
            return null;
        }

        $notification = clone $this->notifications[$key];
        $entity = $this->notificationRepository->findOneByTypeAndEdition($key, $edition, $strict);
        if (!$entity instanceof Entity\Notification) {
            return null;
        }

        $notification
            ->subject($entity->getEmailSubject() ?? '')
            ->content($entity->getEmailText() ?? '')
            ->smsText($entity->getSmsText() ?? '')
        ;

        if ($entity->getEdition() instanceof Edition) {
            $notification->edition($entity->getEdition());
        }

        if ($notification instanceof EmailToAwareNotificationInterface) {
            $notification->emailTo($entity->getEmailTo());
        }

        if ($this->replyTo) {
            $notification->replyTo($this->replyTo);
        }

        if ($entity->getEmailReplyTo()) {
            $notification->replyTo($entity->getEmailReplyTo());
        }

        if ($entity->getEmailAttachment() && ($attachment = $this->getAttachment($entity->getEmailAttachment())) instanceof FilesystemItem) {
            $notification->attachment(new Attachment($this->attachmentsStorage, $attachment));
        }

        return $notification;
    }

    private function getAttachment(DbafsAttachment $attachment): ?FilesystemItem
    {
        return $this->attachmentsStorage->get($attachment->getUuid());
    }
}
