<?php
/**
 * BitForgeNet Guest Tickets - Public shortcodes and registration handling.
 *
 * @package BitForgeNet_Guest_Tickets
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Registers the public shortcodes and processes registration submissions.
 */
class BFNET_Shortcodes {

	/**
	 * Result of a submission handled this request.
	 *
	 * @var array|null
	 */
	private static $result = null;

	/**
	 * The outcome of pricing a discount code, when one was submitted.
	 *
	 * @var array|null
	 */
	public static $applied = null;

	/**
	 * Hook up.
	 */
	public function __construct() {
		add_shortcode( 'bitforgenet_registration', array( $this, 'render_registration' ) );
		add_shortcode( 'bitforgenet_events', array( $this, 'render_events' ) );
		add_shortcode( 'bitforgenet_event', array( $this, 'render_single' ) );

		add_action( 'template_redirect', array( $this, 'maybe_handle_submission' ), 5 );
	}

	/* --------------------------------------------------------------------- */
	/* Submission handling                                                   */
	/* --------------------------------------------------------------------- */

	/**
	 * Process a posted registration form.
	 *
	 * @return void
	 */
	/**
	 * Price a discount code against the basket the guest has built so far.
	 *
	 * @param object $event Event row.
	 * @param array  $post  The submitted form.
	 * @return array
	 */
	public static function price_code( $event, array $post ) {
		$code = isset( $post['bfnet_discount_code'] ) ? sanitize_text_field( wp_unslash( $post['bfnet_discount_code'] ) ) : '';
		$code = BFNET_Offers::normalise( $code );

		if ( '' === $code ) {
			return array( 'code' => '', 'ok' => false, 'message' => '' );
		}

		$quantity = isset( $post['bfnet_quantity'] ) ? max( 1, min( (int) $event->max_per_order, (int) $post['bfnet_quantity'] ) ) : 1;

		// Price the basket exactly as the order would, so the figure shown here
		// and the figure charged cannot disagree.
		$raw      = isset( $post['bfnet_guest'] ) && is_array( $post['bfnet_guest'] ) ? wp_unslash( $post['bfnet_guest'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		$subtotal = 0;

		for ( $i = 0; $i < $quantity; $i++ ) {
			$type_id   = isset( $raw[ $i ]['ticket_type_id'] ) ? (int) $raw[ $i ]['ticket_type_id'] : 0;
			$type      = BFNET_Offers::tier( $type_id );
			$subtotal += (int) BFNET_Offers::price( $event, $type );
		}

		$applied = BFNET_Offers::apply( (int) $event->id, $code, $subtotal, $quantity );

		if ( is_wp_error( $applied ) ) {
			return array(
				'code'    => $code,
				'ok'      => false,
				'message' => $applied->get_error_message(),
			);
		}

		if ( ! is_array( $applied ) ) {
			return array(
				'code'    => $code,
				'ok'      => false,
				'message' => __( 'This event does not take discount codes.', 'bitforgenet-guest-tickets' ),
			);
		}

		return array(
			'code'         => $code,
			'ok'           => true,
			'amount_cents' => (int) $applied['amount_cents'],
			'subtotal'     => $subtotal,
			'label'        => $applied['label'],
			'message'      => sprintf(
				/* translators: %s: money amount taken off. */
				__( '%s off applied.', 'bitforgenet-guest-tickets' ),
				BFNET_Util::money( (int) $applied['amount_cents'], $event->currency )
			),
		);
	}

	public function maybe_handle_submission() {
		if ( 'POST' !== ( isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '' ) ) {
			return;
		}
		if ( empty( $_POST['bfnet_action'] ) || 'register' !== sanitize_key( wp_unslash( $_POST['bfnet_action'] ) ) ) {
			return;
		}

		$GLOBALS['bfnet_is_plugin_route'] = true;

		$nonce = isset( $_POST['bfnet_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['bfnet_nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'bfnet_register' ) ) {
			self::$result = array( 'error' => __( 'Your session expired. Please reload the page and try again.', 'bitforgenet-guest-tickets' ) );
			return;
		}

		// "Apply" is a real submission, answered by the server.
		//
		// It used to be a JavaScript call, which made the feature only as
		// reliable as the freshest copy of a script file — a cached asset, a
		// combiner, a CDN, and a perfectly good code reports itself broken. Now
		// the button posts the form, the code is priced here, and the page comes
		// back with the answer on it. The script that does the same without a
		// round trip is a convenience layered on top; nothing depends on it.
		//
		// Checked before the traps below: pricing a code is not a booking
		// attempt, and trying three codes should not spend the hour's quota.
		// phpcs:ignore WordPress.Security.NonceVerification
		if ( ! empty( $_POST['bfnet_apply_code'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification
			$bfnet_event = BFNET_Event::get( isset( $_POST['bfnet_event_id'] ) ? (int) $_POST['bfnet_event_id'] : 0 );

			if ( $bfnet_event && BFNET_Security::rate_limit( 'promo', 40, HOUR_IN_SECONDS ) ) {
				self::$applied = self::price_code( $bfnet_event, $_POST ); // phpcs:ignore WordPress.Security.NonceVerification
			}

			return;
		}

		// Honeypot: a real browser leaves this empty.
		if ( (int) BFNET_Settings::get( 'enable_honeypot', 1 ) ) {
			$trap = isset( $_POST['bfnet_website'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['bfnet_website'] ) ) ) : '';
			if ( '' !== $trap ) {
				BFNET_Log::security( 'honeypot_triggered', array() );
				self::$result = array( 'error' => __( 'Your submission could not be processed.', 'bitforgenet-guest-tickets' ) );
				return;
			}
		}

		// Time trap: forms completed impossibly fast are bots.
		$min_seconds = (int) BFNET_Settings::get( 'min_form_seconds', 3 );
		$started     = isset( $_POST['bfnet_started'] ) ? (int) $_POST['bfnet_started'] : 0;
		if ( $min_seconds > 0 && $started > 0 && ( time() - $started ) < $min_seconds ) {
			BFNET_Log::security( 'form_too_fast', array( 'elapsed' => time() - $started ) );
			self::$result = array( 'error' => __( 'Please take a moment to review your details before submitting.', 'bitforgenet-guest-tickets' ) );
			return;
		}

		// Rate limit by IP.
		if ( ! BFNET_Security::rate_limit( 'register', (int) BFNET_Settings::get( 'rate_limit_per_hour', 8 ), HOUR_IN_SECONDS ) ) {
			BFNET_Log::security( 'rate_limited', array( 'bucket' => 'register' ) );
			self::$result = array( 'error' => __( 'Too many reservations from this connection. Please try again later or contact us on WhatsApp.', 'bitforgenet-guest-tickets' ) );
			return;
		}

		$event_id = isset( $_POST['bfnet_event_id'] ) ? (int) $_POST['bfnet_event_id'] : 0;
		$event    = BFNET_Event::get( $event_id );

		if ( ! $event || ! BFNET_Event::registration_open( $event ) ) {
			self::$result = array( 'error' => $event ? BFNET_Event::closed_reason( $event ) : __( 'This event is not available.', 'bitforgenet-guest-tickets' ) );
			return;
		}


		if ( (int) BFNET_Settings::get( 'require_terms', 1 ) && empty( $_POST['bfnet_terms'] ) ) {
			self::$result = array( 'error' => __( 'Please accept the terms and conditions to continue.', 'bitforgenet-guest-tickets' ) );
			return;
		}

		$guests   = array();
		$raw      = isset( $_POST['bfnet_guest'] ) && is_array( $_POST['bfnet_guest'] ) ? wp_unslash( $_POST['bfnet_guest'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		$quantity = isset( $_POST['bfnet_quantity'] ) ? max( 1, min( (int) $event->max_per_order, (int) $_POST['bfnet_quantity'] ) ) : 1;

		for ( $i = 0; $i < $quantity; $i++ ) {
			$row      = isset( $raw[ $i ] ) && is_array( $raw[ $i ] ) ? $raw[ $i ] : array();
			$guests[] = array(
				'first_name'     => isset( $row['first_name'] ) ? $row['first_name'] : '',
				'last_name'      => isset( $row['last_name'] ) ? $row['last_name'] : '',
				'ticket_type_id' => isset( $row['ticket_type_id'] ) ? (int) $row['ticket_type_id'] : 0,
				'birth_date'     => isset( $row['birth_date'] ) ? $row['birth_date'] : '',
			);
		}

		$order_id = BFNET_Order::create(
			array(
				'event_id'          => $event_id,
				'first_name'        => isset( $_POST['bfnet_first_name'] ) ? wp_unslash( $_POST['bfnet_first_name'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'last_name'         => isset( $_POST['bfnet_last_name'] ) ? wp_unslash( $_POST['bfnet_last_name'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'email'             => isset( $_POST['bfnet_email'] ) ? wp_unslash( $_POST['bfnet_email'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'phone'             => isset( $_POST['bfnet_phone'] ) ? wp_unslash( $_POST['bfnet_phone'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'whatsapp'          => isset( $_POST['bfnet_whatsapp'] ) ? wp_unslash( $_POST['bfnet_whatsapp'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'city'              => isset( $_POST['bfnet_city'] ) ? wp_unslash( $_POST['bfnet_city'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'customer_note'     => isset( $_POST['bfnet_note'] ) ? wp_unslash( $_POST['bfnet_note'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'age_confirmed'     => ! empty( $_POST['bfnet_age_confirm'] ),
				'consent_terms'     => ! empty( $_POST['bfnet_terms'] ),
				'consent_marketing' => ! empty( $_POST['bfnet_marketing'] ),
				'guests'            => $guests,
				// Read straight from the form. Without this the code the guest typed
				// never reached the order at all, which is why a perfectly valid
				// discount showed on screen and then quietly took nothing off.
				'discount_code'     => isset( $_POST['bfnet_discount_code'] ) ? wp_unslash( $_POST['bfnet_discount_code'] ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
				'source'            => 'website',
			)
		);

		if ( is_wp_error( $order_id ) ) {
			self::$result = array( 'error' => $order_id->get_error_message() );
			return;
		}

		BFNET_Mailer::send_pending( $order_id );
		BFNET_Mailer::notify_team( $order_id );

		$order = BFNET_Order::get( $order_id );

		// Post/Redirect/Get so a refresh cannot duplicate the reservation.
		$redirect = add_query_arg(
			array(
				'bfnet_confirmed' => rawurlencode( $order->uuid ),
			),
			remove_query_arg( array( 'bfnet_confirmed', 'bfnet_event' ) )
		);

		wp_safe_redirect( $redirect );
		exit;
	}

	/* --------------------------------------------------------------------- */
	/* Rendering                                                             */
	/* --------------------------------------------------------------------- */

	/**
	 * [bitforgenet_registration] — the main booking surface.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public function render_registration( $atts ) {
		$atts = shortcode_atts(
			array(
				'event'    => '',
				'series'   => '',
				'show_past' => 'no',
			),
			$atts,
			'bitforgenet_registration'
		);

		$GLOBALS['bfnet_is_plugin_route'] = true;
		wp_enqueue_style( 'bitforgenet-guest-tickets-public' );
		wp_enqueue_script( 'bitforgenet-guest-tickets-public' );

		// Confirmation screen after a successful submission.
		if ( ! empty( $_GET['bfnet_confirmed'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			$uuid  = sanitize_text_field( wp_unslash( $_GET['bfnet_confirmed'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
			$order = BFNET_Order::get_by_uuid( $uuid );
			if ( $order ) {
				return $this->view( 'confirmation', array( 'order' => $order, 'event' => BFNET_Event::get( $order->event_id ) ) );
			}
		}

		$event = $this->resolve_event( $atts );

		if ( $event ) {
			return $this->view(
				'form',
				array(
					'event'  => $event,
					'result' => self::$result,
				)
			);
		}

		$events = BFNET_Event::query(
			array(
				'statuses' => array( 'open', 'published', 'sold_out' ),
				'series'   => $atts['series'],
				'upcoming' => 'yes' !== $atts['show_past'],
				'orderby'  => 'starts_at',
				'order'    => 'ASC',
				'limit'    => 30,
			)
		);

		return $this->view( 'chooser', array( 'events' => $events, 'result' => self::$result ) );
	}

	/**
	 * [bitforgenet_events] — a simple listing.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public function render_events( $atts ) {
		$atts = shortcode_atts(
			array(
				'limit'  => 12,
				'series' => '',
				'past'   => 'no',
			),
			$atts,
			'bitforgenet_events'
		);

		wp_enqueue_style( 'bitforgenet-guest-tickets-public' );

		$events = BFNET_Event::query(
			array(
				'statuses' => array( 'open', 'published', 'sold_out', 'closed' ),
				'series'   => $atts['series'],
				'upcoming' => 'yes' !== $atts['past'],
				'past'     => 'yes' === $atts['past'],
				'orderby'  => 'starts_at',
				'order'    => 'yes' === $atts['past'] ? 'DESC' : 'ASC',
				'limit'    => (int) $atts['limit'],
			)
		);

		return $this->view( 'chooser', array( 'events' => $events, 'result' => null, 'listing_only' => true ) );
	}

	/**
	 * [bitforgenet_event slug="..."] — one event with its form.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return string
	 */
	public function render_single( $atts ) {
		$atts = shortcode_atts( array( 'slug' => '', 'id' => 0 ), $atts, 'bitforgenet_event' );
		return $this->render_registration( array( 'event' => $atts['slug'] ? $atts['slug'] : $atts['id'] ) );
	}

	/**
	 * Work out which event the visitor is looking at.
	 *
	 * @param array $atts Shortcode attributes.
	 * @return object|null
	 */
	private function resolve_event( $atts ) {
		$requested = '';

		if ( ! empty( $_GET['bfnet_event'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			$requested = sanitize_title( wp_unslash( $_GET['bfnet_event'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
		} elseif ( ! empty( $atts['event'] ) ) {
			$requested = $atts['event'];
		}

		if ( $requested ) {
			$event = is_numeric( $requested ) ? BFNET_Event::get( (int) $requested ) : BFNET_Event::get_by_slug( $requested );
			if ( $event && ! in_array( $event->status, array( 'draft', 'archived' ), true ) ) {
				return $event;
			}
			return null;
		}

		// Exactly one event open for registration: go straight to its form.
		$open = BFNET_Event::query(
			array(
				'status'   => 'open',
				'series'   => isset( $atts['series'] ) ? $atts['series'] : '',
				'upcoming' => true,
				'limit'    => 2,
			)
		);

		return ( 1 === count( $open ) ) ? $open[0] : null;
	}

	/**
	 * Load a public view file and return its markup.
	 *
	 * @param string $name Template name.
	 * @param array  $data Variables exposed to the template.
	 * @return string
	 */
	private function view( $name, array $data ) {
		$file = BFNET_PATH . 'public/views/' . sanitize_file_name( $name ) . '.php';

		$override = locate_template( array( 'bitforgenet-guest-tickets/' . sanitize_file_name( $name ) . '.php' ) );
		if ( $override ) {
			$file = $override;
		}

		if ( ! is_readable( $file ) ) {
			return '';
		}

		// A view reads $event and $order, not $data['event'] and $data['order'],
		// because a template littered with array lookups is harder to read than the
		// screen it draws. The array never contains request data: every caller in
		// this class builds it from rows it has already fetched, and EXTR_SKIP means
		// nothing here can overwrite a variable that already exists.
		extract( $data, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract

		ob_start();
		include $file;
		return (string) ob_get_clean();
	}
}
