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

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ferienpass\CoreBundle\Repository\SentMessageRepository;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent;
use Symfony\Component\RemoteEvent\Event\Mailer\MailerEngagementEvent;
use Symfony\Component\Uid\Uuid;

/**
 * Indexes live here, not on the subclasses: with SINGLE_TABLE inheritance Doctrine
 * overwrites a child's table definition with the root's, and the schema tool skips
 * child classes entirely — so an index declared on {@see SentEmail} is silently
 * dropped on the floor. (A fulltext index on emailData sat there for exactly that
 * long, never created, never used.) The columns an index covers may still be mapped
 * on a subclass; they all land in this one table.
 *
 * (type, createdAt) serves the outbox list: every query filters on the discriminator
 * and then sorts by createdAt or bounds it to a time window, which was a filesort
 * over the whole table before. The recipient columns are deliberately *not* indexed —
 * the search matches them with a leading wildcard, which no B-tree can seek; what
 * made that search slow was reading the emailData blob, not the missing index.
 */
#[ORM\Entity(repositoryClass: SentMessageRepository::class)]
#[ORM\Index(name: 'idx_sentmessage_type_created', columns: ['type', 'createdAt'])]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap(['email' => SentEmail::class, 'sms' => SentSms::class])]
class SentMessage implements TenantAwareInterface
{
    use TenantAwareTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer', options: ['unsigned' => true])]
    private ?int $id = null;

    #[ORM\Column(type: UuidType::NAME)]
    private Uuid $uuid;

    #[ORM\ManyToOne(targetEntity: MessengerLog::class, inversedBy: 'notifications')]
    #[ORM\JoinColumn(name: 'log_id', referencedColumnName: 'id', onDelete: 'SET NULL')]
    private ?MessengerLog $logEntry = null;

    /**
     * The conversation this message opened or belongs to: the outbound item a
     * recipient can reply to. Replies thread under it (or its ancestors) as
     * further {@see ConversationItem}s, which the Postausgang renders as a
     * conversation. Nullable — most system mails are one-way and never gain a
     * thread. Promotes the uuid formerly kept only inside SentEmail's JSON.
     */
    #[ORM\ManyToOne(targetEntity: ConversationItem::class)]
    #[ORM\JoinColumn(name: 'conversation_id', referencedColumnName: 'uuid', nullable: true, onDelete: 'SET NULL')]
    private ?ConversationItem $conversation = null;

    #[ORM\Column(type: 'datetime_immutable', options: ['default' => 'CURRENT_TIMESTAMP'])]
    private \DateTimeInterface $createdAt;

    #[ORM\OneToMany(mappedBy: 'message', targetEntity: SentMessageEngagement::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $engagements;

    public function __construct()
    {
        $this->uuid = Uuid::v7();
        $this->createdAt = new \DateTimeImmutable();
        $this->engagements = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUuid(): Uuid
    {
        return $this->uuid;
    }

    public function isEmail(): bool
    {
        return SentEmail::class === static::class;
    }

    public function isSms(): bool
    {
        return SentSms::class === static::class;
    }

    public function getCreatedAt(): \DateTimeInterface
    {
        return $this->createdAt;
    }

    public function setLogEntry(?MessengerLog $logEntry): void
    {
        $this->logEntry = $logEntry;
    }

    public function getLogEntry(): ?MessengerLog
    {
        return $this->logEntry;
    }

    public function getConversation(): ?ConversationItem
    {
        return $this->conversation;
    }

    public function setConversation(?ConversationItem $conversation): void
    {
        $this->conversation = $conversation;
    }

    public function getEngagements(): Collection
    {
        return $this->engagements;
    }

    public function isOpened(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerEngagementEvent::OPEN === $e->getEvent())
            ->isEmpty()
        ;
    }

    public function isClicked(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerEngagementEvent::CLICK === $e->getEvent())
            ->isEmpty()
        ;
    }

    public function isBounced(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerDeliveryEvent::BOUNCE === $e->getEvent())
            ->isEmpty()
        ;
    }

    public function isDelivered(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerDeliveryEvent::DELIVERED === $e->getEvent())
            ->isEmpty()
        ;
    }

    public function isDropped(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerDeliveryEvent::DROPPED === $e->getEvent())
            ->isEmpty()
        ;
    }

    public function isUnsubscribed(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerEngagementEvent::UNSUBSCRIBE === $e->getEvent())
            ->isEmpty()
        ;
    }

    public function isSpam(): bool
    {
        return !$this->engagements
            ->filter(static fn (SentMessageEngagement $e) => MailerEngagementEvent::SPAM === $e->getEvent())
            ->isEmpty()
        ;
    }

    /**
     * The furthest delivery stage this message reached, derived from its engagements.
     *
     * Returns one of: gesendet, zugestellt, geoeffnet, geklickt, bounce — with the later
     * stages taking precedence. Falls back to "gesendet" when no engagement is recorded
     * (e.g. before the mailer webhook reports back), so the UI degrades gracefully.
     */
    public function getDeliveryStatus(): string
    {
        return self::deliveryStatusFor($this->engagements->map(static fn (SentMessageEngagement $e) => $e->getEvent())->toArray());
    }

    /**
     * The same reduction over a bare list of event names, for callers that read
     * engagements in bulk rather than through the entity's lazy collection — so the
     * precedence lives in one place and cannot drift between the two paths.
     *
     * @param string[] $events
     */
    public static function deliveryStatusFor(array $events): string
    {
        return match (true) {
            \in_array(MailerDeliveryEvent::BOUNCE, $events, true) || \in_array(MailerDeliveryEvent::DROPPED, $events, true) => 'bounce',
            \in_array(MailerEngagementEvent::CLICK, $events, true) => 'geklickt',
            \in_array(MailerEngagementEvent::OPEN, $events, true) => 'geoeffnet',
            \in_array(MailerDeliveryEvent::DELIVERED, $events, true) => 'zugestellt',
            default => 'gesendet',
        };
    }
}
