<?php
namespace EM_Pro\API;

require_once EMP_DIR . '/classes/api/emp-api-utils.php';
require_once EMP_DIR . '/classes/api/emp-api-schemas.php';
require_once EMP_DIR . '/classes/api/emp-api-service.php';
require_once EMP_DIR . '/classes/api/emp-api-rest.php';
require_once EMP_DIR . '/classes/api/emp-api-abilities.php';

class API {

	public static function init() {
		REST::init();
		Abilities::init();
		add_filter( 'em_api_prepare_booking', array( static::class, 'prepare_booking' ), 100, 3 );
		add_filter( 'em_api_prepare_event', array( static::class, 'prepare_event' ), 100, 3 );
		add_filter( 'em_api_booking_requirements', array( static::class, 'booking_requirements' ), 10, 2 );

		// Booking on behalf of someone else (a guest or another user) is a Pro capability — core always books under the authenticated account. These three filters, exposed by EM\API\Service::save_booking(), let Pro reshape the request, force the registration path, and assign the explicit booker.
		add_filter( 'em_api_booking_request_data', array( static::class, 'booking_request_data' ), 10, 4 );
		add_filter( 'em_api_booking_force_registration', array( static::class, 'booking_force_registration' ), 10, 4 );
		add_filter( 'em_api_assign_booking_person', array( static::class, 'assign_booking_person' ), 10, 4 );

		// Expose Pro's abilities on EM core's dedicated MCP server by adding our namespace to the prefix list it advertises. Core stays ignorant of which add-ons are installed — each satellite opts its own abilities in here.
		add_filter( 'em_mcp_server_ability_prefixes', array( __CLASS__, 'register_ability_prefix' ) );
	}

	/**
	 * Add the `events-manager-pro/` ability namespace to EM core's MCP server so Pro tools (coupons, transactions, attendance, RSVP, custom forms) appear on the same `wp-json/mcp/events-manager` endpoint as the core ones.
	 */
	public static function register_ability_prefix( $prefixes ) {
		$prefixes[] = 'events-manager-pro/';
		return $prefixes;
	}

	public static function prepare_booking( $api, $EM_Booking, $context ) {
		return apply_filters( 'emp_api_prepare_booking', $api, $EM_Booking, $context );
	}

	public static function prepare_event( $api, $EM_Event, $context ) {
		return apply_filters( 'emp_api_prepare_event', $api, $EM_Event, $context );
	}

	/**
	 * Map a structured booker `person` object onto the booking-form field names, and reconcile the name fields, so the booking form reads on-behalf details like a normal submission. Hooks core's `em_api_booking_request_data`.
	 *
	 * The `person` object's keys (`name`, `email`, `phone`) are translated to the form spellings (`user_name`, `user_email`, `dbem_phone`); only keys the caller didn't already set at the top level are filled. We also reconcile `user_name` with `first_name`/`last_name` both ways, because some custom booking forms split the name into separate required fields while others use a single one — deriving the missing spelling means every form's required fields are satisfied regardless of which the caller sent.
	 */
	public static function booking_request_data( $request, $data, $EM_Booking, $id ) {
		if ( !empty( $data['person'] ) && is_array( $data['person'] ) ) {
			$person = $data['person'];
			$person_map = array(
				'user_name'  => $person['name'] ?? null,
				'first_name' => $person['first_name'] ?? null,
				'last_name'  => $person['last_name'] ?? null,
				'user_email' => $person['email'] ?? null,
				'dbem_phone' => $person['phone'] ?? null,
			);
			foreach ( $person_map as $field => $value ) {
				if ( $value !== null && $value !== '' && !array_key_exists( $field, $request ) ) {
					$request[ $field ] = $value;
				}
			}
		}
		if ( !empty( $request['user_name'] ) && is_string( $request['user_name'] ) ) {
			if ( empty( $request['first_name'] ) && empty( $request['last_name'] ) ) {
				$parts = preg_split( '/\s+/', trim( $request['user_name'] ), 2 );
				$request['first_name'] = $parts[0];
				if ( !empty( $parts[1] ) ) {
					$request['last_name'] = $parts[1];
				}
			}
		} elseif ( empty( $request['user_name'] ) && ( !empty( $request['first_name'] ) || !empty( $request['last_name'] ) ) ) {
			$request['user_name'] = trim( ( $request['first_name'] ?? '' ) . ' ' . ( $request['last_name'] ?? '' ) );
		}
		return $request;
	}

	/**
	 * Decide whether this create is "on behalf of someone else", in which case core flips EM_Bookings::$force_registration so the booking form reads the supplied booker rather than the calling admin. Hooks core's `em_api_booking_force_registration`.
	 *
	 * True only when: it's a create (not an update), the caller can manage bookings, no explicit existing-user `person_id` was given (that's a different intent — book a known WP user, handled in assign_booking_person), and the payload actually carries booker identity.
	 */
	public static function booking_force_registration( $force, $EM_Booking, $data, $id ) {
		if ( $id ) {
			return $force;
		}
		if ( !empty( $data['person_id'] ) && absint( $data['person_id'] ) > 0 ) {
			return $force;
		}
		if ( !Utils::can_manage_bookings() ) {
			return $force;
		}
		return !empty( static::extract_booker_identity( $data ) ) ? true : $force;
	}

	/**
	 * Attribute a booking to an explicit booker supplied by a manager. Hooks core's `em_api_assign_booking_person`; returns a WP_Error to abort the save, or the unchanged `$result` otherwise.
	 *
	 * EM resolves a new booking's person from the current WP session (`em_load_event()` sets the global `$EM_Person` to the logged-in user, and `EM_Booking::get_person()` adopts it because the booking_id is still empty), so without intervention an admin's bookings are all filed under the admin. Here: an explicit `person_id` (book a known WP user) wins outright; otherwise we collect booker identity from any shape the caller used and, for a booking manager, force EM onto the guest path (`person_id = 0`) with the details in `booking_meta['registration']` so `get_person()` applies them. No WP user account is created — this is EM's native guest-person model.
	 */
	public static function assign_booking_person( $result, $EM_Booking, $data, $id ) {
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$can_assign_person = Utils::can_manage_bookings();
		if ( isset( $data['person_id'] ) && $can_assign_person ) {
			$EM_Booking->person_id = absint( $data['person_id'] );
			$EM_Booking->person = new \EM_Person( $EM_Booking->person_id );
			return $result;
		}
		$identity = static::extract_booker_identity( $data );
		if ( empty( $identity ) ) {
			return $result; // No explicit booker — leave the booking attributed to the authenticated account (core's default).
		}
		if ( $can_assign_person || !is_user_logged_in() ) {
			$EM_Booking->person_id = 0;
			$EM_Booking->person = new \EM_Person( 0 );
		}
		$EM_Booking->booking_meta['registration'] = array_merge( $EM_Booking->booking_meta['registration'] ?? array(), $identity );
		if ( !$EM_Booking->person_id ) {
			if ( empty( $identity['user_email'] ) || !is_email( $identity['user_email'] ) ) {
				return Utils::error( 'emp_api_booking_person_invalid', __( 'A valid email address is required to book on behalf of a guest.', 'em-pro' ), 400 );
			}
		}
		return $result;
	}

	/**
	 * Normalise booker identity from any payload shape an agent might use into EM's `booking_meta['registration']` field set (`user_name`, `first_name`, `last_name`, `user_email`, `dbem_phone`).
	 *
	 * Sources are scanned in precedence order and merged field-by-field (first non-empty wins): the top-level payload, then a `person` object, then nested `registration` / `booking` / `booking_fields` / `fields` blocks. Both the `person`-object spellings (`name`, `email`, `phone`) and the form spellings (`user_name`, `user_email`, `dbem_phone`) are accepted. If only first/last names are given, `user_name` is composed from them so EM's display-name builder has something to work with.
	 *
	 * @return array Sanitised registration fields, or an empty array when no identity was supplied.
	 */
	protected static function extract_booker_identity( $data ) {
		$sources = array( $data );
		foreach ( array( 'person', 'registration', 'booking', 'booking_fields', 'fields' ) as $key ) {
			if ( !empty( $data[ $key ] ) && is_array( $data[ $key ] ) ) {
				$sources[] = $data[ $key ];
			}
		}
		$identity = array();
		foreach ( $sources as $src ) {
			if ( !isset( $identity['user_name'] ) ) {
				if ( !empty( $src['user_name'] ) && is_string( $src['user_name'] ) ) {
					$identity['user_name'] = sanitize_text_field( $src['user_name'] );
				} elseif ( !empty( $src['name'] ) && is_string( $src['name'] ) ) {
					$identity['user_name'] = sanitize_text_field( $src['name'] );
				}
			}
			if ( !isset( $identity['first_name'] ) && !empty( $src['first_name'] ) && is_string( $src['first_name'] ) ) {
				$identity['first_name'] = sanitize_text_field( $src['first_name'] );
			}
			if ( !isset( $identity['last_name'] ) && !empty( $src['last_name'] ) && is_string( $src['last_name'] ) ) {
				$identity['last_name'] = sanitize_text_field( $src['last_name'] );
			}
			if ( !isset( $identity['user_email'] ) ) {
				if ( !empty( $src['user_email'] ) ) {
					$identity['user_email'] = sanitize_email( $src['user_email'] );
				} elseif ( !empty( $src['email'] ) ) {
					$identity['user_email'] = sanitize_email( $src['email'] );
				}
			}
			if ( !isset( $identity['dbem_phone'] ) ) {
				if ( !empty( $src['dbem_phone'] ) ) {
					$identity['dbem_phone'] = sanitize_text_field( $src['dbem_phone'] );
				} elseif ( !empty( $src['phone'] ) ) {
					$identity['dbem_phone'] = sanitize_text_field( $src['phone'] );
				}
			}
		}
		// Compose a display name from first/last when no full name was supplied — EM's get_person() prefers user_name and only guesses first/last from it, so seeding it here keeps the split correct.
		if ( !isset( $identity['user_name'] ) && ( isset( $identity['first_name'] ) || isset( $identity['last_name'] ) ) ) {
			$composed = trim( ( $identity['first_name'] ?? '' ) . ' ' . ( $identity['last_name'] ?? '' ) );
			if ( $composed !== '' ) {
				$identity['user_name'] = $composed;
			}
		}
		return $identity;
	}

	/**
	 * Inject Pro booking requirements: custom booking-form fields, attendee-form fields (per ticket space), and active payment gateways. Hooks em_api_booking_requirements (fired by EM\API\Service::get_booking_requirements).
	 */
	public static function booking_requirements( $payload, $EM_Event ) {
		if ( class_exists( 'EM_Booking_Form' ) ) {
			$form = \EM_Booking_Form::get_form( $EM_Event );
			if ( $form && !empty( $form->form_fields ) ) {
				$payload['booking_fields'] = static::requirement_fields( $form->form_fields, 'booking', $EM_Event );
			}
		}
		if ( class_exists( 'EM_Attendees_Form' ) ) {
			$attendee_form = \EM_Attendees_Form::get_form( $EM_Event );
			if ( $attendee_form && !empty( $attendee_form->form_fields ) ) {
				$fields = static::requirement_fields( $attendee_form->form_fields, 'attendee', $EM_Event );
				if ( $fields ) {
					$payload['attendee_fields'] = $fields;
				}
			}
		}
		if ( class_exists( '\EM\Payments\Gateways' ) ) {
			$gateways = array();
			foreach ( \EM\Payments\Gateways::active_gateways( 'bookings' ) as $slug => $Gateway ) {
				$gateways[] = array(
					'slug'   => $slug,
					'label'  => is_string( $Gateway::$title ) ? $Gateway::$title : (string) $slug,
					'online' => ( $slug !== 'offline' ),
				);
			}
			$has_paid = false;
			foreach ( (array) ( $payload['tickets'] ?? array() ) as $ticket ) {
				if ( !empty( $ticket['price'] ) && $ticket['price'] > 0 ) {
					$has_paid = true;
					break;
				}
			}
			$payload['payment'] = array(
				'required_when'   => $gateways ? 'a booked ticket has price > 0' : 'never',
				'field'           => 'gateway',
				'active_gateways' => $gateways,
			);
			if ( $has_paid && !$gateways ) {
				$payload['notes'][] = __( 'This event has paid tickets but no active payment gateway.', 'em-pro' );
			}
		}
		if ( Utils::can_manage_bookings() ) {
			$payload['notes'][] = __( 'To book on someone else\'s behalf, send their `user_name` and `user_email` (or a `person` object). As a manager this files the booking under that guest rather than your own account; omit them and the booking is attributed to you.', 'em-pro' );
		}
		return $payload;
	}

	protected static function requirement_fields( $form_fields, $form_type, $EM_Event = null ) {
		$out = array();
		if ( !is_array( $form_fields ) ) {
			return $out;
		}
		$phone_enabled = class_exists( '\\EM\\Phone' ) && \EM\Phone::is_enabled();
		foreach ( $form_fields as $fieldid => $field ) {
			$type = isset( $field['type'] ) ? $field['type'] : 'text';
			if ( in_array( $type, array( 'html', 'captcha' ), true ) ) {
				continue;
			}
			$entry = array(
				'field'    => $fieldid,
				'label'    => ( isset( $field['label'] ) && $field['label'] !== '' ) ? wp_strip_all_tags( $field['label'] ) : $fieldid,
				'type'     => static::requirement_field_type( $type ),
				'required' => !empty( $field['required'] ),
			);
			if ( in_array( $type, array( 'select', 'multiselect', 'radio', 'checkboxes' ), true ) && !empty( $field['options_select_values'] ) ) {
				$options = array_values( array_filter( array_map( 'trim', preg_split( '/\r\n|\r|\n/', $field['options_select_values'] ) ), 'strlen' ) );
				if ( $options ) {
					$entry['options'] = $options;
				}
			}
			if ( $form_type === 'attendee' ) {
				$entry['per_space'] = true;
				$entry['location']  = 'em_tickets.<ticket_id>.ticket_bookings[].attendee.' . $fieldid;
			} else {
				$entry['location']  = 'booking.' . $fieldid;
			}
			// Whether the actual server-side validator on this field is libphonenumber's E.164 check (not the custom regex the form admin may have entered). That happens for type=tel when EM\Phone is enabled, and for the literal dbem_phone field shape regardless of mapped type. When this is true, any options_text_regex set on the field is dead code in the validator and we must NOT advertise it as the API contract.
			$libphonenumber_in_charge = ( $type === 'tel' && $phone_enabled )
				|| ( $fieldid === 'dbem_phone' && $phone_enabled );

			// Validator-aware metadata (format / pattern / example / messages), built on EM core's helper then refined with the form's per-field overrides where they actually apply to validation.
			if ( class_exists( '\\EM\\API\\Service' ) ) {
				$validation = \EM\API\Service::build_field_validation( $entry, $EM_Event );
			} else {
				$validation = array();
			}

			// Custom regex pattern. Only meaningful when the validator IS the regex itself — when libphonenumber is in charge for a tel field the regex is dead code and would only mislead the consumer.
			if ( ! $libphonenumber_in_charge ) {
				$regex = $field['options_text_regex'] ?? $field['options_reg_regex'] ?? '';
				if ( $regex ) {
					$validation['pattern'] = trim( $regex, '/' );
					// The type-derived ideals (E.164 example, country hint, etc.) reflect what a stock phone validator would accept — but the custom regex IS the real validator on this field, so clear the metadata that no longer matches what passes.
					unset(
						$validation['example'],
						$validation['preferred_format'],
						$validation['human_hint'],
						$validation['country_context']
					);
					$validation['format'] = 'pattern';
				}
			}

			// Type-aware error-message and tip-text overrides, matching the keys the validator in emp-forms.php actually consults for each field type.
			$err_keys_by_type = array(
				'tel'         => array( 'options_tel_error', 'options_text_error' ),
				'select'      => array( 'options_select_error' ),
				'multiselect' => array( 'options_selection_error' ),
				'radio'       => array( 'options_select_error' ),
				'checkboxes'  => array( 'options_selection_error' ),
				'checkbox'    => array( 'options_checkbox_error' ),
				'date'        => array( 'options_date_error' ),
				'upload'      => array( 'options_upload_error' ),
				'reg'         => array( 'options_reg_error' ),
			);
			foreach ( $err_keys_by_type[ $type ] ?? array( 'options_text_error' ) as $key ) {
				if ( ! empty( $field[ $key ] ) ) {
					$validation['invalid_message'] = $field[ $key ];
					break;
				}
			}
			$tip_keys_by_type = array(
				'tel'         => array( 'options_tel_tip', 'options_text_tip' ),
				'select'      => array( 'options_select_tip' ),
				'multiselect' => array( 'options_selection_tip' ),
				'radio'       => array( 'options_select_tip' ),
				'checkboxes'  => array( 'options_selection_tip' ),
				'checkbox'    => array( 'options_checkbox_tip' ),
				'upload'      => array( 'options_upload_tip' ),
				'reg'         => array( 'options_reg_tip' ),
			);
			foreach ( $tip_keys_by_type[ $type ] ?? array( 'options_text_tip' ) as $key ) {
				if ( ! empty( $field[ $key ] ) ) {
					$validation['placeholder'] = $field[ $key ];
					break;
				}
			}

			// Upload-specific affordances (extensions, max size, multiple) so an agent uploading via the bookings-form upload field knows what'll pass.
			if ( $type === 'upload' ) {
				if ( !empty( $field['options_upload_extensions'] ) ) {
					$exts = array_values( array_filter( array_map( 'trim', preg_split( '/[,\s]+/', (string) $field['options_upload_extensions'] ) ), 'strlen' ) );
					if ( $exts ) {
						$validation['accepted_extensions'] = $exts;
					}
				}
				if ( !empty( $field['options_upload_max_file_size'] ) ) {
					$validation['max_file_size_bytes'] = absint( $field['options_upload_max_file_size'] );
				}
				if ( !empty( $field['options_upload_allow_multiple'] ) ) {
					$validation['allow_multiple'] = true;
				}
			}
			if ( !empty( $validation ) ) {
				$entry['validation'] = $validation;
			}
			$out[] = $entry;
		}
		return $out;
	}

	protected static function requirement_field_type( $type ) {
		switch ( $type ) {
			case 'user_email':
				return 'email';
			case 'select':
			case 'radio':
				return 'select';
			case 'checkboxes':
			case 'multiselect':
				return 'multiselect';
			case 'dbem_phone':
				// Semantically a phone field — keep it advertised as `tel` so EM core's build_field_validation gives it E.164 metadata.
				return 'tel';
			case 'first_name':
			case 'last_name':
			case 'user_name':
			case 'text':
			case 'textarea':
			case 'reg':
				return 'text';
			default:
				return $type;
		}
	}
}

API::init();
