<?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\Repository\WishlistRepository;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;

/**
 * The account-side mirror of a browser-localStorage wishlist (Merkliste).
 *
 * Wishlists remain a client-side feature for everyone: the
 * {@see \Ferienpass\CmsBundle\Wishlist\WishlistSynchronizer} only persists a
 * copy for *authenticated* users so the lists travel across devices.
 *
 * The primary key {@see $id} is a server-generated UUIDv7 (time-ordered, so it
 * indexes well). The localStorage identifier lives separately in
 * {@see $clientUuid} (a random v4 the browser generated); it's how a list is
 * matched across localStorage and the account, and it's optional — only lists
 * that originated on a client carry one. It's unique per user.
 *
 * Conflicts between a device's localStorage and the account are resolved
 * last-write-wins by {@see $modifiedAt}. Deletions are soft (a {@see $deletedAt}
 * tombstone) so they can propagate to other devices and feed later retention
 * policies.
 */
#[ORM\Entity(repositoryClass: WishlistRepository::class)]
#[ORM\UniqueConstraint(name: 'user_client_uuid', columns: ['user_id', 'client_uuid'])]
class Wishlist implements TenantAwareInterface
{
    use TenantAwareTrait;

    #[ORM\Id]
    #[ORM\Column(type: UuidType::NAME, unique: true)]
    private Uuid $id;

    /**
     * The client-generated (localStorage) identifier, when this list
     * originated on a device. Null for lists created server-side.
     */
    #[ORM\Column(name: 'client_uuid', type: UuidType::NAME, nullable: true)]
    private ?Uuid $clientUuid = null;

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

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

    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $modifiedAt;

    /**
     * Soft-delete tombstone. A deleted wishlist is kept (for cross-device
     * delete propagation and retention) but excluded from the active set.
     */
    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeImmutable $deletedAt = null;

    /**
     * @var Collection<int, WishlistItem>
     */
    #[ORM\OneToMany(mappedBy: 'wishlist', targetEntity: WishlistItem::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
    private Collection $items;

    public function __construct(
        #[ORM\ManyToOne(targetEntity: User::class)]
        #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'cascade')]
        private User $user,
        string $name,
        ?Uuid $clientUuid = null,
    ) {
        $this->id = Uuid::v7();
        $this->clientUuid = $clientUuid;
        $this->name = $name;
        $this->createdAt = new \DateTimeImmutable();
        $this->modifiedAt = new \DateTimeImmutable();
        $this->items = new ArrayCollection();
    }

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

    public function getUser(): User
    {
        return $this->user;
    }

    public function getClientUuid(): ?Uuid
    {
        return $this->clientUuid;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = $name;
    }

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

    public function getModifiedAt(): \DateTimeImmutable
    {
        return $this->modifiedAt;
    }

    public function setModifiedAt(\DateTimeImmutable $modifiedAt): void
    {
        $this->modifiedAt = $modifiedAt;
    }

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

    public function isDeleted(): bool
    {
        return $this->deletedAt instanceof \DateTimeImmutable;
    }

    public function setDeletedAt(?\DateTimeImmutable $deletedAt): void
    {
        $this->deletedAt = $deletedAt;
    }

    /**
     * @return Collection<int, WishlistItem>
     */
    public function getItems(): Collection
    {
        return $this->items;
    }

    /**
     * @return list<OfferInterface>
     */
    public function getOffers(): array
    {
        return array_values(array_map(static fn (WishlistItem $i) => $i->getOffer(), $this->items->toArray()));
    }

    public function hasOffer(OfferInterface $offer): bool
    {
        return $this->items->exists(static fn (int $k, WishlistItem $i) => (string) $i->getOffer()->getUuid() === (string) $offer->getUuid());
    }

    public function addOffer(OfferInterface $offer): void
    {
        if ($this->hasOffer($offer)) {
            return;
        }

        $this->items->add(new WishlistItem($this, $offer));
    }

    public function removeOffer(OfferInterface $offer): void
    {
        foreach ($this->items as $item) {
            if ((string) $item->getOffer()->getUuid() === (string) $offer->getUuid()) {
                $this->items->removeElement($item);
            }
        }
    }

    /**
     * Replace the item set so it exactly matches the given offers. Existing
     * items are kept (preserving their createdAt); missing ones are removed
     * and new ones added.
     *
     * @param iterable<OfferInterface> $offers
     */
    public function syncOffers(iterable $offers): void
    {
        $target = [];
        foreach ($offers as $offer) {
            $target[(string) $offer->getUuid()] = $offer;
        }

        foreach ($this->items as $item) {
            if (!isset($target[(string) $item->getOffer()->getUuid()])) {
                $this->items->removeElement($item);
            }
        }

        foreach ($target as $offer) {
            $this->addOffer($offer);
        }
    }
}
