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

use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

#[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'chill_main_notification')]
#[ORM\Index(columns: ['relatedentityclass', 'relatedentityid'], name: 'chill_main_notification_related_entity_idx')]
class Notification implements TrackUpdateInterface
{
    #[ORM\Column(type: Types::TEXT, nullable: false)]
    private string $accessKey;

    private array $addedAddresses = [];

    /**
     * @var Collection<int, User>
     */
    #[ORM\ManyToMany(targetEntity: User::class)]
    #[ORM\JoinTable(name: 'chill_main_notification_addresses_user')]
    private Collection $addressees;

    /**
     * @var Collection<int, UserGroup>
     */
    #[ORM\ManyToMany(targetEntity: UserGroup::class)]
    #[ORM\JoinTable(name: 'chill_main_notification_addressee_user_group')]
    private Collection $addresseeUserGroups;

    /**
     * a list of destinee which will receive notifications.
     *
     * @var array|string[]
     */
    #[ORM\Column(type: Types::JSON, options: ['default' => '[]', 'jsonb' => true])]
    private array $addressesEmails = [];

    /**
     * a list of emails adresses which were added to the notification.
     *
     * @var array|string[]
     */
    private array $addressesEmailsAdded = [];

    private ?ArrayCollection $addressesOnLoad = null;

    /**
     * @var Collection<int, NotificationComment>
     */
    #[ORM\OneToMany(mappedBy: 'notification', targetEntity: NotificationComment::class, orphanRemoval: true)]
    #[ORM\OrderBy(['createdAt' => \Doctrine\Common\Collections\Criteria::ASC])]
    private Collection $comments;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    private \DateTimeImmutable $date;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: Types::INTEGER)]
    private ?int $id = null;

    #[ORM\Column(type: Types::TEXT)]
    private string $message = '';

    #[ORM\Column(type: Types::STRING, length: 255)]
    private string $relatedEntityClass = '';

    #[ORM\Column(type: Types::INTEGER)]
    private int $relatedEntityId;

    private array $removedAddresses = [];

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(nullable: true)]
    private ?User $sender = null;

    #[Assert\NotBlank(message: 'notification.Title must be defined')]
    #[ORM\Column(type: Types::TEXT, options: ['default' => ''])]
    private string $title = '';

    /**
     * @var Collection<int, User>
     */
    #[ORM\ManyToMany(targetEntity: User::class)]
    #[ORM\JoinTable(name: 'chill_main_notification_addresses_unread')]
    private Collection $unreadBy;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
    private ?\DateTimeImmutable $updatedAt = null;

    #[ORM\ManyToOne(targetEntity: User::class)]
    private ?User $updatedBy = null;

    #[ORM\Column(name: 'type', type: Types::STRING, nullable: true)]
    private string $type = '';

    public function __construct()
    {
        $this->addressees = new ArrayCollection();
        $this->addresseeUserGroups = new ArrayCollection();
        $this->unreadBy = new ArrayCollection();
        $this->comments = new ArrayCollection();
        $this->setDate(new \DateTimeImmutable());
        $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
    }

    public function addAddressee(User|UserGroup $addressee): self
    {
        if ($addressee instanceof User) {
            if (!$this->addressees->contains($addressee)) {
                $this->addressees->add($addressee);
                $this->addedAddresses[] = $addressee;
            }

            return $this;
        }

        if (!$this->addresseeUserGroups->contains($addressee)) {
            $this->addresseeUserGroups->add($addressee);
        }

        return $this;
    }

    /**
     * @deprecated
     */
    public function addAddressesEmail(string $email)
    {
        if (!\in_array($email, $this->addressesEmails, true)) {
            $this->addressesEmails[] = $email;
            $this->addressesEmailsAdded[] = $email;
        }
    }

    public function addComment(NotificationComment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments[] = $comment;
            $comment->setNotification($this);
        }

        return $this;
    }

    public function addUnreadBy(User $user): self
    {
        if (!$this->unreadBy->contains($user)) {
            $this->unreadBy[] = $user;
        }

        return $this;
    }

    /**
     * @param array $payload
     */
    #[Assert\Callback]
    public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
    {
        if (0 === (\count($this->getAddresseeUserGroups()) + \count($this->getAddressees()))) {
            $context->buildViolation('notification.At least one addressee')
                ->atPath('addressees')
                ->addViolation();
        }
    }

    public function getAddresseeUserGroups(): Collection
    {
        return $this->addresseeUserGroups;
    }

    public function setAddresseeUserGroups(Collection $addresseeUserGroups): void
    {
        $this->addresseeUserGroups = $addresseeUserGroups;
    }

    public function getAccessKey(): string
    {
        return $this->accessKey;
    }

    public function getAddedAddresses(): array
    {
        return $this->addedAddresses;
    }

    /**
     * @return Collection|User[]
     */
    public function getAddressees(): Collection
    {
        // keep a copy to compute changes later
        if (null === $this->addressesOnLoad) {
            $this->addressesOnLoad = new ArrayCollection($this->addressees->toArray());
        }

        return $this->addressees;
    }

    public function getAllAddressees(): array
    {
        $allUsers = [];

        foreach ($this->getAddressees() as $user) {
            $allUsers[$user->getId()] = $user;
        }

        foreach ($this->getAddresseeUserGroups() as $userGroup) {
            foreach ($userGroup->getUsers() as $user) {
                $allUsers[$user->getId()] = $user;
            }
        }

        return array_values($allUsers);
    }

    /**
     * @return array|string[]
     */
    public function getAddressesEmails(): array
    {
        return $this->addressesEmails;
    }

    /**
     * @return array|string[]
     */
    public function getAddressesEmailsAdded(): array
    {
        return $this->addressesEmailsAdded;
    }

    public function getComments(): Collection
    {
        return $this->comments;
    }

    public function getDate(): ?\DateTimeImmutable
    {
        return $this->date;
    }

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

    public function getMessage(): ?string
    {
        return $this->message;
    }

    public function getRelatedEntityClass(): ?string
    {
        return $this->relatedEntityClass;
    }

    public function getRelatedEntityId(): ?int
    {
        return $this->relatedEntityId;
    }

    public function getSender(): ?User
    {
        return $this->sender;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function getUnreadBy(): Collection
    {
        return $this->unreadBy;
    }

    public function getUpdatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function getUpdatedBy(): ?User
    {
        return $this->updatedBy;
    }

    public function isReadBy(User $user): bool
    {
        return !$this->unreadBy->contains($user);
    }

    public function isSystem(): bool
    {
        return null === $this->sender;
    }

    public function markAsReadBy(User $user): self
    {
        return $this->removeUnreadBy($user);
    }

    public function markAsUnreadBy(User $user): self
    {
        return $this->addUnreadBy($user);
    }

    #[ORM\PreFlush]
    public function registerUnread()
    {
        foreach ($this->addedAddresses as $addressee) {
            $this->addUnreadBy($addressee);
        }

        foreach ($this->removedAddresses as $addressee) {
            $this->removeAddressee($addressee);
        }

        if (null !== $this->addressesOnLoad) {
            foreach ($this->addressees as $existingAddresse) {
                if (!$this->addressesOnLoad->contains($existingAddresse)) {
                    $this->addUnreadBy($existingAddresse);
                }
            }

            foreach ($this->addressesOnLoad as $onLoadAddressee) {
                if (!$this->addressees->contains($onLoadAddressee)) {
                    $this->removeUnreadBy($onLoadAddressee);
                }
            }
        }

        $this->removedAddresses = [];
        $this->addedAddresses = [];
        $this->addressesOnLoad = null;
    }

    public function removeAddressee(User|UserGroup $addressee): self
    {
        if ($addressee instanceof User) {
            if ($this->addressees->contains($addressee)) {
                $this->addressees->removeElement($addressee);

                return $this;
            }
        }

        $this->addresseeUserGroups->removeElement($addressee);

        return $this;
    }

    public function removeAddressesEmail(string $email)
    {
        if (\in_array($email, $this->addressesEmails, true)) {
            $this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email);
            $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email);
        }
    }

    public function removeComment(NotificationComment $comment): self
    {
        $this->comments->removeElement($comment);

        return $this;
    }

    public function removeUnreadBy(User $user): self
    {
        $this->unreadBy->removeElement($user);

        return $this;
    }

    public function setDate(\DateTimeImmutable $date): self
    {
        $this->date = $date;

        return $this;
    }

    public function setMessage(?string $message): self
    {
        $this->message = (string) $message;

        return $this;
    }

    public function setRelatedEntityClass(string $relatedEntityClass): self
    {
        $this->relatedEntityClass = $relatedEntityClass;

        return $this;
    }

    public function setRelatedEntityId(int $relatedEntityId): self
    {
        $this->relatedEntityId = $relatedEntityId;

        return $this;
    }

    public function setSender(?User $sender): self
    {
        $this->sender = $sender;

        return $this;
    }

    public function setTitle(?string $title): Notification
    {
        $this->title = (string) $title;

        return $this;
    }

    public function setUpdatedAt(\DateTimeInterface $datetime): self
    {
        $this->updatedAt = \DateTimeImmutable::createFromInterface($datetime);

        return $this;
    }

    public function setUpdatedBy(User $user): self
    {
        $this->updatedBy = $user;

        return $this;
    }

    public function setType(string $type): self
    {
        $this->type = $type;

        return $this;
    }

    public function getType(): string
    {
        return $this->type;
    }
}
