<?php
/**
 * @package   DPCalendar
 * @copyright Copyright (C) 2018 Digital Peak GmbH. <https://www.digital-peak.com>
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
 */

namespace DigitalPeak\Component\DPCalendar\Administrator\Booking\Stages;

\defined('_JEXEC') or die();

use DigitalPeak\Component\DPCalendar\Administrator\Exception\TicketExhaustedException;
use DigitalPeak\Component\DPCalendar\Administrator\Helper\Booking;
use DigitalPeak\Component\DPCalendar\Administrator\Helper\DPCalendarHelper;
use DigitalPeak\Component\DPCalendar\Administrator\Model\CouponModel;
use DigitalPeak\Component\DPCalendar\Administrator\Model\CurrencyModel;
use DigitalPeak\Component\DPCalendar\Administrator\Model\TaxrateModel;
use DigitalPeak\Component\DPCalendar\Administrator\Pipeline\StageInterface;
use Joomla\CMS\Application\AdministratorApplication;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Language\Text;
use Joomla\CMS\User\User;
use Joomla\Registry\Registry;

class SetupForNew implements StageInterface
{
	public function __construct(
		private readonly CMSApplicationInterface $application,
		private readonly User $user,
		private readonly TaxrateModel $taxRateModel,
		private readonly CouponModel $couponModel,
		private readonly CurrencyModel $currencyModel,
		private readonly Registry $params,
		private readonly bool $autoAssignUser
	) {
	}

	public function __invoke(\stdClass $payload): \stdClass
	{
		// Default some data
		$event                             = null;
		$payload->data['price']            = 0;
		$payload->data['price_details']    = [];
		$payload->data['price_tickets']    = 0.0;
		$payload->data['price_options']    = 0.0;
		$payload->data['events_discount']  = 0.0;
		$payload->data['tickets_discount'] = 0.0;
		$payload->data['tax']              = 0.0;
		$payload->data['tax_rate']         = 0.0;
		$payload->data['coupon_id']        = empty($payload->data['coupon_id']) ? 0 : $payload->data['coupon_id'];
		$payload->data['coupon_area']      = 0;
		$payload->data['coupon_rate']      = 0.0;
		$payload->data['id']               = 0;
		$payload->data['currency']         = $this->currencyModel->getActualCurrency()->currency;
		$payload->discounts                = [
			'events_discount_amount'  => 0,
			'events_discount_title'   => '',
			'events_discount_value'   => 0,
			'tickets_discount_amount' => 0,
			'tickets_discount_title'  => '',
			'tickets_discount_value'  => 0
		];

		// On front we force the user id to the logged in user
		if ($this->autoAssignUser) {
			$payload->data['user_id'] = $this->user->id;
		}

		// Collect the data
		$amountTickets            = 0;
		$amountEvents             = 0;
		$payload->data['options'] = [];
		foreach ($payload->events as $event) {
			$this->currencyModel->setupCurrencyPrices($event);

			$this->handleOptions($payload, $event);
			$eventTickets = $this->handleTickets($payload, $event, $amountTickets);

			if ($eventTickets > 0) {
				$amountEvents++;
			}

			$amountTickets += $eventTickets;
		}
		$payload->data['options'] = implode(',', $payload->data['options']);

		// When no amount is found, then abort
		if ($amountTickets === 0 && !$payload->data['price_options']) {
			throw new TicketExhaustedException(Text::_('COM_DPCALENDAR_BOOK_ERROR_NEEDS_TICKETS'));
		}

		if ($amountEvents && !empty($payload->original_event) && !empty($payload->original_event->events_discount)) {
			$savedDiscount = null;
			$newPrice      = (int)$payload->data['price_tickets'];
			foreach ($payload->original_event->events_discount as $discount) {
				$amount = (int)$discount->amount;
				if ($amountEvents < $amount || ($savedDiscount !== null && (int)$savedDiscount->amount > $amount)) {
					continue;
				}

				$savedDiscount = $discount;

				// @phpstan-ignore-next-line
				$payload->discounts['events_discount_title']  = $discount->label ?? '';
				$payload->discounts['events_discount_amount'] = $amount;
				$payload->discounts['events_discount_value']  = $discount->type === 'value' ? DPCalendarHelper::renderPrice($discount->value) : $discount->value . '%';
				$payload->data['events_discount']             = (int)$discount->value;

				if ($discount->type !== 'percentage') {
					$newPrice = $payload->data['price_tickets'] - (int)$discount->value;
					continue;
				}

				$newPrice                         = $payload->data['price_tickets'] - (($payload->data['price_tickets'] / 100) * (int)$discount->value);
				$newPrice                         = $newPrice < 0 ? 0 : $newPrice;
				$payload->data['events_discount'] = $payload->data['price_tickets'] - $newPrice;
			}

			$payload->data['price_tickets'] = $newPrice;
		}

		if (!empty($event->tickets_discount)) {
			$savedDiscount = null;
			$newPrice      = (int)$payload->data['price_tickets'];
			foreach ($event->tickets_discount as $discount) {
				$amount = (int)$discount->amount;
				if ($amountTickets < $amount || ($savedDiscount !== null && (int)$savedDiscount->amount > $amount)) {
					continue;
				}

				$savedDiscount = $discount;

				$payload->discounts['tickets_discount_title']  = $discount->label ?? '';
				$payload->discounts['tickets_discount_amount'] = $amount;
				$payload->discounts['tickets_discount_value']  = $discount->type === 'value' ? DPCalendarHelper::renderPrice($discount->value) : $discount->value . '%';
				$payload->data['tickets_discount']             = (int)$discount->value;

				if ($discount->type !== 'percentage') {
					$newPrice = $payload->data['price_tickets'] - (int)$discount->value;
					continue;
				}

				$newPrice                          = $payload->data['price_tickets'] - (($payload->data['price_tickets'] / 100) * (int)$discount->value);
				$newPrice                          = $newPrice < 0 ? 0 : $newPrice;
				$payload->data['tickets_discount'] = $payload->data['price_tickets'] - $newPrice;
			}

			$payload->data['price_tickets'] = $newPrice;
		}

		// The prices for the different
		$priceOptions = $payload->data['price_options'];
		$priceTickets = $payload->data['price_tickets'];

		// Load the coupon
		$coupon = $this->couponModel->getItemByCode(
			empty($payload->data['coupon_id']) ? '' : $payload->data['coupon_id'],
			$event->catid,
			$payload->data['email'],
			$payload->data['user_id']
		);

		// Reset here to ensure an integer
		$payload->data['coupon_id'] = 0;

		// Set the coupon attributes
		if ($coupon instanceof \stdClass && $coupon->id) {
			$payload->data['coupon_id']   = $coupon->id;
			$payload->data['coupon_area'] = $coupon->area;
		}

		// Tickets price
		if ($coupon instanceof \stdClass && $coupon->id && $coupon->area == 2) {
			$payload->data['price_tickets'] = $this->calculatePrice($coupon, $payload->data['price_tickets']);
		}

		// Options price
		if ($coupon instanceof \stdClass && $coupon->id && $coupon->area == 3) {
			$payload->data['price_options'] = $this->calculatePrice($coupon, $payload->data['price_options']);
		}

		// Set the price from the price minus the coupon price
		$payload->data['price']       = $payload->data['price_tickets'] + $payload->data['price_options'];
		$payload->data['coupon_rate'] = $priceTickets + $priceOptions - $payload->data['price_tickets'] - $payload->data['price_options'];

		// Subtract from full price
		if ($coupon instanceof \stdClass && $coupon->id && $coupon->area == 1) {
			$payload->data['price']       = $this->calculatePrice($coupon, $payload->data['price']);
			$payload->data['coupon_rate'] = $priceTickets + $priceOptions - $payload->data['price'];
		}

		if (($userDiscount = Booking::getPriceWithDiscount($payload->data['price'], $event, '-1', '', 'booking')) !== 0.0) {
			$payload->data['user_group_discount'] = $payload->data['price'] - $userDiscount;
			$payload->data['price']               = $userDiscount;
		}

		if (($earlybirdDiscount = Booking::getPriceWithDiscount($payload->data['price'], $event, '', '-1', 'booking')) !== 0.0) {
			$payload->data['earlybird_discount'] = $payload->data['price'] - $earlybirdDiscount;
			$payload->data['price']              = $earlybirdDiscount;
		}

		// Determine tax
		$taxRate = empty($payload->data['country']) ? null : $this->taxRateModel->getItemByCountry($payload->data['country']);

		// Check if there is a tax free custom field
		if ($fieldName = $this->params->get('bookingsys_tax_free_custom_field')) {
			foreach ($payload->events as $event) {
				// Ensure fields are loaded
				if (empty($event->jcfields)) {
					$this->application->triggerEvent('onContentPrepare', ['com_dpcalendar.event', $event, $this->params, 0]);
				}

				// Still no fields
				if (empty($event->jcfields)) {
					continue;
				}

				// Loop over the fields
				foreach ($event->jcfields as $field) {
					// If the field is the one we are looking for and the value resolves to true, unset the tax
					if ($field->name == $fieldName && filter_var($field->rawvalue, FILTER_VALIDATE_BOOLEAN)) {
						$taxRate = null;
					}
				}
			}
		}

		if ($taxRate instanceof \stdClass) {
			$payload->data['tax'] = $taxRate->inclusive ? $payload->data['price'] - ($payload->data['price'] / (1 + ($taxRate->rate / 100))) : ($payload->data['price'] / 100) * $taxRate->rate;
			$payload->data['price'] += $taxRate->inclusive ? 0 : $payload->data['tax'];
			$payload->data['tax_rate']      = $taxRate->rate;
			$payload->data['tax_title']     = $taxRate->title;
			$payload->data['tax_inclusive'] = $taxRate->inclusive;
		}

		// Do not force state when not on front and is available
		if (!$this->application instanceof AdministratorApplication || empty($payload->data['state'])) {
			if (!\array_key_exists('state', $payload->data)) {
				$payload->data['state'] = 0;
			}

			// When skipping the review step always, set state to tickets reviewed
			if ($payload->data['state'] == 0
				&& $this->params->get('booking_review_step', 2) == 0) {
				$payload->data['state'] = 2;
			}

			// When skipping to review step on one ticket, set state to tickets reviewed
			if ($payload->data['state'] == 0
				&& $this->params->get('booking_review_step', 2) == 2
				&& $amountTickets === 1) {
				$payload->data['state'] = 2;
			}

			// When tickets are reviewed, capacity is not full and confirmation step should be skipped, set state to active
			if (!$payload->data['price']
				&& ($event->capacity === null || $event->capacity_used < $event->capacity)
				&& $payload->data['state'] == 2
				&& !$this->params->get('booking_confirm_step', 1)) {
				$payload->data['state'] = 1;
			}

			// When tickets are reviewed and capacity is full and waiting list is active, put it on the waiting list
			$event = reset($payload->events);
			if ($payload->data['state'] == 2 && ((is_countable($payload->events) ? \count($payload->events) : 0) === 1 || $event->booking_series != 2)
				&& $event->capacity != null && $event->capacity_used >= $event->capacity && $event->booking_waiting_list) {
				$payload->data['state'] = 8;
			}

			// Ensure when tickets are on waiting list, that booking has the correct state
			if ((int)$event->waiting_list_count > 0) {
				$payload->data['state'] = 8;
			}
		}

		// Compute the token
		if ($this->params->get('bookingsys_enable_token')) {
			$payload->data['token'] = bin2hex(random_bytes(16));
		}

		// Return the payload
		return $payload;
	}

	/**
	 * Determine the price for the options of the event.
	 */
	private function handleOptions(\stdClass $payload, \stdClass $event): void
	{
		// The booking options
		if (empty($payload->data['event_id'][$event->id]['options']) || !$event->booking_options) {
			return;
		}

		$amount = $payload->data['event_id'][$event->id]['options'];
		foreach ($event->booking_options as $key => $option) {
			$key = (int)preg_replace('/\D/', '', (string)$key);
			if (empty($amount[$key])) {
				continue;
			}

			// Set the original price
			$priceOriginal = $option->value * $amount[$key];

			// Get the price with a discount
			$priceDiscount = Booking::getPriceWithDiscount($option->value, $event, '', '', 'option') * $amount[$key];

			$payload->data['options'][] = $event->id . '-' . $key . '-' . $amount[$key] . '-' . $priceOriginal . '-' . $priceDiscount;

			$payload->data['price_details'][$event->id]['options'][$key] = [
				'discount' => DPCalendarHelper::renderPrice(number_format($priceDiscount, 2, '.', '')),
				'original' => DPCalendarHelper::renderPrice(number_format($priceOriginal, 2, '.', '')),
				'raw'      => number_format($priceOriginal, 2, '.', '')
			];

			$payload->data['price_options'] += $priceDiscount;
		}
	}

	/**
	 * Determine the price for the tickets of the event.
	 */
	private function handleTickets(\stdClass $payload, \stdClass $event, int $amountTickets): int
	{
		// The tickets to process
		$amount = $payload->data['event_id'][$event->id]['tickets'];
		if (empty($amount)) {
			return 0;
		}

		// Free event
		if (!$event->prices) {
			$event->amount_tickets[0] = $this->getAmountTickets($event, $payload, $amount, 0, 0);

			// Add the event to the list of events with tickets
			if ($event->amount_tickets[0] !== 0) {
				$payload->eventsWithTickets[] = $event;
			}

			// Return the amount of new tickets
			return $event->amount_tickets[0];
		}

		$amountTickets = 0;
		$newTickets    = false;

		// Loop over the prices
		foreach ($event->prices as $key => $price) {
			$key = (int)preg_replace('/\D/', '', (string)$key);

			// Get the amount of tickets
			$event->amount_tickets[$key] = $this->getAmountTickets($event, $payload, $amount, $key, $amountTickets);
			$amountTickets += $event->amount_tickets[$key];

			// Initialize the price details
			$payload->data['price_details'][$event->id]['tickets'][$key] = [
				'discount' => DPCalendarHelper::renderPrice('0.00'),
				'original' => DPCalendarHelper::renderPrice('0.00')
			];

			// Determine the price
			$paymentRequired = Booking::paymentRequired($event);

			// Load the price
			if ($event->amount_tickets[$key] && $paymentRequired) {
				// Set the original price
				$priceOriginal = $price->value * $event->amount_tickets[$key];

				// Get the price with a discount
				$priceDiscount = Booking::getPriceWithDiscount($price->value, $event) * $event->amount_tickets[$key];

				// Set the price details
				$payload->data['price_details'][$event->id]['tickets'][$key] = [
					'discount' => DPCalendarHelper::renderPrice(number_format($priceDiscount, 2, '.', '')),
					'original' => DPCalendarHelper::renderPrice(number_format($priceOriginal, 2, '.', '')),
					'raw'      => number_format($priceOriginal, 2, '.', '')
				];

				// Add the real price to the full price for tickets
				$payload->data['price_tickets'] += $priceDiscount;
			}

			// Ensure there are new tickets
			if ($newTickets) {
				continue;
			}

			if ($event->amount_tickets[$key] === 0) {
				continue;
			}

			$newTickets = true;
		}

		// Add the event to the list of events with tickets
		if ($newTickets) {
			$payload->eventsWithTickets[] = $event;
		}

		// Return the amount of new tickets
		return $amountTickets;
	}

	/**
	 * Determine the price for the options of the event.
	 */
	private function getAmountTickets(\stdClass $event, \stdClass $payload, array $amount, int $key, int $alreadyCollected): int
	{
		// Check if the user or email address has already tickets booked
		$bookedTickets = 0;
		foreach ($event->tickets as $ticket) {
			if (($ticket->email !== $payload->data['email'] && ($this->user->guest || $ticket->user_id != $payload->data['user_id'])) || $ticket->type != $key) {
				continue;
			}
			$bookedTickets++;
		}

		// Make sure booked tickets is correct
		if ($bookedTickets > $event->max_tickets) {
			$bookedTickets = $event->max_tickets;
		}

		// If there are already booked tickets and the limit is hit, reduce the amount
		$amountTickets = $amount[$key] > ($event->max_tickets - $bookedTickets) ? $event->max_tickets - $bookedTickets : $amount[$key];

		// If the amount is bigger than the available space, reduce it when waiting list is not activated
		if ($event->capacity !== null && $amountTickets > ($event->capacity - $event->capacity_used - $alreadyCollected) && !$event->booking_waiting_list) {
			$amountTickets = $event->capacity - $event->capacity_used - $alreadyCollected;
		}

		// If the amount of tickets is 0 raise a warning
		if ($amountTickets < 1 && $amount[$key] > 0) {
			$amountTickets = 0;
			$this->application->enqueueMessage(
				Text::sprintf(
					'COM_DPCALENDAR_BOOK_ERROR_CAPACITY_EXHAUSTED_USER',
					$event->prices ? $event->prices->{'prices' . $key}->label : '',
					$event->title
				),
				'warning'
			);
		}

		return $amountTickets;
	}

	/**
	 * Calculate the price for the coupon.
	 */
	private function calculatePrice(\stdClass $coupon, float $price): float
	{
		// Set the discount price
		$discount = $price;

		// Subtract the value
		if ($coupon->type == 'value') {
			$discount -= $coupon->value;
		}

		// Subtract the percentage
		if ($coupon->type == 'percentage') {
			$discount -= ($price / 100) * $coupon->value;
		}

		// Ensure a valid number
		if ($discount < 0) {
			return 0.0;
		}

		// Return the discount
		return $discount;
	}
}
