<?php
/** 
 * @package     VikBooking
 * @subpackage  core
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2022 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * Taxonomy finance helper class.
 * 
 * @since 	1.16.0 (J) - 1.6.0 (WP)
 */
class VBOTaxonomyFinance
{
	/**
	 * The singleton instance of the class.
	 *
	 * @var VBOTaxonomyFinance
	 */
	protected static $instance = null;

	/**
	 * Class constructor is protected.
	 *
	 * @see 	getInstance()
	 */
	protected function __construct()
	{
		// do nothing
	}

	/**
	 * Returns the global object, either
	 * a new instance or the existing instance
	 * if the class was already instantiated.
	 *
	 * @return 	self 	A new instance of the class.
	 */
	public static function getInstance()
	{
		if (is_null(static::$instance)) {
			static::$instance = new static();
		}

		return static::$instance;
	}

	/**
	 * Returns the number of total units for all rooms, or for a specific room.
	 * By default, the rooms unpublished are skipped, and all rooms are used.
	 * 
	 * @param 	mixed 	$idroom 	int or array.
	 * @param 	bool 	$published 	true or false.
	 *
	 * @return 	int
	 */
	public function countRooms($idroom = 0, $published = true)
	{
		$dbo = JFactory::getDbo();

		$totrooms = 0;
		$clauses = [];

		if (is_int($idroom) && $idroom > 0) {
			$clauses[] = "`id`=" . (int)$idroom;
		} elseif (is_array($idroom) && count($idroom)) {
			$idroom = array_map('intval', $idroom);
			$clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
		}

		if ($published) {
			$clauses[] = "`avail`=1";
		}

		$q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`" . (count($clauses) ? " WHERE " . implode(' AND ', $clauses) : "");
		$dbo->setQuery($q);
		$totrooms = (int)$dbo->loadResult();

		return $totrooms;
	}

	/**
	 * Counts the number of nights of difference between two timestamps.
	 * 
	 * @param 	int 	$to_ts 		the target end date timestamp.
	 * @param 	int 	$from_ts 	the starting date timestamp.
	 * 
	 * @return 	int 	the nights of difference between from and to timestamps.
	 */
	public function countNightsTo($to_ts, $from_ts = 0)
	{
		if (empty($from_ts)) {
			$from_ts = time();
		}

		$from_ymd = date('Y-m-d', $from_ts);
		$to_ymd = date('Y-m-d', $to_ts);

		if ($from_ymd == $to_ymd) {
			return 1;
		}

		$from_date = new DateTime($from_ymd);
		$to_date   = new DateTime($to_ymd);
		$daysdiff  = (int)$from_date->diff($to_date)->format('%a');

		if ($to_ts < $from_ts) {
			// we need a negative integer number in this case
			$daysdiff = $daysdiff - ($daysdiff * 2);
		}

		if ($from_ymd != $to_ymd && $daysdiff > 0) {
			// the to date is actually another night of stay
			$daysdiff += 1;
		}

		return $daysdiff;
	}

	/**
	 * Counts the number of days of difference between two timestamps.
	 * 
	 * @param 	int 	$from_ts 	the starting date timestamp.
	 * @param 	int 	$to_ts 		the target end date timestamp.
	 * 
	 * @return 	int 	the days of difference from the given dates.
	 * 
	 * @since 	1.16.10 (J) - 1.6.10 (WP)
	 */
	public function countDaysDiff($from_ts, $to_ts)
	{
		if (empty($from_ts) || empty($to_ts)) {
			return 0;
		}

		$from_ymd = date('Y-m-d', $from_ts);
		$to_ymd = date('Y-m-d', $to_ts);

		if ($from_ymd == $to_ymd) {
			return 0;
		}

		$from_date = new DateTime($from_ymd);
		$to_date   = new DateTime($to_ymd);
		$daysdiff  = (int)$from_date->diff($to_date)->format('%a');

		if ($to_ts < $from_ts) {
			// we need a negative integer number in this case
			$daysdiff = $daysdiff - ($daysdiff * 2);
		}

		return $daysdiff;
	}


	/**
	 * Helper method to format long currency values into short numbers.
	 * I.e. 2.600.000 = 2.6M (empty decimals are always removed to keep the string short).
	 * 
	 * @param 	int|float 	$num 		the amount to format.
	 * @param 	int 		$decimals 	the precision to use.
	 * 
	 * @return 	string 					the formatted amount string.
	 */
	public function numberFormatShort($num, $decimals = 1)
	{
		// get global formatting values
		$formatvals  = VikBooking::getNumberFormatData();
		$formatparts = explode(':', $formatvals);

		if ($num < 951) {
			// 0 - 950
			$short_amount = number_format($num, $decimals, $formatparts[1], $formatparts[2]);
			$type_amount  = '';
		} elseif ($num < 900000) {
			// 1k - 950k
			$short_amount = number_format($num / 1000, $decimals, $formatparts[1], $formatparts[2]);
			$type_amount  = 'k';
		} elseif ($num < 900000000) {
			// 0.9m - 950m
			$short_amount = number_format($num / 1000000, $decimals, $formatparts[1], $formatparts[2]);
			$type_amount  = 'm';
		} elseif ($num < 900000000000) {
			// 0.9b - 950b
			$short_amount = number_format($num / 1000000000, $decimals, $formatparts[1], $formatparts[2]);
			$type_amount  = 'b';
		} else {
			// >= 0.9t
			$short_amount = number_format($num / 1000000000000, $decimals, $formatparts[1], $formatparts[2]);
			$type_amount  = 't';
		}

		// unpad zeroes from the right
		if ($decimals > 0) {
			while (substr($short_amount, -1, 1) == '0') {
				$short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
			}

			if (substr($short_amount, -1, 1) == $formatparts[1]) {
				// remove also the decimals separator if no more decimals
				$short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
			}
		}
		
		// return the formatted amount string
		return $short_amount . $type_amount;
	}

	/**
	 * Calculate the absolute percent amount between the current and previous financial stat.
	 * This method will use the following calculation: ((A1 - A2) / A2) * 100.
	 * 
	 * @param 	float 	$stat 		the current (previously calculated) financial amount.
	 * @param 	float 	$compare 	the previous period financial amount.
	 * @param 	int 	$precision 	the precision for apply rounding.
	 * 
	 * @return 	int|float 			the absolute percent amount calculated.
	 */
	public function calcAbsPercent($current, $compare, $precision = 1)
	{
		if ($current == $compare) {
			return 0;
		}

		if ($current > $compare && $compare < 1) {
			return 100;
		}

		if ($current < $compare && $current < 1) {
			return 100;
		}

		return round(abs(($current - $compare) / $compare * 100), $precision);
	}

	/**
	 * Obtain booking statistics from a range of dates and an
	 * optional list of room types. The data obtained will be
	 * based on the effective nights of stay within the range,
	 * unless type "booking_dates" to obtain different data.
	 * 
	 * @param 	string 	$from 	the Y-m-d (or website) date from.
	 * @param 	string 	$to 	the Y-m-d (or website) date to.
	 * @param 	array 	$rooms 	list of room IDs to filter.
	 * @param 	string 	$type 	either "stay_dates" or "booking_dates".
	 * 
	 * @return 	array 			associative list of information.
	 * 
	 * @throws 	Exception
	 */
	public function getStats($from, $to, array $rooms = [], $type = 'stay_dates')
	{
		$dbo = JFactory::getDbo();

		// access the availability helper
		$av_helper = VikBooking::getAvailabilityInstance();

		$from_ts = VikBooking::getDateTimestamp($from, 0, 0);
		$to_ts = VikBooking::getDateTimestamp($to, 23, 59, 59);
		if (empty($from) || empty($from_ts) || empty($to_ts) || $to_ts < $from_ts) {
			throw new Exception('Invalid dates provided', 500);
		}

		// total number of days (nights) in the range
		$total_range_nights = $this->countNightsTo($to_ts, $from_ts);
		if ($total_range_nights < 1) {
			throw new Exception('Invalid number of days provided', 500);
		}

		// total number of room units
		$total_room_units = $this->countRooms($rooms);

		if ($total_room_units < 1) {
			throw new Exception('Having no rooms published may lead to divisions by zero, hence errors.', 500);
		}

		// the associative array of statistics to collect and return
		$stats = [
			// booking ids involved
			'bids'            => [],
			// total number of room units counted for stats
			'room_units'      => $total_room_units,
			// total number of room units times the number of nights in the range
			'tot_inventory'   => ($total_room_units * $total_range_nights),
			// number of rooms booked
			'rooms_booked'    => 0,
			// number of bookings found
			'tot_bookings'    => 0,
			// total number of nights booked (proportionally adjusted according to affected dates)
			'nights_booked'   => 0,
			// percent value
			'occupancy'       => 0,
			// average length of stay
			'avg_los'         => 0,
			// points of sale revenue (revenue divided by each individual ota and ibe)
			'pos_revenue'     => [],
			// list of countries with ranking
			'country_ranks'   => [],
			// ibe revenue net
			'ibe_revenue'     => 0,
			// otas revenue net (no matter if commissions were applied)
			'ota_revenue'     => 0,
			// total refunded amounts (never deducted)
			'tot_refunds'     => 0,
			// average daily rate
			'adr'             => 0,
			// revenue per available room
			'revpar'          => 0,
			// average booking window
			'abw'             => 0,
			// amount of all taxes
			'taxes'           => 0,
			// amount of VAT
			'tot_vat'         => 0,
			// amount of city taxes
			'city_taxes'      => 0,
			// amount of damage deposits
			'damage_deposits' => 0,
			// commissions (amount)
			'cmms'            => 0,
			// net revenue before tax (otas + ibe)
			'revenue'         => 0,
			// gross revenue after tax
			'gross_revenue'   => 0,
			// otas total before tax (same as ota_revenue, but only if ota commissions were applied)
			'ota_tot_net'     => 0,
			// otas commissions
			'ota_cmms'        => 0,
			// percent value for average ota commissions amount
			'ota_avg_cmms'    => 0,
			// commission savings amount
			'cmm_savings'     => 0,
			// total cancelled reservations
			'tot_cancellations' => 0,
			// cancelled reservation IDs
			'cancellation_ids'  => [],
			// total amount of cancelled bookings
			'cancellations_amt' => 0,
		];

		// get all (real/completed) bookings in the given range of dates
		$q = $dbo->getQuery(true);
		$q->select($dbo->qn([
			'o.id',
			'o.ts',
			'o.status',
			'o.days',
			'o.checkin',
			'o.checkout',
			'o.totpaid',
			'o.coupon',
			'o.roomsnum',
			'o.total',
			'o.idorderota',
			'o.channel',
			'o.country',
			'o.tot_taxes',
			'o.tot_city_taxes',
			'o.tot_fees',
			'o.tot_damage_dep',
			'o.cmms',
			'o.refund',
			'or.idorder',
			'or.idroom',
			'or.optionals',
			'or.cust_cost',
			'or.cust_idiva',
			'or.extracosts',
			'or.room_cost',
			'c.country_name',
		]));
		$q->from($dbo->qn('#__vikbooking_orders', 'o'));
		$q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
		$q->leftjoin($dbo->qn('#__vikbooking_countries', 'c') . ' ON ' . $dbo->qn('c.country_3_code') . ' = ' . $dbo->qn('o.country'));
		$q->where($dbo->qn('o.total') . ' > 0');
		$q->where($dbo->qn('o.closure') . ' = 0');
		// use the "andWhere" method after having set some "where" clauses
		$q->andWhere([
			$dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'),
			$dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'),
		], 'OR');
		if ($type == 'stay_dates') {
			// regular calculation based on stay dates
			$q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
			$q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
		} else {
			// calculation based on booked dates
			$q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
			$q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
		}

		/**
		 * Do not filter the room booking records by room ID, but always join them, to improve accuracy with calculations.
		 * 
		 * @since 	1.18.0 (J) - 1.8.0 (WP)
		 */
		if ($rooms) {
			// $q->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $rooms)) . ')');
		}

		$q->order($dbo->qn('o.checkin') . ' ASC');
		$q->order($dbo->qn('o.id') . ' ASC');
		$q->order($dbo->qn('or.id') . ' ASC');

		$dbo->setQuery($q);
		$records = $dbo->loadAssocList();

		if (!$records) {
			// no bookings found, do not proceed
			return $stats;
		}

		/**
		 * Join the busy records afterwards to support accurate calculations for split stays or early departures/late arrivals.
		 * This is also needed to support multi-room reservations without joining the busy records together with the room records.
		 * 
		 * @since 	1.18.0 (J) - 1.8.0 (WP)
		 */
		$busy_joined = [];
		foreach ($records as &$booking) {
			if (strcasecmp($booking['status'], 'confirmed')) {
				// we only want to target confirmed bookings
				continue;
			}

			// build cache signature for busy records joined
			$booking_room_signature = $booking['id'] . '-' . $booking['idroom'];

			// build the db query
			$dbo->setQuery(
				$dbo->getQuery(true)
					->select($dbo->qn('ob.idbusy'))
					->select($dbo->qn('b.checkin', 'room_checkin'))
					->select($dbo->qn('b.checkout', 'room_checkout'))
					->from($dbo->qn('#__vikbooking_ordersbusy', 'ob'))
					->leftjoin($dbo->qn('#__vikbooking_busy', 'b') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
					->where($dbo->qn('ob.idorder') . ' = ' . (int) $booking['id'])
					->where($dbo->qn('b.idroom') . ' = ' . (int) $booking['idroom'])
					->order($dbo->qn('b.id') . ' ASC')
			);

			// either fetch the records or use the previously cached ones to support multi-room bookings with equal room IDs
			$busy_joined[$booking_room_signature] = $busy_joined[$booking_room_signature] ?? $dbo->loadAssocList();

			// get the busy record to process for the current booking-room by shifting the list
			$process_busy = array_shift($busy_joined[$booking_room_signature]);

			if (!$process_busy) {
				continue;
			}

			// inject busy details
			$booking['idbusy'] = $process_busy['idbusy'];
			$booking['room_checkin'] = $process_busy['room_checkin'];
			$booking['room_checkout'] = $process_busy['room_checkout'];
		}

		// unset last reference and cached values
		unset($booking, $busy_joined);

		/**
		 * Immediately count cancellations and unset the records found.
		 * 
		 * @since 	1.16.10 (J) - 1.6.10 (WP)
		 */
		foreach ($records as $k => $b) {
			if (!strcasecmp($b['status'], 'cancelled')) {
				if (!in_array($b['id'], $stats['cancellation_ids'])) {
					// add statistics for the cancelled booking only once
					$stats['tot_cancellations']++;
					$stats['cancellations_amt'] += (float) $b['total'];
					$stats['cancellation_ids'][] = $b['id'];
				}

				// get rid of this record to not alter the regular statistics
				unset($records[$k]);
			}
		}

		if ($stats['cancellation_ids']) {
			// reset key values
			$records = array_values($records);
		}

		// nest records with multiple rooms booked inside sub-arrays
		$bookings = [];
		foreach ($records as $b) {
			if (!isset($bookings[$b['id']])) {
				$bookings[$b['id']] = [];
			}

			// calculate the effective from and to stay timestamps for this room (by supporting split stays or early departures/late arrivals)
			$room_checkin  = !empty($b['room_checkin']) && $b['room_checkin'] != $b['checkin'] ? $b['room_checkin'] : $b['checkin'];
			$room_checkout = !empty($b['room_checkout']) && $b['room_checkout'] != $b['checkout'] ? $b['room_checkout'] : $b['checkout'];
			$in_info  = getdate($room_checkin);
			$out_info = getdate($room_checkout);
			$b['stay_from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
			$b['stay_to_ts']   = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
			$b['stay_nights']  = $room_checkin != $b['checkin'] || $room_checkout != $b['checkout'] ? $av_helper->countNightsOfStay($room_checkin, $room_checkout) : $b['days'];
			$b['stay_nights']  = $b['stay_nights'] < 1 ? 1 : $b['stay_nights'];

			// push room-booking
			$bookings[$b['id']][] = $b;
		}

		// free memory up
		unset($records);

		// counters and pools
		$los_counter = 0;
		$pos_pool 	 = [];
		$pos_counter = [];
		$countries 	 = [];
		$country_map = [];

		// sum of booking window
		$sum_booking_window = 0;

		// parse all bookings
		foreach ($bookings as $bid => $booking) {
			// push booking ID
			$stats['bids'][] = $bid;

			// increase tot bookings and los counter
			if (!$rooms || ($rooms && array_intersect(array_column($booking, 'idroom'), $rooms))) {
				// make sure to increase counters only if filtered listing(s) involved or if no filters set
				$stats['tot_bookings']++;
				$los_counter += $booking[0]['days'];
			}

			// count rooms booked within the reservation
			$booking_rooms = count($booking);

			// define the total booking amount for multi-room reservations
			$multi_room_total = 0;

			if ($rooms && $booking[0]['roomsnum'] > 1) {
				// when filters applied to multi-room bookings, count the effective number of rooms involved
				$booking_rooms = count(array_intersect(array_column($booking, 'idroom'), $rooms));

				// overwrite room total amount when filtering by listing(s)
				foreach ($booking as $room_booking) {
					if (!in_array($room_booking['idroom'], $rooms)) {
						continue;
					}
					if ($room_booking['cust_cost'] > 0) {
						$multi_room_total += $room_booking['cust_cost'];
					} elseif ($room_booking['room_cost'] > 0) {
						$multi_room_total += $room_booking['room_cost'];
					}
				}
			}

			// point of sale name
			$pos_name = null;

			// parse all rooms booked
			foreach ($booking as $room_booking) {
				if ($rooms && !in_array($room_booking['idroom'], $rooms)) {
					// room booked is excluded from filters
					continue;
				}

				// increase rooms booked
				$stats['rooms_booked']++;

				// number of nights affected
				$los_affected = $room_booking['stay_nights'];

				// use default total values
				$room_total 		 = $multi_room_total ?: $room_booking['total'];
				$room_cmms 			 = $room_booking['cmms'];
				$room_refund 		 = $room_booking['refund'];
				$room_tot_taxes 	 = $room_booking['tot_taxes'];
				$room_tot_city_taxes = $room_booking['tot_city_taxes'];
				$room_tot_damage_dep = $room_booking['tot_damage_dep'];
				$room_tot_fees 		 = $room_booking['tot_fees'];

				// check if amounts must be calculated proportionally for the range of dates requested
				if ($type == 'stay_dates' && ($room_booking['stay_from_ts'] < $from_ts || $room_booking['stay_to_ts'] > $to_ts)) {
					// calculate number of nights of stay affected
					$los_affected = $this->countNightsAffected($from_ts, $to_ts, $room_booking['stay_from_ts'], $room_booking['stay_to_ts']);

					// adjust the amounts proportionally
					$room_total 		 = $room_total * $los_affected / $room_booking['stay_nights'];
					$room_cmms 			 = $room_cmms * $los_affected / $room_booking['stay_nights'];
					$room_refund 		 = $room_refund * $los_affected / $room_booking['stay_nights'];
					$room_tot_taxes 	 = $room_tot_taxes * $los_affected / $room_booking['stay_nights'];
					$room_tot_city_taxes = $room_tot_city_taxes * $los_affected / $room_booking['stay_nights'];
					$room_tot_damage_dep = $room_tot_damage_dep * $los_affected / $room_booking['stay_nights'];
					$room_tot_fees 		 = $room_tot_fees * $los_affected / $room_booking['stay_nights'];
				}

				// apply average values per room booked (with filters or booked in total)
				$room_total 		 /= $booking_rooms;
				$room_cmms 			 /= $booking_rooms;
				$room_refund 		 /= $booking_rooms;
				$room_tot_taxes 	 /= $booking_rooms;
				$room_tot_city_taxes /= $booking_rooms;
				$room_tot_damage_dep /= $room_booking['roomsnum'];
				$room_tot_fees 		 /= $booking_rooms;

				// calculate and sum average values per room booked
				$tot_net = $multi_room_total ?: ($room_total - (float) $room_tot_taxes - (float) $room_tot_city_taxes - (float) $room_tot_fees - (float) $room_tot_damage_dep - (float) $room_cmms);
				$tot_revenue = $multi_room_total ? ($tot_net / $booking_rooms) : $tot_net;
				$stats['revenue'] += $tot_revenue;
				$stats['gross_revenue'] += $room_total;
				$stats['nights_booked'] += $los_affected;

				// increase booking window for this room-booking
				$booking_window = $this->countDaysDiff($room_booking['ts'], $room_booking['stay_from_ts']);
				$sum_booking_window += $booking_window >= 0 ? $booking_window : 0;

				// increase country stats
				$country_code = !empty($room_booking['country']) ? $room_booking['country'] : 'unknown';
				if (!isset($countries[$country_code])) {
					$countries[$country_code] = 0;
					if (!empty($room_booking['country_name'])) {
						$country_map[$country_code] = $room_booking['country_name'];
					}
				}
				$countries[$country_code] += $tot_revenue;

				if (!empty($room_booking['idorderota']) && !empty($room_booking['channel'])) {
					$stats['ota_revenue'] += $tot_revenue;
					if ($room_cmms > 0) {
						$stats['ota_tot_net'] += $tot_revenue;
						$stats['ota_cmms'] += $room_cmms;
					}
					// set pos name
					$channel_parts = explode('_', $room_booking['channel']);
					$pos_name = trim($channel_parts[0]);
				} else {
					$stats['ibe_revenue'] += $tot_revenue;
					// set pos name
					$pos_name = 'website';
				}

				// set pos net revenue
				if (!isset($pos_pool[$pos_name])) {
					$pos_pool[$pos_name] = 0;
				}
				$pos_pool[$pos_name] += $tot_revenue;

				$stats['taxes'] += (float) $room_tot_taxes + (float) $room_tot_city_taxes + (float) $room_tot_fees;
				$stats['tot_vat'] += (float) $room_tot_taxes;
				$stats['city_taxes'] += (float) $room_tot_city_taxes;
				$stats['damage_deposits'] += (float) $room_tot_damage_dep;
				$stats['cmms'] += (float) $room_cmms;
				$stats['tot_refunds'] += $room_refund;
			}

			if ($pos_name) {
				// increase number of bookings for this pos
				if (!isset($pos_counter[$pos_name])) {
					$pos_counter[$pos_name] = 0;
				}
				$pos_counter[$pos_name]++;
			}
		}

		// count the average length of stay (no proportional data for the dates requested)
		$stats['avg_los'] = $stats['tot_bookings'] > 0 ? round($los_counter / $stats['tot_bookings'], 1) : 0;

		// calculate occupancy percent value
		$stats['occupancy'] = round(($stats['nights_booked'] * 100 / ($total_room_units * $total_range_nights)), 2);

		// count the average daily rate (ADR)
		$stats['adr'] = $stats['rooms_booked'] > 0 ? $stats['revenue'] / $stats['rooms_booked'] / $total_range_nights : 0;

		// count the revenue per available room (RevPAR)
		$stats['revpar'] = $stats['revenue'] / $total_room_units;

		// count the average booking window (ABW - number of days between the reservation date and the check-in date)
		$stats['abw'] = $stats['rooms_booked'] > 0 ? $sum_booking_window / $stats['rooms_booked'] : 0;

		// count OTAs average commission amount
		if ($stats['ota_tot_net'] > 0 && $stats['ota_cmms'] > 0) {
			// find the average percent value of OTA commissions (tot_net : tot_cmms = 100 : x)
			$stats['ota_avg_cmms'] = round(($stats['ota_cmms'] * 100 / $stats['ota_tot_net']), 2);

			if ($stats['ibe_revenue'] > 0) {
				// calculate the commission savings amount
				$stats['cmm_savings'] = $stats['ibe_revenue'] * $stats['ota_avg_cmms'] / 100;
			}
		}

		// get channel logos helper
		$vcm_logos = VikBooking::getVcmChannelsLogo('', true);

		// sort and build pos revenues
		if ($pos_pool && $stats['revenue'] > 0) {
			// apply sorting descending
			arsort($pos_pool);
			// build readable pos values
			foreach ($pos_pool as $pos_name => $pos_revenue) {
				$pos_data = [
					'name' 	   => $pos_name,
					'revenue'  => $pos_revenue,
					'pcent'    => round(($pos_revenue * 100 / $stats['revenue']), 2),
					'logo' 	   => null,
					'bookings' => isset($pos_counter[$pos_name]) ? $pos_counter[$pos_name] : 0,
					'ibe' 	   => false,
					'ota' 	   => false,
				];
				if (!strcasecmp($pos_name, 'website')) {
					// ibe revenue
					$pos_data['name'] = JText::translate('VBORDFROMSITE');
					$pos_data['ibe']  = true;
				} else {
					// ota revenue
					$pos_data['ota'] = true;
					if (is_object($vcm_logos)) {
						$ota_logo_img = $vcm_logos->setProvenience($pos_name)->getSmallLogoURL();
						if ($ota_logo_img !== false) {
							$pos_data['logo'] = $ota_logo_img;
						}
					}
				}
				// push pos data
				$stats['pos_revenue'][] = $pos_data;
			}
		}

		// sort countries revenue
		if ($countries && $stats['revenue'] > 0) {
			// apply sorting descending
			arsort($countries);
			// build readable values
			foreach ($countries as $country_code => $country_revenue) {
				// push country data
				$stats['country_ranks'][] = [
					'code' 	  => $country_code,
					'name' 	  => (isset($country_map[$country_code]) ? $country_map[$country_code] : $country_code),
					'revenue' => $country_revenue,
					'pcent'   => round(($country_revenue * 100 / $stats['revenue']), 2),
				];
			}
		}

		// return the statistics information
		return $stats;
	}

	/**
	 * Returns the information of the given rate plan ID.
	 * 
	 * @param 	int 	$rplan_id 	The rate plan ID.
	 * 
	 * @return 	?array
	 * 
	 * @since 	1.18.2 (J) - 1.8.2 (WP)
	 */
	public function getRatePlanData(int $rplan_id)
	{
		if (empty($rplan_id)) {
			return null;
		}

		$dbo = JFactory::getDbo();

		$dbo->setQuery(
			$dbo->getQuery(true)
				->select('*')
				->from($dbo->qn('#__vikbooking_prices'))
				->where($dbo->qn('id') . ' = ' . $rplan_id)
		);

		return $dbo->loadAssoc();
	}

	/**
	 * Counts the number of nights involved in a range of dates. This is
	 * useful to proportionally calculate the amounts to be used.
	 * 
	 * @param 	int 	$from_ts 	the 00:00:00 timestamp of the range start date.
	 * @param 	int 	$to_ts 		the 23:59:59 timestamp of the range end date.
	 * @param 	int 	$in_ts 		the 00:00:00 timestamp of the check-in date.
	 * @param 	int 	$out_ts 	the 23:59:59 timestamp of the last night of stay (check-out -1).
	 * 
	 * @return 	int 				the number of stay nights involved in the range.
	 */
	protected function countNightsAffected($from_ts, $to_ts, $in_ts, $out_ts)
	{
		$nights_affected = 0;

		if ($from_ts > $to_ts) {
			return $nights_affected;
		}

		$range_from_info = getdate($from_ts);
		while ($range_from_info[0] < $to_ts) {
			if ($range_from_info[0] >= $in_ts && $range_from_info[0] <= $out_ts) {
				$nights_affected++;
			}
			// next day iteration
			$range_from_info = getdate(mktime(0, 0, 0, $range_from_info['mon'], ($range_from_info['mday'] + 1), $range_from_info['year']));
		}

		return $nights_affected;
	}
}
