<?php
/**
 * FE Admin Ticket Types controller.
 *
 * @since 1.0
 * @since 1.4 Event authorization restriction
 */

declare( strict_types=1 );
namespace FeData\FastEvents\RestAPI\Public;

defined( 'ABSPATH' ) || exit;

use ArrayObject;
use FeData\FastEvents\Helpers\AuthHelper;
use FeData\FastEvents\Helpers\EventsHelper;
use FeData\FastEvents\Utilities\JsonUtil;
use FeData\FastEvents\Utilities\UniqIds;
use FeData\FastEvents\Utilities\WebhookUtil;
use stdClass;
use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

/**
 * FE Admin Ticket Types class.
 */
class TicketTypesController extends WP_REST_Controller {

	private const CREATE = 0;
	private const UPDATE = 1;
	private const DELETE = 2;

	/**
	 * New ticket types id.
	 *
	 * @since 1.0
	 *
	 * @var string
	 */
	private string $new_id;

	/**
	 * Old ticket name; used if an existing ticket-name is changed.
	 *
	 * @since 1.0
	 *
	 * @var string
	 */
	private string $old_ticket_name = '';

	/**
	 * Event id.
	 *
	 * @since 1.0
	 *
	 * @var int
	 */
	private int $event_id;

	/**
	 * Array of extra input fields.
	 *
	 * @since 2.1.0
	 *
	 * @var array
	 */
	private array $inp_fields;


	/**
	 * Admin Event Ticket Types constructor.
	 *
	 * @param AuthHelper $auth The authentication helper.
	 *
	 * @since 1.0
	 */
	public function __construct( private readonly AuthHelper $auth ) {
		$this->namespace = 'fast-events/v1';
		$this->rest_base = 'admin/events';
	}


	/**
	 * Register the routes for the objects of the controller.
	 *
	 * @since 1.0
	 * @since 2.0 Refactored for a new permission system
	 */
	public function register_routes(): void {

		// Get ticket types.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/ticket-types',
			array(
				'args'   => array(
					'id' => array(
						'description'       => __( 'The unique id of the event', 'fast-events' ),
						'type'              => 'integer',
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => function () {
						return $this->fast_events_admin_permissions( $this->auth::TICKET_TYPES_READ );
					},
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => function () {
						return $this->fast_events_admin_permissions( $this->auth::TICKET_TYPES_CHANGE );
					},
					'args'                => $this->get_endpoint_args_for_item_schema(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/ticket-types/(?P<field>v[0-9,a-f]{5,6})',
			array(
				'args'   => array(
					'id'    => array(
						'description'       => __( 'The unique id of the event', 'fast-events' ),
						'type'              => 'integer',
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
					'field' => array(
						'description'       => __( 'The unique id of the ticket type', 'fast-events' ),
						'type'              => 'string',
						'required'          => true,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => function () {
						return $this->fast_events_admin_permissions( $this->auth::TICKET_TYPES_READ );
					},
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => function () {
						return $this->fast_events_admin_permissions( $this->auth::TICKET_TYPES_CHANGE );
					},
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => function () {
						return $this->fast_events_admin_permissions( $this->auth::TICKET_TYPES_CHANGE );
					},
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/ticket-types/(?P<field>v[0-9,a-f]{5,6})/reorder',
			array(
				'args'   => array(
					'id'    => array(
						'description'       => __( 'The unique id of the event', 'fast-events' ),
						'type'              => 'integer',
						'required'          => true,
						'sanitize_callback' => 'absint',
					),
					'field' => array(
						'description'       => __( 'The unique id of the ticket type', 'fast-events' ),
						'type'              => 'string',
						'required'          => true,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'reorder_item' ),
					'permission_callback' => function () {
						return $this->fast_events_admin_permissions( $this->auth::TICKET_TYPES_CHANGE );
					},
					'args'                => array(
						'old_index' => array(
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
						),
						'new_index' => array(
							'required'          => true,
							'type'              => 'integer',
							'sanitize_callback' => 'absint',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}


	/**
	 * Get the authorization for the Ticket Types endpoints.
	 *
	 * @param int $action The action on the controller.
	 * @return WP_Error|bool
	 *
	 * @since 1.0
	 * @since 2.0 Refactored for a new permission system
	 */
	public function fast_events_admin_permissions( int $action ): WP_Error|bool {
		return $this->auth->authorize( $action );
	}


	/**
	 * Get all ticket types of the event.
	 *
	 * @param WP_REST_Request $request Request instance.
	 * @return WP_REST_Response|WP_Error|array
	 *
	 * @since 1.0
	 */
	public function get_items( $request ): WP_Error|WP_REST_Response|array {
		$result = $this->get_event_data( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$ticket_types = array();

		foreach ( $result as $item ) {
			$data           = $this->prepare_item_for_response( $item, $request );
			$ticket_types[] = $this->prepare_response_for_collection( $data );
		}

		$response = rest_ensure_response( $ticket_types );

		// Generate the totals headers.
		$response->header( 'X-WP-Total', count( $ticket_types ) );
		$response->header( 'X-WP-TotalPages', 1 );

		return $response;
	}


	/**
	 * Retrieve ticket_types.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array|WP_Error Response object on success, or WP_Error object on failure.
	 *
	 * @since 1.0
	 * @since 1.4.0 Event authorization restriction
	 * @since 2.1.0 Added personalising and layout
	 * @since 2.2.0 Added volume pricing
	 */
	private function get_event_data( WP_REST_Request $request ): WP_Error|array {
		global $wpdb;

		// Check the event authorization.
		$resp = $this->auth->check_event_authorization( $request['id'] );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		// Look up the event.
		$event = $wpdb->get_row( $wpdb->prepare( "SELECT id, input_fields, pdf_info FROM {$wpdb->prefix}fe_events WHERE id = %d", $request['id'] ) );
		if ( empty( $event ) ) {
			return new WP_Error( 'not_found', __( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		$inp = JsonUtil::get_input_fields( $event->input_fields );

		// Save event_id.
		$this->event_id = (int) $event->id;

		// Add all fields to the structure.
		$ticket_types = array();
		if ( isset( $inp->tickets ) ) {
			foreach ( $inp->tickets as $field ) {
				$tmp['id']               = $field->id;
				$tmp['name']             = $field->name;
				$tmp['stock_control']    = ! ( '' === $field->stock );
				$tmp['price']            = (float) $field->price;
				$tmp['volume_price']     = $field->volume_price ?? array();
				$tmp['vat']              = (float) $field->vat;
				$tmp['sold']             = (int) $field->sold;
				$tmp['minimum_to_order'] = $field->min;
				$tmp['maximum_to_order'] = $field->max;
				$tmp['is_counted']       = 1 === $field->cnt;
				$tmp['show_remaining']   = 1 === $field->remain;
				$tmp['personalise']      = ( $field->personalise ?? '' );
				$tmp['layout']           = ( $field->layout ?? '' );
				if ( $tmp['stock_control'] ) {
					$tmp['stock'] = (int) $field->stock;
				}
				$ticket_types[] = $tmp;
			}
		}
		$this->inp_fields = $inp->fields;

		$pdf_info = json_decode( $event->pdf_info );
		if ( null !== $pdf_info ) {

			// Add the attachment_id to the tickets types.
			foreach ( $pdf_info->ticket->types as $key => $value ) {
				$el = array_search( $key, array_column( $ticket_types, 'name' ), true );
				if ( false !== $el ) {
					$ticket_types[ $el ]['attachment_id'] = (int) $value;
				}
			}
		}

		return $ticket_types;
	}


	/**
	 * Retrieve a single ticket type.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|array Response object on success, or WP_Error object on failure.
	 *
	 * @since 1.0
	 */
	public function get_item( $request ): WP_Error|WP_REST_Response|array {

		// Look up the ticket types.
		$result = $this->get_event_data( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Does this id exist?
		$key = array_search( $request['field'], array_column( $result, 'id' ), true );
		if ( false === $key ) {
			return new WP_Error( 'not_found', __( 'Ticket type not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		return rest_ensure_response( $this->prepare_item_for_response( $result[ $key ], $request ) );
	}


	/**
	 * Create a new ticket type.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 1.0
	 * @since 2.1.0 Added personalising and layout
	 * @since 2.2.0 Added volume pricing
	 * @since 2.4.0 Added 'show_remaining'
	 */
	public function create_item( $request ): WP_Error|WP_REST_Response|bool|array|stdClass {

		// Look up the ticket types.
		$result = $this->get_event_data( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$ticket_type = array();

		// Is a name provided?
		if ( isset( $request['name'] ) ) {
			$name = $this->sanitize_ticket_name( $request['name'] );
			if ( in_array( $name, array_column( $result, 'name' ), true ) ) {
				return new WP_Error( 'invalid_key', __( 'Ticket type already exists', 'fast-events' ), array( 'status' => 400 ) );
			}
			$ticket_type['name'] = $name;
		} else {
			return new WP_Error( 'no_name', __( 'Name field is missing', 'fast-events' ), array( 'status' => 400 ) );
		}
		$ticket_type['sold'] = 0;

		// Is the price provided?
		if ( isset( $request['price'] ) ) {
			$ticket_type['price'] = $request['price'];
		}

		// Is the volume price provided?
		if ( isset( $request['volume_price'] ) ) {
			$ticket_type['volume_price'] = $request['volume_price'];
		} else {
			$ticket_type['volume_price'] = array();
		}

		// Is the vat percentage provided?
		if ( isset( $request['vat'] ) ) {
			$ticket_type['vat'] = $request['vat'];
		}

		// Is the stock control flag provided?
		if ( isset( $request['stock_control'] ) ) {
			$ticket_type['stock_control'] = 1 === (int) $request['stock_control'];
		}

		// Is the stock provided?
		if ( isset( $request['stock'] ) ) {
			$ticket_type['stock'] = $request['stock'];
		}

		// Is the minimum number of tickets to order provided?
		if ( isset( $request['minimum_to_order'] ) ) {
			$ticket_type['minimum_to_order'] = $request['minimum_to_order'];
		}

		// Is the maximum number of tickets to order provided?
		if ( isset( $request['maximum_to_order'] ) ) {
			$ticket_type['maximum_to_order'] = $request['maximum_to_order'];
		}

		// Is the is_counted flag provided?
		if ( isset( $request['is_counted'] ) ) {
			$ticket_type['is_counted'] = 1 === (int) $request['is_counted'];
		}

		// Is the show_remaining flag provided?
		if ( isset( $request['show_remaining'] ) ) {
			$ticket_type['show_remaining'] = 1 === (int) $request['show_remaining'];
		}

		// Is the attachment id provided?
		if ( isset( $request['attachment_id'] ) ) {
			$ticket_type['attachment_id'] = $request['attachment_id'];
		}

		// Are there personalised input fields?
		if ( isset( $request['personalise'] ) ) {
			$resp = $this->input_field_exists( $request['personalise'], false );
			if ( is_wp_error( $resp ) ) {
				return $resp;
			}
			$ticket_type['personalise'] = $request['personalise'];
		} else {
			$ticket_type['personalise'] = '';
		}

		// Is a specific layout used? If empty is the standard layout.
		if ( isset( $request['layout'] ) ) {
			$resp = $this->input_field_exists( $request['layout'], true );
			if ( is_wp_error( $resp ) ) {
				return $resp;
			}
			$ticket_type['layout'] = $request['layout'];
		} else {
			$ticket_type['layout'] = '';
		}

		// Update event.
		$resp = $this->update_event_data( $ticket_type );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}
		$ticket_type['id'] = $this->new_id;

		$request->set_param( 'context', 'view' );

		return rest_ensure_response( $this->prepare_item_for_response( $ticket_type, $request ) );
	}


	/**
	 * Update a single ticket type.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 1.0
	 * @since 2.1.0 Added personalising and layout
	 * @since 2.2.0 Added volume pricing
	 * @since 2.4.0 Added 'show_remaining'
	 */
	public function update_item( $request ): WP_Error|WP_REST_Response|bool|array|stdClass {

		// Look up the ticket types.
		$result = $this->get_event_data( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Does this key exist?
		$key = array_search( $request['field'], array_column( $result, 'id' ), true );
		if ( false === $key ) {
			return new WP_Error( 'not_found', __( 'Ticket type not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Save the 'old' ticket-name.
		$this->old_ticket_name = $result[ $key ]['name'];

		// Did the name change?
		if ( isset( $request['name'] ) ) {
			$name = $this->sanitize_ticket_name( $request['name'] );
			$idx  = array_search( $name, array_column( $result, 'name' ), true );
			if ( false !== $idx && $idx !== $key ) {
				return new WP_Error( 'invalid_key', __( 'Ticket type already exists', 'fast-events' ), array( 'status' => 400 ) );
			}
			$result[ $key ]['name'] = $name;
		}

		// Did the price change?
		if ( isset( $request['price'] ) ) {
			$result[ $key ]['price'] = $request['price'];
		}

		// Did the volume price change?
		if ( isset( $request['volume_price'] ) ) {
			$result[ $key ]['volume_price'] = $request['volume_price'];
		}

		// Did the vat change?
		if ( isset( $request['vat'] ) ) {
			$result[ $key ]['vat'] = $request['vat'];
		}

		// Did the stock control change?
		if ( isset( $request['stock_control'] ) ) {
			$result[ $key ]['stock_control'] = $request['stock_control'];
		}

		// Did the stock change?
		if ( isset( $request['stock'] ) && $result[ $key ]['stock_control'] ) {
			$result[ $key ]['stock'] = $request['stock'];
		}

		// Did the minimum to order change?
		if ( isset( $request['minimum_to_order'] ) ) {
			$result[ $key ]['minimum_to_order'] = $request['minimum_to_order'];
		}

		// Did the maximum to order change?
		if ( isset( $request['maximum_to_order'] ) ) {
			$result[ $key ]['maximum_to_order'] = $request['maximum_to_order'];
		}

		// Did the is_counted flag change?
		if ( isset( $request['is_counted'] ) ) {
			$result[ $key ]['is_counted'] = $request['is_counted'];
		}

		// Did the show_remaining flag change?
		if ( isset( $request['show_remaining'] ) ) {
			$result[ $key ]['show_remaining'] = $request['show_remaining'];
		}

		// Did the PDF template id change?
		if ( isset( $request['attachment_id'] ) ) {
			$result[ $key ]['attachment_id'] = trim( sanitize_text_field( $request['attachment_id'] ) );
		}

		// Did the personalised input fields change?
		if ( isset( $request['personalise'] ) ) {
			$resp = $this->input_field_exists( $request['personalise'], false );
			if ( is_wp_error( $resp ) ) {
				return $resp;
			}
			$result[ $key ]['personalise'] = $request['personalise'];
		}

		// Did the layout change?.
		if ( isset( $request['layout'] ) ) {
			$resp = $this->input_field_exists( $request['layout'], true );
			if ( is_wp_error( $resp ) ) {
				return $resp;
			}
			$result[ $key ]['layout'] = $request['layout'];
		}

		// Update event.
		$resp = $this->update_event_data( $result[ $key ], self::UPDATE );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		$request->set_param( 'context', 'edit' );

		return rest_ensure_response( $this->prepare_item_for_response( $result[ $key ], $request ) );
	}


	/**
	 * Check if list names are present in the input fields.
	 *
	 * @param string $fields Comma-separated list of input field names.
	 * @param bool   $layout Is this the layout comma-separated list?.
	 * @return bool|WP_Error.
	 *
	 * @since 2.1.0
	 * @since 2.4.1 For layouts: handle empty fields
	 */
	private function input_field_exists( string $fields, bool $layout ): bool|WP_Error {
		if ( empty( $fields ) ) {
			return true;
		}

		$items = explode( ',', $fields );
		foreach ( $items as $item ) {
			if ( ! $layout ) {
				foreach ( $this->inp_fields as $inp ) {
					if ( $inp->name === $item && 1 === $inp->personalise ) {
						continue 2;
					}
				}
			} else {
				if ( empty( $this->inp_fields ) ) {
					if ( 'Order-id' === $item || 'Name' === $item || 'Email' === $item ) {
						continue;
					}
				} else {
					foreach ( $this->inp_fields as $inp ) {
						if ( $inp->name === $item || 'Order-id' === $item || 'Name' === $item || 'Email' === $item ) {
							continue 2;
						}
					}
				}
			}

			/* translators: 1: field name  */
			return new WP_Error( 'not_found', sprintf( __( 'Field [%1$s] not found', 'fast-events' ), $item ), array( 'status' => 400 ) );
		}

		return true;
	}


	/**
	 * Reorder a single ticket type.
	 * The frontend (FE Admin App) dragged the item to another position
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 2.0
	 * @since 2.3.0 Use fast_events_json_encode.
	 * @since 2.4.0 Replaced array_search with in_array.
	 */
	public function reorder_item( WP_REST_Request $request ): WP_Error|WP_REST_Response|bool|array|stdClass {
		global $wpdb;

		// Start the transaction.
		$wpdb->query( 'START TRANSACTION' );

		// Look up the event.
		$event = $wpdb->get_row( $wpdb->prepare( "SELECT input_fields FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE", $request['id'] ) );
		if ( empty( $event ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'not_found', __( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		$inp = JsonUtil::get_input_fields( $event->input_fields );

		// Does this id exist?
		$key = in_array( $request['field'], array_column( $inp->tickets, 'id' ), true );
		if ( false === $key ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'not_found', __( 'Ticket type not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		// Are the indexes in range?
		if ( $request['old_index'] > count( $inp->tickets ) || $request['new_index'] > count( $inp->tickets ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'invalid_index', __( 'Index out of range', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Reorder element.
		$moved_item = array_splice( $inp->tickets, $request['old_index'], 1 );
		array_splice( $inp->tickets, $request['new_index'], 0, $moved_item );

		// Update event.
		$result = $wpdb->update(
			$wpdb->prefix . 'fe_events',
			array( 'input_fields' => JsonUtil::fast_events_json_encode( $inp ) ),
			array( 'id' => $request['id'] )
		);

		// Analyse the update.
		if ( false === $result ) {
			$error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'failed', __( 'Rearrange ticket types failed: ', 'fast-events' ) . $error, array( 'status' => 400 ) );
		}
		if ( 0 === $result ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'nothing_to_update', __( 'Rearrange ticket types: nothing to update', 'fast-events' ), array( 'status' => 400 ) );
		}

		$wpdb->query( 'COMMIT' );

		return rest_ensure_response( array( 'result' => true ) );
	}


	/**
	 * Sanitize ticket name.
	 * Remove possible comma's, sanitize and trim.
	 *
	 * @param string $ticket_name Name of the ticket.
	 * @return string The sanitized name.
	 *
	 * @since 1.0
	 */
	private function sanitize_ticket_name( string $ticket_name ): string {
		return trim( sanitize_text_field( str_replace( ',', '', $ticket_name ) ) );
	}


	/**
	 * Delete a single ticket type.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error|bool|array|stdClass Response object on success, or WP_Error object on failure.
	 *
	 * @since 1.0
	 */
	public function delete_item( $request ): WP_Error|WP_REST_Response|bool|array|stdClass {

		// Look up the ticket types.
		$result = $this->get_event_data( $request );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Does this ticket type exist?
		$key = array_search( $request['field'], array_column( $result, 'id' ), true );
		if ( false === $key ) {
			return new WP_Error( 'not_found', __( 'Ticket type not found', 'fast-events' ), array( 'status' => 404 ) );
		}
		$old_data = $result[ $key ];

		// Delete the ticket type and update the event.
		$resp = $this->update_event_data( $result[ $key ], self::DELETE );
		if ( is_wp_error( $resp ) ) {
			return $resp;
		}

		$request->set_param( 'context', 'edit' );

		$previous = $this->prepare_item_for_response( $old_data, $request );

		$response = new WP_REST_Response();
		$response->set_data(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);

		return $response;
	}


	/**
	 * Update event data.
	 *
	 * @param array $ticket_type Ticket type object.
	 * @param int   $method      Type of action (create, update, or delete).
	 *
	 * @return bool|WP_Error|array|stdClass True, or WP_Error object on failure.
	 *
	 * @since 1.0
	 * @since 1.6.0 - ROLLBACK in case of errors
	 *              - replace unset with array_splice to mitigate json_encode side effects
	 *              - pass $ticket_type by reference
	 * @since 2.1.0 Added personalising and layout
	 * @since 2.2.0 Added volume pricing
	 * @since 2.3.0 Use fast_events_json_encode
	 * @since 2.4.0 Added 'show_remaining'
	 */
	private function update_event_data( array &$ticket_type, int $method = self::CREATE ): WP_Error|bool|array|stdClass {
		global $wpdb;

		if ( $ticket_type['minimum_to_order'] > $ticket_type['maximum_to_order'] ) {
			return new WP_Error( 'wrong_order_amount', __( '\'minimum_to_order\' is larger than \'maximum_to_order\'', 'fast-events' ), array( 'status' => 400 ) );
		}

		// Start the transaction.
		$wpdb->query( 'START TRANSACTION' );

		// Look up the event.
		$event = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT input_fields, pdf_info, webhooks_needed FROM {$wpdb->prefix}fe_events WHERE id = %d FOR UPDATE",
				$this->event_id
			)
		);
		if ( empty( $event ) ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'not_found', __( 'Event not found', 'fast-events' ), array( 'status' => 404 ) );
		}

		$inp = JsonUtil::get_input_fields( $event->input_fields );

		$pdf = $this->get_pdf_fields( $event->pdf_info, $event->input_fields, $ticket_type, $method );
		if ( is_wp_error( $pdf ) ) {
			return $pdf;
		}

		switch ( $method ) {
			case self::DELETE:
				// Does this id exist?
				$key = array_search( $ticket_type['id'], array_column( $inp->tickets, 'id' ), true );
				if ( false === $key ) {
					$wpdb->query( 'ROLLBACK' );

					return new WP_Error( 'not_found', __( 'Ticket type not found', 'fast-events' ), array( 'status' => 404 ) );
				}

				// Remove row.
				array_splice( $inp->tickets, $key, 1 );
				break;

			case self::UPDATE:
				// Does this id exist?
				$key = array_search( $ticket_type['id'], array_column( $inp->tickets, 'id' ), true );
				if ( false === $key ) {
					$wpdb->query( 'ROLLBACK' );

					return new WP_Error( 'not_found', __( 'Ticket type not found', 'fast-events' ), array( 'status' => 404 ) );
				}

				// Update row.
				$inp->tickets[ $key ]->name         = $ticket_type['name'];
				$inp->tickets[ $key ]->price        = (string) $ticket_type['price'];
				$inp->tickets[ $key ]->volume_price = $ticket_type['volume_price'];
				$inp->tickets[ $key ]->vat          = (string) $ticket_type['vat'];
				$inp->tickets[ $key ]->min          = $ticket_type['minimum_to_order'];
				$inp->tickets[ $key ]->max          = $ticket_type['maximum_to_order'];
				$inp->tickets[ $key ]->cnt          = true === $ticket_type['is_counted'] ? 1 : 0;
				$inp->tickets[ $key ]->remain       = true === $ticket_type['show_remaining'] ? 1 : 0;
				if ( $ticket_type['stock_control'] ) {
					$inp->tickets[ $key ]->stock = (int) $ticket_type['stock'];
				} else {
					$inp->tickets[ $key ]->stock = '';
				}
				$inp->tickets[ $key ]->personalise = $ticket_type['personalise'];
				$inp->tickets[ $key ]->layout      = $ticket_type['layout'];
				break;

			case self::CREATE:
				// Does this new name exist?
				$key = in_array( $ticket_type['name'], array_column( $inp->tickets, 'name' ), true );
				if ( false !== $key ) {
					$wpdb->query( 'ROLLBACK' );

					return new WP_Error( 'not_found', __( 'Ticket type already exists', 'fast-events' ), array( 'status' => 400 ) );
				}

				// Create a new row.
				$tmp          = new stdClass();
				$tmp->id      = 'v' . ( count( $inp->tickets ) + count( $inp->tickets ) ) . UniqIds::uniq_field();
				$this->new_id = $tmp->id;
				$tmp->name    = $ticket_type['name'];
				if ( $ticket_type['stock_control'] ) {
					$tmp->stock = (int) $ticket_type['stock'];
				} else {
					$tmp->stock = '';
				}
				$tmp->sold         = 0;
				$tmp->price        = (string) $ticket_type['price'];
				$tmp->volume_price = $ticket_type['volume_price'] ?? array();
				$tmp->min          = $ticket_type['minimum_to_order'];
				$tmp->max          = $ticket_type['maximum_to_order'];
				$tmp->vat          = (string) $ticket_type['vat'];
				$tmp->remain       = true === $ticket_type['show_remaining'] ? 1 : 0;
				$tmp->cnt          = true === $ticket_type['is_counted'] ? 1 : 0;
				$tmp->personalise  = $ticket_type['personalise'];
				$tmp->layout       = $ticket_type['layout'];
				$inp->tickets[]    = $tmp;
				break;
		}

		// Update event.
		$result = $wpdb->update(
			$wpdb->prefix . 'fe_events',
			array(
				'input_fields' => JsonUtil::fast_events_json_encode( $inp ),
				'pdf_info'     => JsonUtil::fast_events_json_encode( $pdf ),
			),
			array(
				'id' => $this->event_id,
			)
		);

		// Analyse the update.
		if ( false === $result ) {
			$error = $wpdb->last_error;
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'failed', __( 'Ticket types update failed: ', 'fast-events' ) . $error, array( 'status' => 400 ) );
		}
		if ( 0 === $result ) {
			$wpdb->query( 'ROLLBACK' );

			return new WP_Error( 'nothing_to_update', __( 'Ticket types update: nothing to update', 'fast-events' ), array( 'status' => 400 ) );
		}

		$wpdb->query( 'COMMIT' );

		// Fire webhooks if needed.
		if ( 1 === (int) $event->webhooks_needed ) {
			WebhookUtil::create_webhook( 'event.updated', $this->event_id );
		}

		return true;
	}


	/**
	 * Retrieve the pdf_info structure.
	 *
	 * @param string $pdf_info     PDF JSON string.
	 * @param string $input_fields Input JSON string.
	 * @param array  $ticket       Ticket info.
	 * @param int    $method       Delete, update, or create.
	 *
	 * @return WP_Error|stdClass|array Error object or pdf_info object.
	 *
	 * @since 1.0
	 * @since 1.6   Set to default attachment_id if 0
	 * @since 2.0.1 Removed pertype and set default id if it is 0
	 * @since 2.2.0 Replace non-existing ticket id
	 */
	private function get_pdf_fields( string $pdf_info, string $input_fields, array &$ticket, int $method ): WP_Error|array|stdClass {
		global $fevt_container;

		$pdf = json_decode( $pdf_info, true );
		if ( empty( $pdf ) ) {
			$pdf = array(
				'ticket'  => array(
					'id'        => 0,
					'x'         => 13,
					'y'         => 58,
					'rotate'    => 0,
					'scale'     => 100,
					'no_border' => 0,
					'types'     => new ArrayObject(),
				),
				'invoice' => array(
					'id'        => 0,
					'vat'       => 0,
					'name'      => array(
						'x' => 27,
						'y' => 64,
					),
					'invoiceno' => array(
						'x' => 127,
						'y' => 64,
					),
					'line'      => array(
						'x' => 25,
						'y' => 92,
						'w' => 158,
					),
				),
			);

			// Add the ticket types.
			$inp = json_decode( $input_fields );
			if ( null !== $inp ) {
				$tmp = array();
				foreach ( $inp->tickets as $inp_ticket ) {
					$tmp[ $inp_ticket->name ] = $pdf['ticket']['id'];
				}
				$pdf['ticket']['types'] = $tmp;
			}
		}

		switch ( $method ) {
			case self::DELETE:
				unset( $pdf['ticket']['types'][ $ticket['name'] ] );
				break;

			case self::CREATE:
			case self::UPDATE:
				// Check if the PDF template exists.
				if ( 0 !== (int) $ticket['attachment_id'] && ! $fevt_container->get( EventsHelper::class )->check_pdf_template_id( (int) $ticket['attachment_id'] ) ) {
					return new WP_Error( 'not_found', __( 'PDF ticket template not found', 'fast-events' ), array( 'status' => 404 ) );
				}

				// Was there a ticket-name change or did we create a new one?
				if ( '' !== $this->old_ticket_name && $this->old_ticket_name !== $ticket['name'] ) {
					unset( $pdf['ticket']['types'][ $this->old_ticket_name ] );
				}
				if ( 0 === (int) $ticket['attachment_id'] ) {
					$pdf['ticket']['types'][ $ticket['name'] ] = (int) $pdf['ticket']['id'];
					$ticket['attachment_id']                   = (int) $pdf['ticket']['id'];
				} else {
					$pdf['ticket']['types'][ $ticket['name'] ] = (int) $ticket['attachment_id'];
					if ( 0 === $pdf['ticket']['id'] || ! $fevt_container->get( EventsHelper::class )->check_pdf_template_id( (int) $pdf['ticket']['id'] ) ) {
						$pdf['ticket']['id'] = (int) $ticket['attachment_id'];
					}
				}
				break;
		}

		return $pdf;
	}


	/**
	 * Retrieves the query params for the collections.
	 *
	 * @return array Query parameters for the collection.
	 *
	 * @since 2.0
	 */
	public function get_collection_params(): array {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}


	/**
	 * Prepares a single ticket type output for response.
	 *
	 * @param mixed           $item    Ticket type object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 *
	 * @since 1.0
	 * @since 2.1.0 Added personalising and layout
	 * @since 2.2.0 Added volume pricing
	 * @since 2.4.0 Added 'show_remaining'
	 */
	public function prepare_item_for_response( $item, $request ): WP_REST_Response {

		$data   = array();
		$fields = $this->get_fields_for_response( $request );

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $item['id'];
		}

		if ( rest_is_field_included( 'name', $fields ) ) {
			$data['name'] = $item['name'];
		}

		if ( rest_is_field_included( 'price', $fields ) ) {
			$data['price'] = $item['price'];
		}

		if ( rest_is_field_included( 'volume_price', $fields ) ) {
			if ( ! empty( $item['volume_price'] ) ) {
				usort( $item['volume_price'], fn( $a, $b ) => (int) array_key_first( (array) $a ) - (int) array_key_first( (array) $b ) );
			}
			$data['volume_price'] = $item['volume_price'];
		}

		if ( rest_is_field_included( 'vat', $fields ) ) {
			$data['vat'] = $item['vat'];
		}

		if ( rest_is_field_included( 'stock_control', $fields ) ) {
			$data['stock_control'] = $item['stock_control'];
		}

		if ( rest_is_field_included( 'stock', $fields ) && $item['stock_control'] ) {
			$data['stock'] = $item['stock'];
		}

		if ( rest_is_field_included( 'sold', $fields ) ) {
			$data['sold'] = $item['sold'];
		}

		if ( rest_is_field_included( 'minimum_to_order', $fields ) ) {
			$data['minimum_to_order'] = $item['minimum_to_order'];
		}

		if ( rest_is_field_included( 'maximum_to_order', $fields ) ) {
			$data['maximum_to_order'] = $item['maximum_to_order'];
		}

		if ( rest_is_field_included( 'is_counted', $fields ) ) {
			$data['is_counted'] = $item['is_counted'];
		}

		if ( rest_is_field_included( 'show_remaining', $fields ) ) {
			$data['show_remaining'] = $item['show_remaining'];
		}

		if ( rest_is_field_included( 'attachment_id', $fields ) ) {
			$data['attachment_id'] = $item['attachment_id'];
		}

		if ( rest_is_field_included( 'attachment_name', $fields ) ) {
			if ( 0 !== $item['attachment_id'] ) {
				$data['attachment_name'] = get_the_title( $item['attachment_id'] ) ?? '';
			}
		}

		if ( rest_is_field_included( 'personalise', $fields ) ) {
			$data['personalise'] = $item['personalise'];
		}

		if ( rest_is_field_included( 'layout', $fields ) ) {
			$data['layout'] = $item['layout'];
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'embed';

		$data = $this->add_additional_fields_to_object( $data, $request );
		$data = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $item ) );
		}

		return $response;
	}


	/**
	 * Prepares links for the ticket_types request.
	 *
	 * @param array $field Ticket type object.
	 * @return array Links for the given order.
	 *
	 * @since 1.0
	 */
	protected function prepare_links( array $field ): array {
		return array(
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%d/ticket-types/%s', $this->namespace, $this->rest_base, $this->event_id, $field['id'] ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s/%d/ticket-types', $this->namespace, $this->rest_base, $this->event_id ) ),
			),
		);
	}


	/**
	 * Return the ticket types schema.
	 *
	 * @return array
	 *
	 * $since 1.0
	 * @since 2.1.0 Added personalising and layout
	 * @since 2.2.0 Added volume pricing
	 * @since 2.4.0 Added 'show_remaining'
	 */
	public function get_item_schema(): array {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'ticket-types',
			'type'       => 'object',
			'properties' => array(
				'id'               => array(
					'description' => __( 'The unique id of the ticket type.', 'fast-events' ),
					'context'     => array( 'view', 'embed' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'name'             => array(
					'description' => __( 'The name of the ticket type.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'required'    => true,
					'minLength'   => 1,
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'price'            => array(
					'description' => __( 'The price of a single ticket.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'number',
					'minimum'     => 0,
					'default'     => 0.00,
				),
				'volume_price'     => array(
					'description' => __( 'Volume pricing.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'array',
					'uniqueItems' => true,
					'items'       => array(
						'type'                 => 'object',
						'patternProperties'    => array(
							'^[0-9]+$' => array(
								'description' => __( 'Starting price for a certain quantity.', 'fast-events' ),
								'context'     => array( 'view', 'edit', 'embed' ),
								'type'        => 'number',
								'minimum'     => 0,
							),
						),
						'additionalProperties' => false,
					),
				),
				'vat'              => array(
					'description' => __( 'The VAT percentage.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'number',
					'minimum'     => 0,
					'default'     => 0,
				),
				'stock_control'    => array(
					'description' => __( 'Is stock control used.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'boolean',
					'default'     => false,
				),
				'stock'            => array(
					'description' => __( 'Stock for this ticket type.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'integer',
					'minimum'     => 0,
					'default'     => 0,
				),
				'sold'             => array(
					'description' => __( 'Stock for this ticket type.', 'fast-events' ),
					'context'     => array( 'view', 'embed' ),
					'type'        => 'integer',
					'readonly'    => true,
				),
				'minimum_to_order' => array(
					'description' => __( 'The minimum number of tickets to order.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'integer',
					'minimum'     => 0,
					'default'     => 0,
				),
				'maximum_to_order' => array(
					'description' => __( 'The maximum number of tickets to order.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'integer',
					'minimum'     => 1,
					'default'     => 1,
				),
				'is_counted'       => array(
					'description' => __( 'Are the sold tickets counted on the event level.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'boolean',
					'default'     => true,
				),
				'show_remaining'   => array(
					'description' => __( 'Show the remaining tickets in the order form.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'boolean',
					'default'     => false,
				),
				'attachment_id'    => array(
					'description' => __( 'The unique media id of the PDF template.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'integer',
					'default'     => 0,
				),
				'attachment_name'  => array(
					'description' => __( 'The name of the PDF template.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'personalise'      => array(
					'description' => __( 'Comma-separated list of input field names used for personalisation.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'layout'           => array(
					'description' => __( 'Comma-separated list of input field names that will be printed in the eticket qrcode block.', 'fast-events' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'type'        => 'string',
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}
}
