<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\Service\Reservation;

use BookiFlex\Core\Exception\InvalidArgumentException;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Core\Exception\NotFoundException;
use BookiFlex\Core\Exception\RuntimeException;
use BookiFlex\Core\Model\Reservation\CancellationActor;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Model\Reservation\ReservationStatus;
use BookiFlex\Core\Repository\ReservationRepository;
use BookiFlex\Core\Util\DateTimeUtils;
use Cycle\ORM\EntityManagerInterface;
use Symfony\Component\Uid\UuidV4;

defined( 'ABSPATH' ) || exit;

class ReservationStatusManager
{
    public function __construct(
        private readonly ReservationRepository $reservationRepository,
        private readonly EntityManagerInterface $entityManager
    ) {
    }

    /**
     * @param int|string $id id or sid
     * @param string $status
     * @return void
     * @throws MoneyCalculationException
     * @throws \Exception
     */
    public function changeStatus(int|string $id, string $status, ?string $reason = null): void
    {
        if ($status === ReservationStatus::STATUS_CONFIRMED) {
            $this->confirmReservation($id);
        } elseif ($status === ReservationStatus::STATUS_CANCELLED) {
            $this->cancelReservation($id, actor: CancellationActor::Admin, reason: $reason);
        } elseif ($status === ReservationStatus::STATUS_WAITING_PAYMENT) {
            $this->prolongTimeForPayment($id);
        } elseif ($status === ReservationStatus::STATUS_IN_HOUSE) {
            $this->inHouse($id);
        } elseif ($status === ReservationStatus::STATUS_NO_SHOW) {
            $this->noShow($id);
        } elseif ($status === ReservationStatus::STATUS_COMPLETED) {
            $this->complete($id);
        } else {
            throw new InvalidArgumentException('Invalid status', 400, context: ['errorCode' => 'invalid_status']);
        }
    }

    /**
     * @param int|string $id id or sid
     * @param string|null $code
     * @param bool $checkCode
     * @return void
     * @throws MoneyCalculationException
     */
    public function cancelReservation(
        int|string $id,
        ?string $code = null,
        bool $checkCode = false,
        CancellationActor $actor = CancellationActor::Admin,
        ?string $reason = null,
    ): void {
        $reservation = $this->getReservation($id);
        if ($checkCode && (empty($code) || $code !== $reservation->getCancellationCode())) {
            throw new InvalidArgumentException('Invalid cancellation code or reservation', 400, context: ['errorCode' => 'invalid_cancellation_code']);
        }

        $reservation->markAsCancelled($actor, reason: $reason);
        $this->save($reservation);
    }

    /**
     * @param int|string $id id or sid
     * @return void
     */
    public function confirmReservation(int|string $id): void
    {
        $reservation = $this->getReservation($id);
        $reservation->markAsConfirmed();
        $this->save($reservation);
    }

    /**
     * @param int|string $id id or sid
     * @return void
     */
    public function needConfirmation(int|string $id): void
    {
        $reservation = $this->getReservation($id);
        $reservation->markAsWaitingForConfirmation();
        $this->save($reservation);
    }

    /**
     * @param int|string $id id or sid
     * @return void
     * @throws \Exception
     */
    public function prolongTimeForPayment(int|string $id): void
    {
        $reservation = $this->getReservation($id);
        $reservation->prolongTimeForPayment();
        $this->save($reservation);
    }

    public function inHouse(int|string $id): void
    {
        $reservation = $this->getReservation($id);
        $reservation->markAsInHouse();
        $this->save($reservation);
    }

    public function noShow(int|string $id): void
    {
        $reservation = $this->getReservation($id);
        $reservation->markAsNoShow();
        $this->save($reservation);
    }

    public function complete(int|string $id): void
    {
        $reservation = $this->getReservation($id);
        $reservation->markAsCompleted();
        $this->save($reservation);
    }

    public function checkOverdue(): void
    {
        /** @var Reservation[] $reservations */
        $reservations = $this->getOverdueReservations();
        foreach ($reservations as $reservation) {
            $reservation->markAsOverdue();
            $this->entityManager->persist($reservation);
        }
        $this->entityManager->run();
    }

    private function getReservation(int|string $id): Reservation
    {
        $reservation = null;
        if (is_numeric($id)) {
            $reservation = $this->reservationRepository->findByPK($id);
        } elseif (UuidV4::isValid($id)) {
            $reservation = $this->reservationRepository->findOne(['sid' => $id]);
        }

        if ($reservation === null) {
            throw new NotFoundException('Reservation not found', 404, context: ['errorCode' => 'reservation_not_found']);
        }
        return $reservation;
    }

    private function getOverdueReservations(): array
    {
        return $this->reservationRepository->findAll([
            'status'                => ReservationStatus::STATUS_WAITING_PAYMENT,
            'waiting_payment_until' => [' <' => DateTimeUtils::nowUtc()->format('Y-m-d H:i:s')],
        ]);
    }

    private function save(Reservation $reservation): void
    {
        try {
            $this->entityManager->persist($reservation);
            $this->entityManager->run();
        } catch (\Exception $e) {
            throw new RuntimeException('Failed to save reservation', 500, $e, ['errorCode' => 'reservation_save_failed']);
        }
    }
}
