<?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\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Repository\ConversationItemRepository;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;

/**
 * A unified, cross-entity comment. Replaces the free-text comment that used to
 * live on the per-entity *Log tables (which now only record system events).
 *
 * The subject is polymorphic via one of the nullable foreign keys below —
 * exactly one is set — mirroring the {@see MessengerLog} pattern so we keep real
 * FKs and cascades instead of an unconstrained polymorphic association.
 */
#[ORM\Entity(repositoryClass: ConversationItemRepository::class)]
#[ORM\Index(columns: ['offer_id', 'createdAt'])]
#[ORM\Index(columns: ['participant_id', 'createdAt'])]
#[ORM\Index(columns: ['attendance_id', 'createdAt'])]
#[ORM\Index(columns: ['account_id', 'createdAt'])]
#[ORM\Index(columns: ['host_id', 'createdAt'])]
#[ORM\Index(columns: ['edition_id', 'createdAt'])]
class ConversationItem implements \Stringable
{
    public const CHANNEL_APP = 'app';
    public const CHANNEL_EMAIL = 'email';

    public const VISIBILITY_INTERNAL = 'internal';
    public const VISIBILITY_SHARED = 'shared';

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

    #[ORM\ManyToOne(targetEntity: OfferInterface::class, inversedBy: 'conversation')]
    #[ORM\JoinColumn(name: 'offer_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?OfferInterface $offer = null;

    #[ORM\ManyToOne(targetEntity: ParticipantInterface::class, inversedBy: 'conversation')]
    #[ORM\JoinColumn(name: 'participant_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?ParticipantInterface $participant = null;

    #[ORM\ManyToOne(targetEntity: Attendance::class, inversedBy: 'conversation')]
    #[ORM\JoinColumn(name: 'attendance_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?Attendance $attendance = null;

    #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'conversation')]
    #[ORM\JoinColumn(name: 'account_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?User $account = null;

    #[ORM\ManyToOne(targetEntity: Host::class, inversedBy: 'conversation')]
    #[ORM\JoinColumn(name: 'host_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?Host $host = null;

    #[ORM\ManyToOne(targetEntity: Edition::class, inversedBy: 'conversation')]
    #[ORM\JoinColumn(name: 'edition_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
    private ?Edition $edition = null;

    /**
     * The author as a known user, or null for external senders (email replies
     * from a host/parent we couldn't match) and deleted accounts.
     */
    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: 'author_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
    private ?User $author = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $authorEmail = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $authorName = null;

    #[ORM\Column(type: 'text')]
    private string $body;

    #[ORM\Column(type: 'string', length: 16, options: ['default' => self::CHANNEL_APP])]
    private string $channel = self::CHANNEL_APP;

    /**
     * `internal` = visible to the admin team only; `shared` = a host/parent was
     * notified, so the conversation reached outside the admin team.
     */
    #[ORM\Column(type: 'string', length: 16, options: ['default' => self::VISIBILITY_INTERNAL])]
    private string $visibility = self::VISIBILITY_INTERNAL;

    /**
     * Resolved notify targets, e.g. `[{type:'group',g:'host'},{type:'user',g:'admin',id:42,label:'…'}]`.
     * Drives the "Benachrichtigt" line and the fan-out.
     *
     * @var array<int, array<string, mixed>>|null
     */
    #[ORM\Column(type: 'json', nullable: true)]
    private ?array $notify = null;

    #[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'replies')]
    #[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'uuid', nullable: true, onDelete: 'CASCADE')]
    private ?self $parent = null;

    /**
     * @var Collection<int, self>
     */
    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
    private Collection $replies;

    /**
     * @var Collection<int, ActivityNotification>
     */
    #[ORM\OneToMany(mappedBy: 'conversationItem', targetEntity: ActivityNotification::class, cascade: ['persist', 'remove'])]
    private Collection $notifications;

    #[ORM\OneToMany(mappedBy: 'conversationItem', targetEntity: ConversationItemAttachment::class, cascade: ['persist', 'remove'])]
    private Collection $attachments;

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

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeInterface $editedAt = null;

    /**
     * @param array<int, array<string, mixed>> $notify
     */
    public function __construct(object $subject, ?User $author, string $body, string $channel = self::CHANNEL_APP, ?self $parent = null, array $notify = [], ?string $authorEmail = null, ?string $authorName = null)
    {
        $this->uuid = Uuid::v7();
        $this->createdAt = new \DateTimeImmutable();
        $this->replies = new ArrayCollection();
        $this->notifications = new ArrayCollection();
        $this->attachments = new ArrayCollection();

        $this->setSubject($subject);
        $this->author = $author;
        $this->authorEmail = $authorEmail;
        $this->authorName = $authorName;
        $this->body = $body;
        $this->channel = $channel;
        $this->parent = $parent;
        $this->notify = $notify ?: null;
        $this->visibility = self::deriveVisibility($notify);
    }

    public function __toString(): string
    {
        return $this->uuid->toBase58();
    }

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

    public function getSubject(): object
    {
        return $this->attendance ?? $this->offer ?? $this->participant ?? $this->host ?? $this->edition ?? $this->account
            ?? throw new \LogicException('ConversationItem has no subject.');
    }

    public function getSubjectType(): string
    {
        return match (true) {
            null !== $this->attendance => 'attendance',
            null !== $this->offer => 'offer',
            null !== $this->participant => 'participant',
            null !== $this->host => 'host',
            null !== $this->edition => 'edition',
            null !== $this->account => 'account',
            default => throw new \LogicException('ConversationItem has no subject.'),
        };
    }

    public function getOffer(): ?OfferInterface
    {
        return $this->offer;
    }

    public function getParticipant(): ?ParticipantInterface
    {
        return $this->participant;
    }

    public function getAttendance(): ?Attendance
    {
        return $this->attendance;
    }

    public function getAccount(): ?User
    {
        return $this->account;
    }

    public function getHost(): ?Host
    {
        return $this->host;
    }

    public function getEdition(): ?Edition
    {
        return $this->edition;
    }

    public function getAuthor(): ?User
    {
        return $this->author;
    }

    public function getAuthorEmail(): ?string
    {
        return $this->authorEmail;
    }

    public function getAuthorName(): ?string
    {
        return $this->authorName;
    }

    /**
     * A display name regardless of whether the author is a known user.
     */
    public function getAuthorLabel(): ?string
    {
        return $this->author?->getName() ?? $this->authorName ?? $this->authorEmail;
    }

    public function getBody(): string
    {
        return $this->body;
    }

    public function getChannel(): string
    {
        return $this->channel;
    }

    public function isFromEmail(): bool
    {
        return self::CHANNEL_EMAIL === $this->channel;
    }

    public function getVisibility(): string
    {
        return $this->visibility;
    }

    public function isInternal(): bool
    {
        return self::VISIBILITY_INTERNAL === $this->visibility;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function getNotify(): array
    {
        return $this->notify ?? [];
    }

    public function getParent(): ?self
    {
        return $this->parent;
    }

    public function isReply(): bool
    {
        return null !== $this->parent;
    }

    /**
     * @return Collection<int, self>
     */
    public function getReplies(): Collection
    {
        return $this->replies;
    }

    /**
     * @return Collection<int, ActivityNotification>
     */
    public function getNotifications(): Collection
    {
        return $this->notifications;
    }

    /**
     * @return Collection<int, ConversationItemAttachment>
     */
    public function getAttachments(): Collection
    {
        return $this->attachments;
    }

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

    public function getEditedAt(): ?\DateTimeInterface
    {
        return $this->editedAt;
    }

    public function edit(string $body): void
    {
        $this->body = $body;
        $this->editedAt = new \DateTimeImmutable();
    }

    private function setSubject(object $subject): void
    {
        switch (true) {
            case $subject instanceof Attendance:
                $this->attendance = $subject;
                break;
            case $subject instanceof OfferInterface:
                $this->offer = $subject;
                break;
            case $subject instanceof ParticipantInterface:
                $this->participant = $subject;
                break;
            case $subject instanceof Host:
                $this->host = $subject;
                break;
            case $subject instanceof Edition:
                $this->edition = $subject;
                break;
            case $subject instanceof User:
                $this->account = $subject;
                break;
            default:
                throw new \InvalidArgumentException(\sprintf('Unsupported comment subject "%s".', $subject::class));
        }
    }

    /**
     * @param array<int, array<string, mixed>> $notify
     */
    private static function deriveVisibility(array $notify): string
    {
        foreach ($notify as $target) {
            $group = \is_array($target) ? ($target['g'] ?? null) : null;
            if (\in_array($group, ['host', 'account'], true)) {
                return self::VISIBILITY_SHARED;
            }
        }

        return self::VISIBILITY_INTERNAL;
    }
}
