<?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\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Facade\PaymentsFacade;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

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

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

    #[ORM\OneToMany(mappedBy: 'debtor', targetEntity: Payment::class)]
    private Collection $payments;

    #[ORM\OneToMany(mappedBy: 'debtor', targetEntity: BookEntry::class)]
    #[ORM\OrderBy(['bookingDate' => 'DESC'])]
    private Collection $bookEntries;

    public function __construct(#[ORM\OneToOne(inversedBy: 'debtor', targetEntity: User::class)]
        #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
        private ?User $user = null)
    {
        $this->uuid = Uuid::v7();
        $this->payments = new ArrayCollection();
        $this->bookEntries = new ArrayCollection();
    }

    /*
     * For array_unique function
     */
    public function __toString(): string
    {
        return $this->uuid->toRfc4122();
    }

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

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

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

    public function getPayments(): Collection
    {
        return $this->payments;
    }

    public function getBookEntries(): Collection
    {
        return $this->bookEntries;
    }

    /**
     * @param string[] $editionAliases Scope the balance to book entries whose
     *                                 payment relates to one of these editions.
     *                                 Empty array = all editions (unscoped).
     */
    public function getBalance(array $editionAliases = []): int
    {
        return array_sum(
            $this->getBookEntries()
                ->filter(static fn (BookEntry $e) => !$e->isPayment())
                ->filter(fn (BookEntry $e) => $this->bookEntryMatchesEditions($e, $editionAliases))
                ->map(static fn (BookEntry $e) => $e->getValue())
                ->toArray()
        );
    }

    public function getSummedPayments(): int
    {
        return array_sum(
            $this->getBookEntries()
                ->filter(static fn (BookEntry $e) => $e->isPayment())
                ->map(static fn (BookEntry $e) => $e->getValue())
                ->toArray()
        );
    }

    /**
     * @param string[] $editionAliases Restrict to attendances whose offer
     *                                 belongs to one of these editions. Empty
     *                                 array = all editions (unscoped).
     *
     * @return Attendance[]
     */
    public function getAttendancesDue(array $editionAliases = []): array
    {
        if (!$this->user instanceof User) {
            return [];
        }

        $criteria = Criteria::create();
        PaymentsFacade::addCriteria($criteria);

        $attendances = array_merge(
            ...$this->user->getParticipants()
            ->map(static fn (ParticipantInterface $participant) => $participant->getAttendances()->matching($criteria)->toArray())
            ->toArray()
        );

        // Seasons that don't collect fees owe nothing, no matter what the
        // attendance's own payable/paid flags say.
        $attendances = array_values(array_filter(
            $attendances,
            static fn (Attendance $a) => $a->usesPayments()
        ));

        if ([] !== $editionAliases) {
            $attendances = array_values(array_filter(
                $attendances,
                static fn (Attendance $a) => \in_array($a->getOffer()->getEdition()?->getAlias(), $editionAliases, true)
            ));
        }

        return $attendances;
    }

    /**
     * @param string[] $editionAliases
     */
    public function getDueSum(?EventDispatcherInterface $dispatcher = null, array $editionAliases = []): int
    {
        return array_sum(
            array_map(
                static fn (Attendance $a) => $a->getOffer()->getFeePayable($a->getParticipant(), $dispatcher),
                $this->getAttendancesDue($editionAliases)
            )
        );
    }

    /**
     * A book entry is always created off a payment, so we attribute it to an
     * edition via that payment's items → attendance → offer → edition.
     *
     * @param string[] $editionAliases
     */
    private function bookEntryMatchesEditions(BookEntry $entry, array $editionAliases): bool
    {
        if ([] === $editionAliases) {
            return true;
        }

        $payment = $entry->getPayment();
        if (null === $payment) {
            return false;
        }

        foreach ($payment->getItems() as $item) {
            $alias = $item->getAttendance()?->getOffer()->getEdition()?->getAlias();
            if (null !== $alias && \in_array($alias, $editionAliases, true)) {
                return true;
            }
        }

        return false;
    }
}
