<?php
/**
 * BitForgeNet Guest Tickets - Front-end assets and signed file endpoints.
 *
 * @package BitForgeNet_Guest_Tickets
 * @since   1.0.0
 */

defined( 'ABSPATH' ) || exit;

// This file talks to the plugin's own tables. WordPress has no API for them --
// they are not posts and not meta -- so $wpdb is the only way in. The rows
// change on every booking and every scan, which makes an object cache wrong
// more often than it is right. Table names are built from $wpdb->prefix and
// never from a request, so interpolating one carries nothing to inject; every
// value goes through $wpdb->prepare(), and the few identifiers that cannot
// (ORDER BY columns) are matched against a whitelist first.
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable WordPress.DB.DirectDatabaseQuery.SchemaChange
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter


/**
 * Registers public assets and serves protected files.
 */
class BFNET_Frontend {

	/**
	 * Hook up.
	 */
	public function __construct() {
		add_action( 'wp_enqueue_scripts', array( $this, 'register_assets' ) );
		add_action( 'init', array( $this, 'serve_signed_files' ), 20 );

		// Quoting a discount goes through admin-ajax rather than the REST API:
		// the REST route is the first thing a security plugin switches off, and
		// a code that silently never applies is worse than no code at all.
		add_action( 'wp_ajax_bfnet_quote', array( $this, 'quote_discount' ) );
		add_action( 'wp_ajax_nopriv_bfnet_quote', array( $this, 'quote_discount' ) );
	}

	/**
	 * Register (not enqueue) the public assets; shortcodes enqueue on demand.
	 *
	 * @return void
	 */
	/**
	 * Price a discount code without committing to anything.
	 *
	 * @return void
	 */
	public function quote_discount() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$event_id = isset( $_REQUEST['event'] ) ? (int) $_REQUEST['event'] : 0;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$code = isset( $_REQUEST['code'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['code'] ) ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$subtotal = isset( $_REQUEST['subtotal'] ) ? max( 0, (int) $_REQUEST['subtotal'] ) : 0;
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$quantity = isset( $_REQUEST['quantity'] ) ? max( 1, (int) $_REQUEST['quantity'] ) : 1;

		if ( ! BFNET_Security::rate_limit( 'promo', 60, HOUR_IN_SECONDS ) ) {
			wp_send_json( array( 'ok' => false, 'message' => __( 'Too many attempts. Try again shortly.', 'bitforgenet-guest-tickets' ) ) );
		}

		$event = BFNET_Event::get( $event_id );
		if ( ! $event ) {
			wp_send_json( array( 'ok' => false, 'message' => __( 'This event is no longer available.', 'bitforgenet-guest-tickets' ) ) );
		}

		$applied = BFNET_Offers::apply( $event_id, $code, $subtotal, $quantity );

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

		// Nothing is listening, so codes are not a thing on this build. Say that
		// plainly rather than reporting a reduction of nothing.
		if ( ! is_array( $applied ) ) {
			wp_send_json( array( 'ok' => false, 'message' => __( 'This event does not take discount codes.', 'bitforgenet-guest-tickets' ) ) );
		}

		wp_send_json(
			array(
				'ok'       => true,
				'label'    => $applied['label'],
				'amount'   => (int) $applied['amount_cents'],
				'off'      => BFNET_Util::money( (int) $applied['amount_cents'], $event->currency ),
				'total'    => BFNET_Util::money( max( 0, $subtotal - (int) $applied['amount_cents'] ), $event->currency ),
				'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 register_assets() {
		wp_register_style( 'bitforgenet-guest-tickets-public', BFNET_URL . 'public/assets/bitforgenet-guest-tickets.css', array(), BFNET_Util::asset_version( 'public/assets/bitforgenet-guest-tickets.css' ) );
		wp_register_script( 'bitforgenet-guest-tickets-public', BFNET_URL . 'public/assets/bitforgenet-guest-tickets.js', array(), BFNET_Util::asset_version( 'public/assets/bitforgenet-guest-tickets.js' ), true );

		$currencies = BFNET_Util::currencies();
		$currency   = (string) BFNET_Settings::get( 'currency', 'CAD' );

		wp_localize_script(
			'bitforgenet-guest-tickets-public',
			'BFNetConfig',
			array(
				'symbol'            => isset( $currencies[ $currency ] ) ? $currencies[ $currency ]['symbol'] : $currency,
				'decimals'          => (int) BFNET_Settings::get( 'decimals', 2 ),
				'decimalSeparator'  => (string) BFNET_Settings::get( 'decimal_separator', '.' ),
				'thousandSeparator' => (string) BFNET_Settings::get( 'thousand_separator', ' ' ),
				'position'          => (string) BFNET_Settings::get( 'currency_position', 'suffix_space' ),
				'quoteUrl'          => BFNET_Door::quote_url(),
				'ajaxUrl'           => admin_url( 'admin-ajax.php' ),
			)
		);

		$primary = (string) BFNET_Settings::get( 'brand_primary', '#C9A227' );
		$dark    = (string) BFNET_Settings::get( 'brand_secondary', '#0B0B0B' );

		wp_add_inline_style( 'bitforgenet-guest-tickets-public', sprintf( '.bfnet{--bfnet-primary:%s;--bfnet-dark:%s;}', esc_attr( $primary ), esc_attr( $dark ) ) );
	}

	/**
	 * Serve a ticket PDF behind a signed, expiring URL.
	 *
	 * The files live outside the web root's reach (denied by .htaccess), so this is
	 * the only way to read them and every request must carry a valid HMAC.
	 *
	 * @return void
	 */
	public function serve_signed_files() {
		if ( empty( $_GET['bfnet_download'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			return;
		}

		$uuid      = sanitize_text_field( wp_unslash( $_GET['bfnet_download'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
		$expires   = isset( $_GET['expires'] ) ? (int) $_GET['expires'] : 0; // phpcs:ignore WordPress.Security.NonceVerification
		$signature = isset( $_GET['signature'] ) ? sanitize_text_field( wp_unslash( $_GET['signature'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification

		if ( ! BFNET_Security::verify_signature( 'ticket:' . $uuid, $expires, $signature ) ) {
			BFNET_Log::security( 'bad_download_signature', array( 'uuid' => substr( $uuid, 0, 8 ) ) );
			wp_die( esc_html__( 'This download link has expired. Please ask the organizer to resend your ticket.', 'bitforgenet-guest-tickets' ), esc_html__( 'Link expired', 'bitforgenet-guest-tickets' ), array( 'response' => 403 ) );
		}

		global $wpdb;
		$table  = BFNET_Ticket::table();
		$ticket = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE uuid = %s", $uuid ) ); // phpcs:ignore WordPress.DB.PreparedSQL

		if ( ! $ticket || empty( $ticket->pdf_file ) ) {
			wp_die( esc_html__( 'Ticket not found.', 'bitforgenet-guest-tickets' ), esc_html__( 'Not found', 'bitforgenet-guest-tickets' ), array( 'response' => 404 ) );
		}

		$storage = BFNET_Install::storage();
		$path    = $storage['dir'] . '/tickets/' . basename( $ticket->pdf_file );

		if ( ! is_readable( $path ) ) {
			wp_die( esc_html__( 'This ticket file is no longer available.', 'bitforgenet-guest-tickets' ), esc_html__( 'Not found', 'bitforgenet-guest-tickets' ), array( 'response' => 404 ) );
		}

		nocache_headers();
		header( 'Content-Type: application/pdf' );
		header( 'Content-Disposition: inline; filename="' . sanitize_file_name( $ticket->code ) . '.pdf"' );
		header( 'Content-Length: ' . filesize( $path ) );
		header( 'X-Content-Type-Options: nosniff' );

		readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		exit;
	}
}
