<?php
/**
 * Class for parameter-based Booking querying.
 *
 * @package JET_ABAF\Resources
 */

namespace JET_ABAF\Resources;

defined( 'ABSPATH' ) || exit; // Exit if accessed directly.

class Booking_Query {

	/**
	 * Stores query data.
	 *
	 * @var array
	 */
	protected $query_vars = [];

	public function __construct( $args = [] ) {
		$this->query_vars = wp_parse_args( $args, $this->get_default_query_vars() );
	}

	/**
	 * Get the current query vars.
	 *
	 * @since 3.3.0
	 *
	 * @return array
	 */
	public function get_query_vars() {
		return $this->query_vars;
	}

	/**
	 * Get default query vars.
	 *
	 * Returns list of default valid query variables for bookings.
	 *
	 * @since 3.3.0
	 *
	 * @return array
	 */
	protected function get_default_query_vars() {
		return [
			'status'         => '',
			'include'        => '',
			'exclude'        => '',
			'apartment_id'   => '',
			'apartment_unit' => '',
			'order_id'       => '',
			'user_id'        => '',
			'date_query'     => [],
			'meta_query'     => [],
			'sorting'        => [],
			'limit'          => 0,
			'offset'         => 0,
			'return'         => 'objects',
		];
	}

	/**
	 * Get bookings.
	 *
	 * Returns bookings matching the current query vars.
	 *
	 * @since 3.3.0
	 *
	 * @return array|object of Bookings objects.
	 */
	public function get_bookings() {
		return $this->query( $this->get_query_vars() );
	}

	/**
	 * Query for bookings matching specific criteria.
	 *
	 * @since 3.3.0
	 * @since 3.6.0 Refactored array return type.
	 * @since 4.0.0 Added `booking_vendor` handling.
	 * @since 4.1.0 Added support for additional columns and enhanced query clause handling.
	 *
	 * @param array $query_vars Query variables.
	 *
	 * @return array
	 */
	public function query( $query_vars ) {

		global $wpdb;

		$query_clause = [];

		if ( ! empty( $query_vars['status'] ) ) {
			$statuses = $this->sanitize_text_list( $query_vars['status'] );

			if ( ! empty( $statuses ) ) {
				$placeholders   = implode( ', ', array_fill( 0, count( $statuses ), '%s' ) );
				$query_clause[] = $wpdb->prepare( "status IN ($placeholders)", $statuses );
			}
		}

		if ( ! empty( $query_vars['include'] ) ) {
			$include = $this->sanitize_id_list( $query_vars['include'] );

			if ( ! empty( $include ) ) {
				$placeholders   = implode( ', ', array_fill( 0, count( $include ), '%d' ) );
				$query_clause[] = $wpdb->prepare( "booking_id IN ($placeholders)", $include );
			}
		} else {
			if ( ! empty( $query_vars['exclude'] ) ) {
				$exclude = $this->sanitize_id_list( $query_vars['exclude'] );

				if ( ! empty( $exclude ) ) {
					$placeholders   = implode( ', ', array_fill( 0, count( $exclude ), '%d' ) );
					$query_clause[] = $wpdb->prepare( "booking_id NOT IN ($placeholders)", $exclude );
				}
			}
		}

		if ( ! empty( $query_vars['booking_vendor'] ) ) {
			$vendors = $this->sanitize_id_list( $query_vars['booking_vendor'] );

			if ( ! empty( $vendors ) ) {
				$placeholders   = implode( ', ', array_fill( 0, count( $vendors ), '%d' ) );
				$query_clause[] = $wpdb->prepare( "booking_vendor IN ($placeholders)", $vendors );
			}
		}

		if ( ! empty( $query_vars['apartment_id'] ) ) {
			$apartment_ids = $this->sanitize_id_list( $query_vars['apartment_id'] );

			if ( ! empty( $apartment_ids ) ) {
				$placeholders   = implode( ', ', array_fill( 0, count( $apartment_ids ), '%d' ) );
				$query_clause[] = $wpdb->prepare( "apartment_id IN ($placeholders)", $apartment_ids );

				if ( ! empty( $query_vars['apartment_unit'] ) ) {
					$units = $this->sanitize_id_list( $query_vars['apartment_unit'] );

					if ( ! empty( $units ) ) {
						$placeholders   = implode( ', ', array_fill( 0, count( $units ), '%d' ) );
						$query_clause[] = $wpdb->prepare( "apartment_unit IN ($placeholders)", $units );
					}
				}
			}
		}

		if ( ! empty( $query_vars['order_id'] ) ) {
			$order_ids = $this->sanitize_id_list( $query_vars['order_id'] );

			if ( ! empty( $order_ids ) ) {
				$placeholders   = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
				$query_clause[] = $wpdb->prepare( "order_id IN ($placeholders)", $order_ids );
			}
		}

		if ( ! empty( $query_vars['user_id'] ) ) {
			$user_ids = $this->sanitize_id_list( $query_vars['user_id'] );

			if ( ! empty( $user_ids ) ) {
				$placeholders   = implode( ', ', array_fill( 0, count( $user_ids ), '%d' ) );
				$query_clause[] = $wpdb->prepare( "user_id IN ($placeholders)", $user_ids );
			}
		}

		if ( ! empty( $query_vars['date_query'] ) ) {
			$date_query_clause = [];

			foreach ( $query_vars['date_query'] as $key => $clause ) {
				if ( 'relation' === $key ) {
					continue;
				}

				if ( ! is_array( $clause ) || empty( $clause['column'] ) ) {
					continue;
				}

				$column = sanitize_key( $clause['column'] );

				if ( empty( $column ) ) {
					continue;
				}

				$operator = $this->sanitize_operator( $clause['operator'] ?? '=', $this->get_allowed_operators( 'date' ) );
				$value    = $this->sanitize_date( $clause['value'] ?? null );

				if ( ! $value ) {
					continue;
				}

				if ( 'check_in_date' === $column && ! is_array( $value ) ) {
					$value ++;
				}

				if ( in_array( $operator, [ 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
					if ( ! is_array( $value ) || 2 > count( $value ) ) {
						continue;
					}

					$value               = array_values( $value );
					$date_query_clause[] = $wpdb->prepare( "%i $operator %d AND %d", $column, absint( $value[0] ), absint( $value[1] ) );
				} else {
					if ( is_array( $value ) ) {
						$value = reset( $value );
					}

					$date_query_clause[] = $wpdb->prepare( "%i $operator %d", $column, absint( $value ) );
				}
			}

			if ( ! empty( $date_query_clause ) ) {
				$date_query_rel = $this->sanitize_relation( $query_vars['date_query']['relation'] ?? 'AND' );
				$query_clause[] = '( ' . implode( ' ' . $date_query_rel . ' ', $date_query_clause ) . ' )';
			}
		}

		if ( ! empty( $query_vars['meta_query'] ) ) {
			$meta_query_clause = [];

			foreach ( $query_vars['meta_query'] as $key => $clause ) {
				if ( 'relation' === $key ) {
					continue;
				}

				if ( ! is_array( $clause ) || empty( $clause['column'] ) ) {
					continue;
				}

				$operator = $this->sanitize_operator( $clause['operator'] ?? '=', $this->get_allowed_operators() );

				// Numeric literal column support (used in calendar day filter queries).
				if ( is_numeric( $clause['column'] ) ) {
					if ( ! in_array( $operator, [ 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
						continue;
					}

					if ( ! array_key_exists( 'value', $clause ) || ! is_array( $clause['value'] ) || 2 > count( $clause['value'] ) ) {
						continue;
					}

					$value_keys = [
						is_string( $clause['value'][0] ) ? sanitize_key( $clause['value'][0] ) : '',
						is_string( $clause['value'][1] ) ? sanitize_key( $clause['value'][1] ) : '',
					];

					if ( ! $value_keys[0] || ! $value_keys[1] ) {
						continue;
					}

					$meta_query_clause[] = $wpdb->prepare( "%d $operator %i AND %i", absint( $clause['column'] ), $value_keys[0], $value_keys[1] );

					continue;
				}

				if ( ! is_string( $clause['column'] ) ) {
					continue;
				}

				$column = sanitize_key( $clause['column'] );

				if ( empty( $column ) ) {
					continue;
				}

				if ( in_array( $operator, [ 'IS NULL', 'IS NOT NULL' ], true ) ) {
					$meta_query_clause[] = $wpdb->prepare( '%i ' . $operator, $column );
					continue;
				}

				if ( ! array_key_exists( 'value', $clause ) ) {
					continue;
				}

				$value = $clause['value'];

				if ( in_array( $operator, [ 'IN', 'NOT IN' ], true ) ) {
					$values = is_array( $value ) ? $value : array_map( 'trim', explode( ',', $value ) );
					$values = array_values( array_filter( $values, static function ( $val ) {
						return '' !== $val && null !== $val;
					} ) );

					if ( empty( $values ) ) {
						continue;
					}

					$value_placeholders = [];
					$value_args         = [];

					foreach ( $values as $val ) {
						$value_placeholders[] = is_numeric( $val ) ? '%d' : '%s';
						$value_args[]         = $val;
					}

					$placeholders        = implode( ', ', $value_placeholders );
					$meta_query_clause[] = $wpdb->prepare( "%i $operator ($placeholders)", array_merge( [ $column ], $value_args ) );

					continue;
				}

				if ( in_array( $operator, [ 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
					if ( ! is_array( $value ) || 2 > count( $value ) ) {
						continue;
					}

					$value              = array_values( $value );
					$first_placeholder  = is_numeric( $value[0] ) ? '%d' : '%s';
					$second_placeholder = is_numeric( $value[1] ) ? '%d' : '%s';

					$meta_query_clause[] = $wpdb->prepare( "%i $operator $first_placeholder AND $second_placeholder", array_merge( [ $column ], [ $value[0], $value[1] ] ) );

					continue;
				}

				if ( is_array( $value ) ) {
					$value = reset( $value );
				}

				$is_string_operator = in_array( $operator, [ 'LIKE', 'NOT LIKE', 'REGEXP', 'NOT REGEXP' ], true );
				$value_placeholder  = $is_string_operator ? '%s' : ( is_numeric( $value ) ? '%d' : '%s' );

				$meta_query_clause[] = $wpdb->prepare( "%i $operator $value_placeholder", [ $column, $value ] );
			}

			if ( ! empty( $meta_query_clause ) ) {
				$meta_query_rel = $this->sanitize_relation( $query_vars['meta_query']['relation'] ?? 'AND' );
				$query_clause[] = '( ' . implode( ' ' . $meta_query_rel . ' ', $meta_query_clause ) . ' )';
			}
		}

		$query = $wpdb->prepare( "SELECT * FROM %i", jet_abaf()->db->bookings->table() );

		if ( ! empty( $query_clause ) ) {
			$query .= ' WHERE ' . implode( ' AND ', $query_clause );
		}

		if ( ! empty( $query_vars['sorting'] ) && is_array( $query_vars['sorting'] ) ) {
			$sorting_query_clause = [];

			foreach ( $query_vars['sorting'] as $sorting ) {
				if ( empty( $sorting['orderby'] ) ) {
					continue;
				}

				$orderby = sanitize_key( $sorting['orderby'] );

				if ( empty( $orderby ) ) {
					continue;
				}

				$order = ! empty( $sorting['order'] ) ? strtoupper( $sorting['order'] ) : 'ASC';

				if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
					$order = 'ASC';
				}

				$sorting_query_clause[] = $wpdb->prepare( '%i', $orderby ) . ' ' . $order;
			}

			if ( ! empty( $sorting_query_clause ) ) {
				$query .= ' ORDER BY ' . implode( ', ', $sorting_query_clause );
			}
		}

		if ( ! empty( $query_vars['limit'] ) && intval( $query_vars['limit'] ) > 0 ) {
			$query .= $wpdb->prepare( ' LIMIT %d', absint( $query_vars['limit'] ) );

			if ( ! empty( $query_vars['offset'] ) ) {
				$query .= $wpdb->prepare( ' OFFSET %d', absint( $query_vars['offset'] ) );
			}
		}

		$results = $wpdb->get_results( $query, ARRAY_A );

		return array_filter( array_map( function ( $item ) use ( $query_vars ) {
			$booking = new Booking( $item );

			if ( isset( $query_vars['return'] ) && 'arrays' === $query_vars['return'] ) {
				return $booking->get_object_vars();
			} else {
				return $booking;
			}
		}, $results ) );

	}

	/**
	 * Sanitize a list of IDs from a string or array.
	 *
	 * @since 4.1.0
	 *
	 * @param mixed $value List of IDs.
	 *
	 * @return int[]
	 */
	protected function sanitize_id_list( $value ) {

		if ( empty( $value ) ) {
			return [];
		}

		if ( is_string( $value ) ) {
			$ids = wp_parse_id_list( $value );
		} else {
			$ids = array_map( 'absint', (array) $value );
			$ids = array_filter( $ids );
		}

		return array_values( $ids );

	}

	/**
	 * Sanitize a list of strings from a string or array.
	 *
	 * @since 4.1.0
	 *
	 * @param mixed $value List of strings.
	 *
	 * @return string[]
	 */
	protected function sanitize_text_list( $value ) {

		if ( empty( $value ) ) {
			return [];
		}

		$values = is_array( $value ) ? $value : array_map( 'trim', explode( ',', $value ) );
		$values = array_map( 'sanitize_text_field', $values );
		$values = array_filter( $values, 'strlen' );

		return array_values( $values );

	}

	/**
	 * Sanitize a query relation.
	 *
	 * @since 4.1.0
	 *
	 * @param string $relation Relation value.
	 *
	 * @return string
	 */
	protected function sanitize_relation( $relation ) {

		$relation = strtoupper( $relation );

		return in_array( $relation, [ 'AND', 'OR' ], true ) ? $relation : 'AND';

	}

	/**
	 * Sanitize a query operator.
	 *
	 * @since 4.1.0
	 *
	 * @param string $operator Operator value.
	 * @param array  $allowed  Allowed operators.
	 *
	 * @return string
	 */
	protected function sanitize_operator( $operator, $allowed ) {

		$operator = strtoupper( trim( $operator ) );

		return in_array( $operator, $allowed, true ) ? $operator : '=';

	}

	/**
	 * Allowed operators for query clauses.
	 *
	 * @since 4.1.0
	 *
	 * @param string $context Operators context: "date" or "meta".
	 *
	 * @return string[]
	 */
	protected function get_allowed_operators( $context = 'meta' ) {

		$operators = [ '=', '!=', '>', '>=', '<', '<=', 'BETWEEN', 'NOT BETWEEN' ];

		if ( 'date' === $context ) {
			return $operators;
		}

		return array_merge( $operators, [
			'LIKE',
			'NOT LIKE',
			'IN',
			'NOT IN',
			'REGEXP',
			'NOT REGEXP',
			'IS NULL',
			'IS NOT NULL',
		] );

	}

	/**
	 * Sanitizes date query.
	 *
	 * @since 3.3.0
	 * @since 4.1.0 Refactored to handle both single and range date queries.
	 *
	 * @param string|array $date Date query parameter row..
	 *
	 * @return int|int[]|false
	 */
	public function sanitize_date( $date ) {

		if ( empty( $date ) ) {
			return false;
		}

		if ( is_array( $date ) ) {
			$date_1 = is_numeric( $date[0] ) ? absint( $date[0] ) : strtotime( $date[0] );
			$date_2 = is_numeric( $date[1] ) ? absint( $date[1] ) : strtotime( $date[1] );

			if ( ! $date_1 || ! $date_2 ) {
				return false;
			}

			return [ $date_1, $date_2 ];
		} else {
			$date = is_numeric( $date ) ? absint( $date ) : strtotime( $date );
		}

		return $date;

	}

}
