<?php
/**
 * Fast Events personalisation functions
 *
 * @since 3.0.0
 */

declare( strict_types=1 );
namespace FeData\FastEvents\Helpers;

defined( 'ABSPATH' ) || exit;

use FeData\FastEvents\Logging\LoggerFactory;
use FeData\FastEvents\Utilities\PluginUtil;
use stdClass;

/**
 * A class with personalization functions
 */
final class PersonalisationHelper {

	/**
	 * PersonalisationHelper constructor.
	 *
	 * @since 3.0.0
	 *
	 * @param LoggerFactory $logger Logger.
	 * @param SeatsHelper   $seats  SeatsHelper.
	 */
	public function __construct( private readonly LoggerFactory $logger, private readonly SeatsHelper $seats ) {
	}

	/**
	 * Create an array with personalisation and layout configuration.
	 * Example: array['ticketname' = array(
	 *     'personalise' => 'Place, Year'
	 *     'layout' => 'Order, Place'
	 * )
	 *
	 * Personalised fields come from the input field names that can be personalised.
	 *
	 * Layout fields come from the input field names that can be personalised and
	 * optionally added with 'Order-id', 'Name' and 'Email';
	 *
	 * @param string $event_input Ticket and field definition (JSON).
	 *
	 * @return array Array with personalization and layout data
	 * @since 2.1.0
	 */
	public function get_ticket_info( string $event_input ): array {
		$personalise = array();
		$layout      = array();

		$inp = json_decode( $event_input );
		if ( ! empty( $inp->tickets ) ) {
			foreach ( $inp->tickets as $ticket ) {
				if ( empty( $ticket->personalise ) ) {
					$personalise[ $ticket->name ] = '';
				} else {
					$personalise[ $ticket->name ] = $ticket->personalise;
				}
				if ( empty( $ticket->layout ) ) {
					$layout[ $ticket->name ] = '';
				} else {
					$layout[ $ticket->name ] = $ticket->layout;
				}
			}
		}
		return array( $personalise, $layout );
	}


	/**
	 * Generate a download SHA1-hash based on 1 or more qrcode ids.
	 *
	 * @param string $qrcode_ids 1 or more qrcode ids (eg. 22-34--234).
	 *
	 * @return string SHA1 hash
	 * @since 2.1.0
	 */
	public function get_download_hash( string $qrcode_ids ): string {
		return hash_hmac( 'sha1', 'download' . $qrcode_ids, wp_salt() );
	}


	/**
	 * Validate a download SHA1-hash based on 1 or more qrcode ids.
	 *
	 * @param string $qrcode_ids 1 or more qrcode ids (eg. 22-34--234).
	 * @param string $signature  The SAH1-hash.
	 *
	 * @return bool Is the hash valid?
	 * @since 2.1.0
	 */
	public function validate_download_hash( string $qrcode_ids, string $signature ): bool {
		return hash_equals( hash_hmac( 'sha1', 'download' . $qrcode_ids, wp_salt() ), $signature );
	}


	/**
	 * Generate a read and update SHA1-hash based on 1 qrcode id.
	 * Nonce is generated and on top of this a SHA1-hash.
	 * Using the nonce makes the links only valid for 12-23 hours.
	 * Trailing URL format: "/int/nonce/hash"
	 *
	 * @param string $qrcode_id The unique id of the qrcode.
	 *
	 * @return array Array with nonce and SHA1 hash
	 * @since 2.1.0
	 */
	public function get_update_nonce_hash( string $qrcode_id ): array {
		$nonce = wp_create_nonce( 'update' . $qrcode_id );
		return array( $nonce, hash_hmac( 'sha1', "update$qrcode_id/$nonce", wp_salt() ) );
	}


	/**
	 * Validate the hash and nonce for reading/updating personalised input.
	 *
	 * @param string $qrcode_id The unique id of the qrcode.
	 * @param string $nonce     The nonce of this url.
	 * @param string $signature The SHA1-hash.
	 *
	 * @return bool Are the hash and nonce valid?
	 * @since 2.1.0
	 */
	public function validate_update_nonce_hash( string $qrcode_id, string $nonce, string $signature ): bool {
		if ( hash_equals( hash_hmac( 'sha1', "update$qrcode_id/$nonce", wp_salt() ), $signature ) ) {
			return is_int( wp_verify_nonce( $nonce, 'update' . $qrcode_id ) );
		}
		return false;
	}


	/**
	 * Get the personalised input.
	 *
	 * @param ?string $personalised_input Personalised input (JSON).
	 *
	 * @return stdClass Personalised object
	 * @since 2.1.0
	 */
	public function get_input( ?string $personalised_input ): stdClass {
		$result = json_decode( $personalised_input ?? '' );
		if ( null === $result || false === $result ) {
			$result               = new stdClass();
			$result->personalised = false;
			$result->shared       = false;
			$result->locked       = false;
			$result->data         = new stdClass();
		}
		return $result;
	}


	/**
	 * Get the order input data.
	 *
	 * @param string $order_input Personalised input (JSON).
	 * @param string $event_id    Unique event id.
	 * @param string $order_id    Unique order id.
	 *
	 * @return stdClass Object of input data
	 * @since 2.1.0
	 */
	public function get_order_input( string $order_input, string $event_id, string $order_id ): stdClass {
		$result = json_decode( $order_input );
		if ( null === $result || false === $result ) {
			$this->logger->error(
				'Order ' . $order_id . ', input fields data not available or corrupt',
				array(
					'type'     => 'Personalisation',
					'event_id' => (int) $event_id,
					'order_id' => (int) $order_id,
				)
			);
			$result               = new stdClass();
			$result->fields       = array();
			$result->tickets      = array();
			$result->invoice      = new stdClass();
			$result->order_amount = '';
			$result->order_vat    = '';
			$result->events       = array();
			$result->orders       = array();
		}
		if ( ! isset( $result->fields ) ) {
			$result->fields = array();
		}
		if ( ! isset( $result->invoice ) ) {
			$result->invoice = new stdClass();
		}
		return $result;
	}


	/**
	 * Get the personalisation fields for this event per ticket type and the definitions of the input fields.
	 *
	 * @param string $event_id      Unique event id.
	 * @param string $order_id      Unique order id.
	 * @param string $input_fields  If empty read from event table.
	 *
	 * @return array Array with:
	 *      - array with personalisation fields per ticket type
	 *      - array with input field definition of personalise fields
	 * @since 2.1.0
	 */
	public function get_personalise_fields( string $event_id, string $order_id, string $input_fields = '' ): array {
		global $wpdb;

		// Get the input fields from the event.
		if ( empty( $input_fields ) ) {
			$event = $wpdb->get_row( $wpdb->prepare( "SELECT input_fields FROM {$wpdb->prefix}fe_events WHERE id = %s", $event_id ) );
			if ( empty( $event ) ) {

				$this->logger->error(
					'Event ' . $event_id . ' not found while searching for personalise fields per ticket type',
					array(
						'type'     => 'Personalisation',
						'event_id' => (int) $event_id,
						'order_id' => (int) $order_id,
					)
				);
				return array();
			}
		}
		$pers   = array();
		$result = json_decode( empty( $input_fields ) ? $event->input_fields : $input_fields );
		if ( null === $result || false === $result || ! isset( $result->fields ) || ! isset( $result->tickets ) ) {
				$this->logger->error(
					'Event ' . $event_id . ', no tickets found or data is corrupt',
					array(
						'type'     => 'Personalisation',
						'event_id' => (int) $event_id,
						'order_id' => (int) $order_id,
					)
				);
			return array();
		}

		// Loop through all tickets.
		foreach ( $result->tickets as $ticket ) {
			if ( ! isset( $ticket->personalise ) ) {
				$pers[ $ticket->name ] = array();
			} else {
				$pers[ $ticket->name ] = empty( $ticket->personalise ) ? array() : explode( ',', $ticket->personalise );
			}
		}

		// Loop through all input fields.
		$inp = array();
		foreach ( $result->fields as $field ) {
			if ( ! isset( $field->personalise ) ) {
				continue;
			}
			if ( 1 === $field->personalise ) {
				$inp[ $field->name ] = $field;
			}
		}

		return array( $pers, $inp );
	}


	/**
	 * Get the ids structure per ticket based on the order uid.
	 *
	 * @param string $uid   Unique order uid.
	 *
	 * @return array
	 * @since 2.1.0
	 */
	public function get_ids_for_uid( string $uid ): array {
		global $wpdb;

		// Get all the tickets.
		$etable  = $wpdb->prefix . 'fe_events';
		$otable  = $wpdb->prefix . 'fe_event_orders';
		$qtable  = $wpdb->prefix . 'fe_event_qrcodes';
		$tickets = $wpdb->get_results( // phpcs:disable
			"SELECT $otable.id, $otable.name, $otable.email, $otable.created_at, $otable.input_fields as order_input, $etable.event_name, " .
			"$etable.event_date, $etable.event_date_format, $etable.seats_needed, $etable.invoice_needed, $etable.tracking_needed, $etable.input_fields, " .
			"$qtable.id as qrcode_id, $qtable.qrcode, $qtable.ticket_type, $qtable.event_id, $qtable.personalised_input " .
			"FROM $qtable " .
			"INNER JOIN $otable ON $otable.id = $qtable.order_id " .
			"INNER JOIN $etable ON $etable.id = $qtable.event_id " .
			"WHERE $qtable.order_id = (SELECT id FROM $otable WHERE uid = '" . $uid . "')"
		); //phpcs:enable

		if ( empty( $tickets ) ) {
			return array();
		}
		list( $pers_fields, $inp ) = $this->get_personalise_fields( $tickets[0]->event_id, $tickets[0]->id, $tickets[0]->input_fields );
		$info                      = $this->get_ids_info( $tickets, $pers_fields );

		// Add order data.
		$info['invoice_needed'] = $tickets[0]->invoice_needed;
		$info['invoice_nonce']  = wp_create_nonce( $uid );
		$info['order_id']       = $tickets[0]->id;
		$info['name']           = $tickets[0]->name;
		$info['email']          = $tickets[0]->email;
		$info['created_at']     = $tickets[0]->created_at;

		// Create the share nonce.
		$ids                 = implode( '-', array_column( $info['data'], 'id' ) );
		$info['share_nonce'] = wp_create_nonce( 'share-' . $uid . $ids );

		// Add download URL and seats if all tickets are locked.
		if ( ! in_array( false, array_column( $info['data'], 'locked' ), true ) ) {
			$info['download'] = rest_url( 'fast-events/v1/tickets/' . $uid );

			// Check if seats are used and add them if needed.
			if ( 1 === (int) $tickets[0]->seats_needed ) {
				foreach ( $info['data'] as $key => $value ) {
					$info['data'][ $key ]['ticket_type'] .= $this->seats->get_seat( (int) $tickets[0]->event_id, $value['qrcode'] );
				}
			}
		}
		return $info;
	}


	/**
	 * Get the ids structure per ticket based on a string of ticket ids.
	 *
	 * @param string $ids   String of unique tickets ids separated by '-'.
	 *
	 * @return array
	 * @since 2.1.0
	 */
	public function get_ids_for_list( string $ids ): array {
		global $wpdb;

		// Get all the tickets.
		$etable  = $wpdb->prefix . 'fe_events';
		$otable  = $wpdb->prefix . 'fe_event_orders';
		$qtable  = $wpdb->prefix . 'fe_event_qrcodes';
		$tickets = $wpdb->get_results( // phpcs:disable
			"SELECT $otable.id, $otable.name, $otable.email, $otable.created_at, $otable.input_fields as order_input, $etable.event_name, " .
			"$etable.event_date, $etable.event_date_format, $etable.seats_needed, $etable.invoice_needed, $etable.tracking_needed, $etable.input_fields, " .
			"$qtable.id as qrcode_id, $qtable.qrcode, $qtable.ticket_type, $qtable.event_id, $qtable.personalised_input " .
			"FROM $qtable " .
			"INNER JOIN $otable ON $otable.id = $qtable.order_id " .
			"INNER JOIN $etable ON $etable.id = $qtable.event_id " .
			"WHERE $qtable.id IN (" . str_replace( '-', ',', $ids ) . ')'
		); // phpcs:enable

		if ( empty( $tickets ) ) {
			return array();
		}
		list( $pers_fields, $inp ) = $this->get_personalise_fields( $tickets[0]->event_id, $tickets[0]->id, $tickets[0]->input_fields );
		$info                      = $this->get_ids_info( $tickets, $pers_fields );

		// Add download URL if all tickets are locked.
		if ( ! in_array( false, array_column( $info['data'], 'locked' ), true ) ) {
			$ids              = implode( '-', array_column( $info['data'], 'id' ) );
			$info['download'] = rest_url( 'fast-events/v1/tickets/' . $ids . '/' . $this->get_download_hash( $ids ) );

			// Check if seats are used and add them if needed.
			if ( 1 === (int) $tickets[0]->seats_needed ) {
				foreach ( $info['data'] as $key => $value ) {
					$info['data'][ $key ]['ticket_type'] .= $this->seats->get_seat( (int) $tickets[0]->event_id, $value['qrcode'] );
				}
			}
		}

		return $info;
	}


	/**
	 * Get the ids structure per ticket
	 *
	 * @param array $tickets       List of tickets.
	 * @param array $pers_fields   List of personalisation fields per ticket type.
	 *
	 * @return array Array with:
	 * *      - id: ticket id
	 * *      - nonce, if the ticket is not yet locked
	 * *      - hash, sha1 hash (edit and download different prefix)
	 * *      - qrcode, if the ticket is available for download
	 * *      - event name
	 * *      - ticket type with optional seating info
	 * *      - date of event
	 * *      - order input fields
	 * *      - ticket personalisation fields
	 * * @since 2.1.0
	 */
	public function get_ids_info( array $tickets, array $pers_fields ): array {
		$ids = array();
		foreach ( $tickets as $ticket ) {
			$personalised_input = $this->get_input( $ticket->personalised_input );

			// Fill structure.
			$tmp                       = array();
			$tmp['id']                 = $ticket->qrcode_id;
			$tmp['event_name']         = $ticket->event_name;
			$tmp['event_date']         = $ticket->event_date;
			$tmp['ticket_type']        = $ticket->ticket_type;
			$tmp['personalised']       = $personalised_input->personalised;
			$tmp['shared']             = $personalised_input->shared;
			$tmp['locked']             = empty( $pers_fields[ $ticket->ticket_type ] ) ? true : $personalised_input->locked;
			$tmp['personalised_input'] = (array) $personalised_input->data;
			$tmp['qrcode']             = 0 === (int) $ticket->tracking_needed
				? $ticket->qrcode
				: ( $ticket->qrcode . ':' . $ticket->qrcode_id . ':' .
					PluginUtil::get_home_domain() . ':' .
					PluginUtil::get_qrcode_signature( $ticket->qrcode, (int) $ticket->qrcode_id ) );

			list( $tmp['nonce'], $tmp['hash'] ) = $this->get_update_nonce_hash( $ticket->qrcode_id );

			$ids[] = $tmp;
		}

		// Are all tickets locked?
		if ( ! in_array( false, array_column( $ids, 'locked' ), true ) ) {
			foreach ( $ids as $key => $id ) {
				unset( $ids[ $key ]['nonce'] );
				$ids[ $key ]['hash'] = $this->get_download_hash( $id['id'] );
			}
		} else {
			foreach ( $ids as $key => $id ) {
				unset( $ids[ $key ]['qrcode'] );
			}
		}
		return array(
			'order_input' => $this->get_order_input( $tickets[0]->order_input, $tickets[0]->event_id, $tickets[0]->id ),
			'data'        => $ids,
		);
	}
}
