<?php

namespace SugarCalendarBookings\Calendar\Admin;

use DateTimeImmutable;
use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Options;
use SugarCalendarBookings\Plugin;
use SugarCalendarBookings\Support\Database\QueryBuilder;
use SugarCalendarBookings\Support\Helpers\Date;

/**
 * CalendarQuery class.
 *
 * @since 1.0.0
 */
class CalendarQuery {

	/**
	 * Base URL.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private $base_url;

	/**
	 * Parameters.
	 *
	 * @since 1.0.0
	 * @since 1.3.0 Made protected.
	 *
	 * @var array
	 */
	protected $filters;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param string $base_url Base URL.
	 * @param array  $filters  Query parameters.
	 */
	public function __construct( $base_url, $filters = [] ) {

		$this->base_url = $base_url;

		$filters = array_intersect_key( $filters, $this->get_defaults() );

		$this->filters = wp_parse_args( $filters, $this->get_defaults() );
	}

	/**
	 * Get the base URL.
	 *
	 * @since 1.2.1
	 *
	 * @return string
	 */
	public function get_base_url() {

		return $this->base_url;
	}

	/**
	 * Get default parameters.
	 *
	 * @since 1.0.0
	 * @since 1.3.0 Made protected.
	 *
	 * @return string[]
	 */
	protected function get_defaults() {

		$today = $this->get_today();

		return [
			'mode'       => 'month',
			'status'     => '',
			'search'     => '',
			'year'       => (int) $today->format( 'Y' ),
			'month'      => (int) $today->format( 'n' ),
			'day'        => (int) $today->format( 'j' ),
			'service_id' => '',
		];
	}

	/**
	 * Get current filters query parameters.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_applied_filters() {

		$filters = [
			'mode'       => $this->get_mode(),
			'status'     => $this->get_status(),
			'search'     => $this->get_search(),
			'year'       => $this->get_year(),
			'month'      => $this->get_month(),
			'day'        => $this->get_day(),
			'service_id' => $this->get_service_id(),
		];

		$filters = array_filter( $filters, fn( $filter ) => $filter !== '' );

		return $filters;
	}

	/**
	 * Get current view mode.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_mode() {

		return sanitize_key( wp_unslash( $this->filters['mode'] ) );
	}

	/**
	 * Get current status.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_status() {

		return sanitize_key( wp_unslash( $this->filters['status'] ) );
	}

	/**
	 * Get current search.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_search() {

		return sanitize_text_field( wp_unslash( $this->filters['search'] ) );
	}

	/**
	 * Get current year.
	 *
	 * @since 1.0.0
	 *
	 * @return int
	 */
	public function get_year() {

		return (int) $this->filters['year'];
	}

	/**
	 * Get current month.
	 *
	 * @since 1.0.0
	 *
	 * @return int
	 */
	public function get_month() {

		return (int) $this->filters['month'];
	}

	/**
	 * Get current day.
	 *
	 * @since 1.0.0
	 *
	 * @return int
	 */
	public function get_day() {

		return (int) $this->filters['day'];
	}

	/**
	 * Get current parameters date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_date() {

		return $this
			->get_today()
			->setDate( $this->get_year(), $this->get_month(), $this->get_day() );
	}

	/**
	 * Get today's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_today() {

		static $today = null;

		if ( is_null( $today ) ) {
			$today = Date::create()->setTimezone( Date::get_default_timezone() );
		}

		return $today;
	}

	/**
	 * Get previous day's date.
	 *
	 * @since 1.1.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_previous_day() {

		static $previous_day = null;

		if ( is_null( $previous_day ) ) {
			$previous_day = $this->get_date()->modify( '-1 day' );
		}

		return $previous_day;
	}

	/**
	 * Get previous week's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_previous_week() {

		static $previous_week = null;

		if ( is_null( $previous_week ) ) {
			$previous_week = $this->get_date()->modify( '-1 week' );
		}

		return $previous_week;
	}

	/**
	 * Get previous month's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_previous_month() {

		static $previous_month = null;

		if ( is_null( $previous_month ) ) {
			$previous_month = $this->get_date()->modify( '-1 month' );
		}

		return $previous_month;
	}

	/**
	 * Get next day's date.
	 *
	 * @since 1.1.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_next_day() {

		static $next_day = null;

		if ( is_null( $next_day ) ) {
			$next_day = $this->get_date()->modify( '+1 day' );
		}

		return $next_day;
	}

	/**
	 * Get next week's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_next_week() {

		static $next_week = null;

		if ( is_null( $next_week ) ) {
			$next_week = $this->get_date()->modify( '+1 week' );
		}

		return $next_week;
	}

	/**
	 * Get next month's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_next_month() {

		static $next_month = null;

		if ( is_null( $next_month ) ) {
			$next_month = $this->get_date()->modify( '+1 month' );
		}

		return $next_month;
	}

	/**
	 * Get previous years's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_previous_year() {

		static $previous_year = null;

		if ( is_null( $previous_year ) ) {
			$previous_year = $this->get_date()->modify( '-1 year' );
		}

		return $previous_year;
	}

	/**
	 * Get next years's date.
	 *
	 * @since 1.0.0
	 *
	 * @return DateTimeImmutable
	 */
	public function get_next_year() {

		static $next_year = null;

		if ( is_null( $next_year ) ) {
			$next_year = $this->get_date()->modify( '+1 year' );
		}

		return $next_year;
	}

	/**
	 * Get current service.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_service_id() {

		return sanitize_key( wp_unslash( $this->filters['service_id'] ) );
	}

	/**
	 * Get current url.
	 *
	 * @since 1.0.0
	 *
	 * @param array $filters Query filters.
	 *
	 * @return string
	 */
	public function get_url( $filters = [] ) {

		$filters = wp_parse_args( $filters, $this->get_applied_filters() );
		$filters = array_filter( $filters, fn( $filter ) => $filter !== '' );

		return add_query_arg( $filters, $this->base_url );
	}

	/**
	 * Get date URL.
	 *
	 * @since 1.0.0
	 *
	 * @param DateTimeImmutable $date Date.
	 *
	 * @return string
	 */
	public function get_url_with_date( $date ) {

		return $this->get_url(
			[
				'year'  => $date->format( 'Y' ),
				'month' => $date->format( 'n' ),
				'day'   => $date->format( 'j' ),
			]
		);
	}

	/**
	 * Get the first week day before a given date.
	 *
	 * @since 1.0.0
	 *
	 * @param DateTimeImmutable $date Date.
	 *
	 * @return DateTimeImmutable
	 */
	private function get_first_day_of_week_before( $date ) {

		$first_day_of_week   = Plugin::get( Options::class )->get( 'display.start_of_week' );
		$current_day_of_week = (int) $date->format( 'w' );
		$days_to_subtract    = ( $current_day_of_week - $first_day_of_week + 7 ) % 7;

		if ( $days_to_subtract > 0 ) {
			$date = $date->modify( "-$days_to_subtract days" );
		}

		return $date;
	}

	/**
	 * Get the last week day after a given date.
	 *
	 * @since 1.0.0
	 *
	 * @param DateTimeImmutable $date Date.
	 *
	 * @return DateTimeImmutable
	 */
	private function get_last_day_of_week_after( $date ) {

		$first_day_of_week     = (int) Plugin::get( Options::class )->get( 'display.start_of_week' );
		$month_end_day_of_week = (int) $date->format( 'w' );
		$last_day_of_week      = ( $first_day_of_week - 1 + 7 ) % 7;
		$days_to_add           = ( $last_day_of_week - $month_end_day_of_week + 7 ) % 7;

		if ( $days_to_add > 0 ) {
			$date = $date->modify( "+$days_to_add days" );
		}

		return $date;
	}

	/**
	 * Get current view mode start date.
	 *
	 * @since 1.0.0
	 * @since 1.3.1 Added support for agenda view.
	 *
	 * @return DateTimeImmutable
	 */
	public function get_start_date() {

		$mode = $this->filters['mode'];

		switch ( $mode ) {
			case 'day':
			case 'agenda':
				$start_date = $this->get_date();
				break;

			case 'week':
				$date       = $this->get_date();
				$start_date = $this->get_first_day_of_week_before( $date );
				break;

			case 'month':
			default:
				$start_date = $this->get_first_day_of_week_before(
					$this->get_date()->modify( 'first day of this month' )
				);
				break;
		}

		return $start_date->setTime( 0, 0 );
	}

	/**
	 * Get current view mode end date.
	 *
	 * @since 1.0.0
	 * @since 1.3.1 Added support for agenda view.
	 *
	 * @return DateTimeImmutable
	 */
	public function get_end_date() {

		$mode = $this->filters['mode'];

		switch ( $mode ) {
			case 'day':
				$end_date = $this->get_date();
				break;

			case 'week':
				$date     = $this->get_date();
				$end_date = $this->get_last_day_of_week_after( $date );
				break;

			case 'agenda':
				$end_date = $this->get_date()->modify( '+4 days' );
				break;

			case 'month':
			default:
				$end_date = $this->get_last_day_of_week_after(
					$this->get_date()->modify( 'last day of this month' )
				);
				break;
		}

		return $end_date->setTime( 23, 59, 59 );
	}

	/**
	 * Get status counts.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public function get_status_counts() {

		$filters = $this->get_applied_filters();

		unset( $filters['status'] );

		$query = ( new QueryBuilder() )
			->table( 'appointments' )
			->select( [ 'appointments.status', 'COUNT(DISTINCT id)' ] )
			->group_by( [ 'appointments.status' ] );

		$query = $this->build_query( $query, $filters );

		$counts     = $query->get();
		$counts     = array_combine( array_column( $counts, 'status' ), array_column( $counts, 'count' ) );
		$counts[''] = array_sum( $counts );

		return $counts;
	}

	/**
	 * Turn parameters into a database query.
	 *
	 * @since 1.0.0
	 *
	 * @param QueryBuilder $query   Current query.
	 * @param array        $filters Filters.
	 *
	 * @return QueryBuilder
	 */
	protected function build_query( $query, $filters ) {

		$start_date = Date::to_utc( $this->get_start_date() )->format( Date::DB_DATETIME_FORMAT );
		$end_date   = Date::to_utc( $this->get_end_date() )->format( Date::DB_DATETIME_FORMAT );

		// Wrap date range in a single grouped condition so all other filters are ANDed to it.
		// Use overlap logic: appointment overlaps period if start_at < period_end AND end_at > period_start
		// This catches all cases: appointments starting/ending within period, and appointments spanning the period.
		$query = $query->where(
			function ( $q ) use ( $start_date, $end_date ) {

				$q->where( 'appointments.start_at', '<', $end_date, '%s' )
					->where( 'appointments.end_at', '>', $start_date, '%s' );
			}
		);

		if ( ! empty( $filters['status'] ) ) {
			$query = $query->where( 'appointments.status', '=', $filters['status'], '%s' );
		}

		if ( isset( $filters['service_id'] ) && $filters['service_id'] !== '' ) {
			$query = $query->where( 'appointments.service_id', '=', $filters['service_id'], '%s' );
		}

		if ( ! empty( $filters['search'] ) ) {
			$search_term = $filters['search'];

			$query
				->join( 'services', 'appointments.service_id', '=', 'services.id' )
				->join( 'customers', 'appointments.customer_id', '=', 'customers.id' )
				->join( 'locations', 'appointments.location_id', '=', 'locations.id' )
				->where(
					function ( $q ) use ( $search_term ) {

						$q->where( 'appointments.id', 'like', "%$search_term%", '%s' )
							->or_where( 'services.name', 'like', "%$search_term%", '%s' )
							->or_where( 'customers.name', 'like', "%$search_term%", '%s' )
							->or_where( 'customers.email', 'like', "%$search_term%", '%s' )
							->or_where( 'customers.phone', 'like', "%$search_term%", '%s' )
							->or_where( 'locations.name', 'like', "%$search_term%", '%s' );
					}
				);
		}

		return $query->order_by( 'start_at' );
	}

	/**
	 * Get entries.
	 *
	 * @since 1.0.0
	 * @since 1.3.0 Added $relationships parameter.
	 *
	 * @param array $relationships Relationships to eager load.
	 *
	 * @return array
	 */
	public function get_appointments( $relationships = [] ) {

		static $entries = null;

		if ( is_null( $entries ) ) {

			$filters = $this->get_applied_filters();
			$query   = Plugin::get( AppointmentModel::class )::query()->with( $relationships );
			$query   = $this->build_query( $query, $filters );

			$entries = $query->get();
		}

		return $entries;
	}
}
