<?php
/**
 * Front-end behaviour for the DrezSync Booking field.
 *
 * Enqueues the booking script on forms that use the field, and answers the
 * availability request that fills the time list from the calendar.
 *
 * @package DrezSync
 */

defined( 'ABSPATH' ) || exit;

/**
 * Loads the front-end script and handles the availability AJAX request.
 */
class DrezSync_Field_Frontend {

	/**
	 * Google Calendar connection.
	 *
	 * @var DrezSync_Connection
	 */
	private $connection;

	/**
	 * Constructor.
	 *
	 * @param DrezSync_Connection $connection Connection service.
	 */
	public function __construct( DrezSync_Connection $connection ) {
		$this->connection = $connection;
		add_action( 'gform_enqueue_scripts', array( $this, 'enqueue' ), 10, 2 );
		add_action( 'wp_ajax_drezsync_availability', array( $this, 'ajax_availability' ) );
		add_action( 'wp_ajax_nopriv_drezsync_availability', array( $this, 'ajax_availability' ) );
		add_action( 'wp_ajax_drezsync_month', array( $this, 'ajax_month' ) );
		add_action( 'wp_ajax_nopriv_drezsync_month', array( $this, 'ajax_month' ) );
		add_action( 'gform_after_submission', array( $this, 'create_events' ), 10, 2 );
	}


	/**
	 * Enqueue the booking script on forms that use the field.
	 *
	 * @param array $form    Form object.
	 * @param bool  $is_ajax Whether the form is being rendered via AJAX.
	 */
	public function enqueue( $form, $is_ajax ) {
		if ( ! $this->form_has_field( $form ) ) {
			return;
		}

		$js     = DREZSYNC_DIR . 'assets/js/booking.js';
		$js_ver = file_exists( $js ) ? filemtime( $js ) : DREZSYNC_VERSION;

		wp_enqueue_script( 'drezsync-booking', DREZSYNC_URL . 'assets/js/booking.js', array( 'jquery' ), $js_ver, true );
		wp_localize_script(
			'drezsync-booking',
			'drezsyncBooking',
			array(
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'drezsync_availability' ),
				'formId'  => absint( $form['id'] ),
				'loading'  => esc_html__( 'Loading times...', 'drezsync' ),
				'none'     => esc_html__( 'No times available for that date.', 'drezsync' ),
				'pickDate' => esc_html__( 'Pick a date to see available times.', 'drezsync' ),
				'months'   => array(
					__( 'January', 'drezsync' ),
					__( 'February', 'drezsync' ),
					__( 'March', 'drezsync' ),
					__( 'April', 'drezsync' ),
					__( 'May', 'drezsync' ),
					__( 'June', 'drezsync' ),
					__( 'July', 'drezsync' ),
					__( 'August', 'drezsync' ),
					__( 'September', 'drezsync' ),
					__( 'October', 'drezsync' ),
					__( 'November', 'drezsync' ),
					__( 'December', 'drezsync' ),
				),
				'weekdays' => array(
					__( 'Su', 'drezsync' ),
					__( 'Mo', 'drezsync' ),
					__( 'Tu', 'drezsync' ),
					__( 'We', 'drezsync' ),
					__( 'Th', 'drezsync' ),
					__( 'Fr', 'drezsync' ),
					__( 'Sa', 'drezsync' ),
				),
				'prevMonth' => esc_html__( 'Previous month', 'drezsync' ),
				'nextMonth' => esc_html__( 'Next month', 'drezsync' ),
				'calLoading' => esc_html__( 'Loading calendar...', 'drezsync' ),
				'pickSource'    => esc_html__( 'Choose an option above first to see availability.', 'drezsync' ),
			)
		);
	}

	/**
	 * Does the form contain a DrezSync Booking field?
	 *
	 * @param array $form Form object.
	 * @return bool
	 */
	private function form_has_field( $form ) {
		if ( empty( $form['fields'] ) ) {
			return false;
		}
		foreach ( $form['fields'] as $field ) {
			if ( 'drezsync_booking' === $field->type ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Light per-visitor rate limit on the public availability endpoints.
	 *
	 * Generous enough that a real person clicking dates never hits it, but it
	 * stops a script from enumerating dates to exhaust the Google API quota.
	 */
	private function throttle() {
		/**
		 * Max availability requests per minute per IP. Filterable so a busy site,
		 * or one behind a shared proxy, can raise it, or set 0 to disable.
		 *
		 * @param int $max Requests allowed per minute.
		 */
		$max = (int) apply_filters( 'drezsync_availability_rate_limit', 120 );
		if ( $max <= 0 ) {
			return;
		}
		$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		if ( '' === $ip ) {
			return;
		}
		$key   = 'drezsync_rl_' . md5( $ip );
		$count = (int) get_transient( $key );
		if ( $count >= $max ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Too many requests. Please slow down and try again.', 'drezsync' ) ), 429 );
		}
		set_transient( $key, $count + 1, MINUTE_IN_SECONDS );
	}

	/**
	 * Answer the availability request with the open times for a date.
	 */
	public function ajax_availability() {
		check_ajax_referer( 'drezsync_availability', 'nonce' );
		$this->throttle();

		$form_id  = isset( $_POST['formId'] ) ? absint( $_POST['formId'] ) : 0;
		$field_id = isset( $_POST['fieldId'] ) ? absint( $_POST['fieldId'] ) : 0;
		$date     = isset( $_POST['date'] ) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : '';

		if ( ! $form_id || ! $field_id || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) || ! class_exists( 'GFAPI' ) ) {
			wp_send_json_error();
		}

		$form = GFAPI::get_form( $form_id );
		if ( ! $form ) {
			wp_send_json_error();
		}

		$field = null;
		foreach ( $form['fields'] as $f ) {
			if ( (int) $f->id === $field_id && 'drezsync_booking' === $f->type ) {
				$field = $f;
				break;
			}
		}
		if ( ! $field ) {
			wp_send_json_error();
		}

		$cal_value = isset( $_POST['calValue'] ) ? sanitize_text_field( wp_unslash( $_POST['calValue'] ) ) : '';
		$calendar  = DrezSync_Booking_Field::resolve_calendar( $field, $cal_value );
		if ( '' === $calendar ) {
			wp_send_json_success( array( 'times' => array() ) );
		}

		$rules = array(
			'days'     => isset( $field->drezsyncDays ) ? $field->drezsyncDays : array( 1, 2, 3, 4, 5 ),
			'start'    => isset( $field->drezsyncStart ) ? $field->drezsyncStart : '09:00',
			'end'      => isset( $field->drezsyncEnd ) ? $field->drezsyncEnd : '17:00',
			'slot'     => isset( $field->drezsyncSlot ) ? $field->drezsyncSlot : 30,
			'capacity' => (int) apply_filters( 'drezsync_slot_capacity', 1, $field ),
		);

		// Cache the open times briefly so repeated clicks are instant and do not
		// hammer the Google API. The submit-time re-check runs uncached, so this
		// short cache never allows a double booking, it only speeds the display.
		$cache_key = 'drezsync_av_' . md5( $calendar . '|' . $date . '|' . wp_json_encode( $rules ) );
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			wp_send_json_success( array( 'times' => $cached ) );
		}

		require_once DREZSYNC_DIR . 'includes/booking/class-availability.php';

		$times = array();
		try {
			$availability = new DrezSync_Availability( $this->connection );
			$times        = $availability->get_times( $calendar, $date, $rules );
		} catch ( \Throwable $t ) {
			$times = array();
		}

		set_transient( $cache_key, $times, MINUTE_IN_SECONDS );
		wp_send_json_success( array( 'times' => $times ) );
	}

	/**
	 * Answer which days in a month are available, for greying the calendar.
	 *
	 * Slot mode returns the days that have at least one open time. Range mode (Pro)
	 * returns the days whose night is open, from which the front end composes valid
	 * stays. One calendar fetch per month, cached briefly like the day query.
	 */
	public function ajax_month() {
		check_ajax_referer( 'drezsync_availability', 'nonce' );
		$this->throttle();

		$form_id  = isset( $_POST['formId'] ) ? absint( $_POST['formId'] ) : 0;
		$field_id = isset( $_POST['fieldId'] ) ? absint( $_POST['fieldId'] ) : 0;
		$year     = isset( $_POST['year'] ) ? absint( $_POST['year'] ) : 0;
		$month    = isset( $_POST['month'] ) ? absint( $_POST['month'] ) : 0;
		$mode     = ( isset( $_POST['mode'] ) && 'range' === $_POST['mode'] ) ? 'range' : 'slot';

		if ( ! $form_id || ! $field_id || $year < 2000 || $year > 2100 || $month < 1 || $month > 12 || ! class_exists( 'GFAPI' ) ) {
			wp_send_json_error();
		}

		$form = GFAPI::get_form( $form_id );
		if ( ! $form ) {
			wp_send_json_error();
		}

		$field = null;
		foreach ( $form['fields'] as $f ) {
			if ( (int) $f->id === $field_id && 'drezsync_booking' === $f->type ) {
				$field = $f;
				break;
			}
		}
		if ( ! $field ) {
			wp_send_json_error();
		}

		$cal_value = isset( $_POST['calValue'] ) ? sanitize_text_field( wp_unslash( $_POST['calValue'] ) ) : '';
		$calendar  = DrezSync_Booking_Field::resolve_calendar( $field, $cal_value );
		if ( '' === $calendar ) {
			wp_send_json_success( array( 'days' => array(), 'cal' => '' ) );
		}

		// A short, non-reversible identity for the resolved calendar. The front end
		// uses it to tell whether two booking fields point at the SAME calendar, so
		// overlap protection only blocks fields that share one. The real calendar id
		// never leaves the server.
		$cal_key = substr( md5( $calendar ), 0, 12 );

		$rules = array(
			'days'     => isset( $field->drezsyncDays ) ? $field->drezsyncDays : array( 1, 2, 3, 4, 5 ),
			'start'    => isset( $field->drezsyncStart ) ? $field->drezsyncStart : '09:00',
			'end'      => isset( $field->drezsyncEnd ) ? $field->drezsyncEnd : '17:00',
			'slot'     => isset( $field->drezsyncSlot ) ? $field->drezsyncSlot : 30,
			'capacity' => (int) apply_filters( 'drezsync_slot_capacity', 1, $field ),
		);

		$cache_key = 'drezsync_mo_' . md5( $calendar . '|' . $year . '|' . $month . '|' . $mode . '|' . wp_json_encode( $rules ) );
		$cached    = get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			wp_send_json_success( array( 'days' => $cached, 'cal' => $cal_key ) );
		}

		require_once DREZSYNC_DIR . 'includes/booking/class-availability.php';

		$want_nights = false;

		$days = array();
		try {
			$availability = new DrezSync_Availability( $this->connection );
			if ( $want_nights ) {
			} else {
				$days = $availability->available_days( $calendar, $year, $month, $rules );
			}
		} catch ( \Throwable $t ) {
			$days = array();
		}

		set_transient( $cache_key, $days, MINUTE_IN_SECONDS );
		wp_send_json_success( array( 'days' => $days, 'cal' => $cal_key ) );
	}


	/**
	 * After a successful submission, write each booking to its calendar.
	 *
	 * @param array $entry Entry object.
	 * @param array $form  Form object.
	 */
	public function create_events( $entry, $form ) {
		$client = $this->connection->get_client();
		if ( ! $client || ! class_exists( '\Google\Service\Calendar' ) ) {
			return;
		}

		$service = new \Google\Service\Calendar( $client );
		$tz      = wp_timezone();

		foreach ( $form['fields'] as $field ) {
			if ( 'drezsync_booking' !== $field->type ) {
				continue;
			}

			$value = rgar( $entry, (string) $field->id );
			if ( empty( $value ) ) {
				continue;
			}

			$source_value = '';
			$calendar = DrezSync_Booking_Field::resolve_calendar( $field, $source_value );
			if ( '' === $calendar ) {
				continue;
			}

			$title = $form['title'];

			$event = null;


			if ( ! $event && preg_match( '/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})$/', $value, $m ) ) {
				$slot = max( 5, (int) ( isset( $field->drezsyncSlot ) ? $field->drezsyncSlot : 30 ) );

				try {
					$start = new DateTime( $m[1] . ' ' . $m[2], $tz );
				} catch ( \Exception $e ) {
					continue;
				}
				$end = clone $start;
				$end->modify( '+' . $slot . ' minutes' );

				$event = new \Google\Service\Calendar\Event(
					array(
						'summary'     => $title,
						'description' => $this->build_description( $form, $entry ),
						'start'       => array(
							'dateTime' => $start->format( 'c' ),
							'timeZone' => $tz->getName(),
						),
						'end'         => array(
							'dateTime' => $end->format( 'c' ),
							'timeZone' => $tz->getName(),
						),
					)
				);
			}

			if ( ! $event ) {
				continue;
			}

			$opt = array();

			try {
				$service->events->insert( $calendar, $event, $opt );
			} catch ( \Exception $e ) {
				// The booking is saved either way; the calendar write is best-effort.
				continue;
			}
		}
	}



	/**
	 * Build the event description from the submitted field values.
	 *
	 * @param array $form  Form object.
	 * @param array $entry Entry object.
	 * @return string
	 */
	private function build_description( $form, $entry ) {
		$lines = array();
		foreach ( $form['fields'] as $field ) {
			if ( in_array( $field->type, array( 'drezsync_booking', 'html', 'section', 'page', 'captcha' ), true ) ) {
				continue;
			}
			$value   = GFFormsModel::get_lead_field_value( $entry, $field );
			$display = $field->get_value_entry_detail( $value, rgar( $entry, 'currency' ), false, 'text' );
			if ( '' !== trim( (string) $display ) ) {
				$lines[] = GFCommon::get_label( $field ) . ': ' . $display;
			}
		}
		return implode( "\n", $lines );
	}
}
