<?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\AdminBundle\Components;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Debtor;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\Entity\PaymentItem;
use Ferienpass\CoreBundle\Enum\PaymentStatus;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use Ferienpass\CoreBundle\Repository\DebtorRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;

#[AsLiveComponent(name: 'Admin:SettleAttendances', route: 'live_component_admin')]
#[IsGranted('ROLE_ADMIN')]
class SettleAttendances extends AbstractController
{
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    #[LiveProp]
    public array $ids = [];

    #[LiveProp]
    public ?string $debtorId = null;

    #[LiveProp(writable: true)]
    public bool $useBalance = true;

    #[LiveProp(writable: true)]
    public array $items = [];

    #[LiveProp(writable: true)]
    public string $address = '';

    #[LiveProp(writable: true)]
    #[Assert\Email(message: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.')]
    public string $email = '';

    #[LiveProp(writable: true)]
    public bool $paid = true;

    #[LiveProp(writable: true)]
    #[Assert\Expression(
        'value == false or this.paid == true',
        message: 'Eine Benachrichtigung kann nur gesendet werden, wenn die Zahlung erfolgt ist.'
    )]
    #[Assert\Expression(
        'value == false or this.email != ""',
        message: 'Eine Benachrichtigung kann nur gesendet werden, wenn eine E-Mail-Adresse hinterlegt ist.'
    )]
    public bool $notify = true;
    public ?Payment $newPayment = null;

    public function __construct(private readonly AttendanceRepository $attendanceRepository, private readonly EventDispatcherInterface $dispatcher)
    {
    }

    public function mount(array $ids): void
    {
        $this->ids = $ids;
        $attendances = $this->attendanceRepository->findBy(['id' => $ids]);
        $attendances = array_filter($attendances, static fn (Attendance $a) => !$a->isPaid());

        $possibleDebtors = array_unique(array_filter(array_map(static fn (Attendance $a) => $a->getParticipant()->getUser()?->getDebtor(), $attendances)));
        /** @var Debtor $debtor */
        $debtor = $possibleDebtors[0] ?? new Debtor();
        $this->debtorId = $debtor->getUuid()->toBase58();

        $payment = Payment::fromAttendances($debtor, $attendances, $this->dispatcher, user: $this->getUser());

        if ($debtor->getBalance() > 0) {
            // Clamp the credit to the bill so an over-balance can't drive the
            // payment negative (ledger integrity — see SECURITY_REVIEW FP-012),
            // mirroring the frontend Payment::redirectToPay() invariant.
            $balance = min($debtor->getBalance(), $payment->getTotalAmount());
            $payment->addItem(new PaymentItem($payment, (-1) * $balance, type: 'discount'));
        }

        $this->address = (string) $payment->getBillingAddress();
        $this->email = (string) $payment->getBillingEmail();

        foreach ($payment->getItems() as $item) {
            $this->items[] = [
                'amount' => $item->getAmount() / 100,
                'label' => $item->isAttendance() ? ($item->getAttendance()->getOffer()->getName().' ('.$item->getAttendance()->getParticipant()->getName().')') : 'Guthaben',
                'attendanceId' => $item->getAttendance()?->getId(),
                'participant' => $item->isAttendance() ? $item->getAttendance()->getParticipant()->getName() : null,
                'offer' => $item->isAttendance() ? $item->getAttendance()->getOffer()->getName() : null,
                'type' => $item->getType(),
            ];
        }
    }

    public function getTotalAmount(): float
    {
        return (float) array_sum(array_column(array_filter($this->items, fn ($item) => $this->useBalance || 'discount' !== ($item['type'] ?? '')), 'amount'));
    }

    #[LiveAction]
    public function toggleUseBalance(): void
    {
        $this->useBalance = !$this->useBalance;
    }

    #[LiveAction]
    public function removeItem(#[LiveArg] int $index): void
    {
        if (isset($this->items[$index]['attendanceId'])) {
            $this->ids = array_values(array_diff($this->ids, [$this->items[$index]['attendanceId']]));
        }

        unset($this->items[$index]);
        $this->items = array_values($this->items);
    }

    #[LiveListener('debtor_balance:changed')]
    public function onBalanceChanged(DebtorRepository $debtorRepository): void
    {
        if (null === $this->debtorId) {
            return;
        }

        $debtor = $debtorRepository->findOneBy(['uuid' => Uuid::fromString($this->debtorId)]);
        if (null === $debtor) {
            return;
        }

        $balance = $debtor->getBalance();
        $found = false;

        foreach ($this->items as $index => $item) {
            if ('discount' === ($item['type'] ?? null)) {
                if ($balance > 0) {
                    $this->items[$index]['amount'] = (-1) * $balance / 100;
                    $found = true;
                } else {
                    unset($this->items[$index]);
                }
                break;
            }
        }

        if (!$found && $balance > 0) {
            $this->items[] = [
                'amount' => (-1) * $balance / 100,
                'label' => 'Guthaben',
                'attendanceId' => null,
                'participant' => null,
                'offer' => null,
                'type' => 'discount',
            ];
        }

        $this->items = array_values($this->items);
    }

    #[LiveAction]
    public function submit(EntityManagerInterface $entityManager, AttendanceRepository $attendanceRepository, WorkflowInterface $paymentStateMachine): void
    {
        $attendances = $attendanceRepository->findBy(['id' => $this->ids]);
        $attendances = array_filter($attendances, static fn (Attendance $a) => !$a->isPaid());

        $possibleDebtors = array_unique(array_filter(array_map(static fn (Attendance $a) => $a->getParticipant()->getUser()?->getDebtor(), $attendances)));
        $debtor = $possibleDebtors[0] ?? new Debtor();

        $payment = new Payment($debtor, user: $this->getUser());
        $payment->setBillingAddress($this->address);
        $payment->setBillingEmail($this->email);

        foreach ($this->items as $itemData) {
            if (!$this->useBalance && 'discount' === ($itemData['type'] ?? '')) {
                continue;
            }
            $attendance = isset($itemData['attendanceId']) ? $attendanceRepository->find($itemData['attendanceId']) : null;
            $payment->addItem(new PaymentItem($payment, (int) round($itemData['amount'] * 100), $attendance, $itemData['type'] ?? 'attendance'));
        }

        if ($this->paid) {
            $paymentStateMachine->apply($payment, PaymentStatus::TRANSITION_PAY, ['notify' => $this->notify]);
        }

        $this->newPayment = $payment;

        $entityManager->persist($payment);
        $entityManager->flush();
    }
}
