<?php
/**
 * Events REST API Controller
 *
 * @since   PBOOKING_SINCE
 * @package Nemtly_Booking
 */

namespace NemtlyBooking\API;

use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;
use WP_Query;
use NemtlyBooking\Core\Event_Creator;
use NemtlyBooking\Services\Recurrence_Calculator;
use NemtlyBooking\Services\Booking_Service;
use NemtlyBooking\Services\Event_Service;
use NemtlyBooking\Repositories\Booking_Repository;
use NemtlyBooking\Repositories\Event_Repository;
use NemtlyBooking\Core\Event_Manager;
use NemtlyBooking\Core\Container;

/**
 * Events Controller class
 *
 * Handles REST API endpoints for events
 *
 * @since PBOOKING_SINCE
 */
class Events_Controller extends Nemtly_API {

    /**
     * Rest base for the current object
     *
     * @var string
     */
    protected $rest_base = 'events';

    /**
     * Dependency injection container
     *
     * @var Container
     */
    private $container;

    /**
     * Cached total after PHP-level filtering in get_filtered_events().
     *
     * @var int
     */
    private $filtered_total = 0;

    /**
     * Constructor
     *
     * @since PBOOKING_SINCE
     *
     * @param Container $container Dependency injection container.
     */
    public function __construct( Container $container ) {
        parent::__construct();
        $this->container = $container;
    }

    /**
     * Register the routes for events
     *
     * @since PBOOKING_SINCE
     */
    public function register_routes() {
        // Register collection endpoints
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base,
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_items' ],
                    'permission_callback' => [ $this, 'permission_check' ],
                    'args'                => $this->get_collection_params(),
                ],
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [ $this, 'create_item' ],
                    'permission_callback' => [ $this, 'permission_check' ],
                    'args'                => $this->get_endpoint_args_for_item_schema(),
                ],
                'schema' => [ $this, 'get_public_item_schema' ],
            ]
        );

        // Add duplicate endpoint
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<id>[\d]+)/duplicate',
            [
                'args' => [
                    'id' => [
                        'description' => __( 'Unique identifier for the event to duplicate.', 'nemtly-booking' ),
                        'type'        => 'integer',
                    ],
                ],
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [ $this, 'duplicate_item' ],
                    'permission_callback' => [ $this, 'permission_check' ],
                ],
            ]
        );

        // Register single event endpoints
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<id>[\d]+)',
            [
                'args'   => [
                    'id' => [
                        'description' => __( 'Unique identifier for the event.', 'nemtly-booking' ),
                        'type'        => 'integer',
                    ],
                ],
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_item' ],
                    'permission_callback' => [ $this, 'permission_check' ],
                    'args'                => [
                        'context' => $this->get_context_param( [ 'default' => 'view' ] ),
                    ],
                ],
                [
                    'methods'             => WP_REST_Server::EDITABLE,
                    'callback'            => [ $this, 'update_item' ],
                    'permission_callback' => [ $this, 'permission_check' ],
                    'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
                ],
                [
                    'methods'             => WP_REST_Server::DELETABLE,
                    'callback'            => [ $this, 'delete_item' ],
                    'permission_callback' => [ $this, 'permission_check' ],
                    'args'                => [
                        'force' => [
                            'type'        => 'boolean',
                            'default'     => false,
                            'description' => __( 'Whether to bypass trash and force deletion.', 'nemtly-booking' ),
                        ],
                    ],
                ],
                'schema' => [ $this, 'get_public_item_schema' ],
            ]
        );

        // Frontend events endpoint
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/frontend',
            [
                [
                    'methods'             => WP_REST_Server::CREATABLE,
                    'callback'            => [ $this, 'get_frontend_events' ],
                    'permission_callback' => [ $this, 'check_public_permissions' ],
                    'args'                => [
                        'filter' => [
                            'type'        => 'string',
                            'default'     => 'all',
                            'description' => __( 'Filter events by status: all (live + upcoming), live (in progress), or upcoming (not yet started).', 'nemtly-booking' ),
                            'enum'        => [ 'all', 'live', 'upcoming' ],
                        ],
                        'page'   => [
                            'type'        => 'integer',
                            'default'     => 1,
                            'description' => __( 'Page number.', 'nemtly-booking' ),
                        ],
                        'limit'  => [
                            'type'        => 'integer',
                            'default'     => 12,
                            'description' => __( 'Number of events per page.', 'nemtly-booking' ),
                        ],
                        'search' => [
                            'type'              => 'string',
                            'default'           => '',
                            'description'       => __( 'Search events by title or content.', 'nemtly-booking' ),
                            'sanitize_callback' => 'sanitize_text_field',
                        ],
                    ],
                ],
            ]
        );

        // Event availability endpoint
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<id>[\d]+)/availability',
            [
                'args' => [
                    'id' => [
                        'description' => __( 'Unique identifier for the event.', 'nemtly-booking' ),
                        'type'        => 'integer',
                    ],
                ],
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_event_availability' ],
                    'permission_callback' => [ $this, 'check_public_permissions' ], // Public access with rate limiting
                ],
            ]
        );

        // Event time slots endpoint — returns available slots for a date
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<id>[\d]+)/slots',
            [
                'args' => [
                    'id' => [
                        'description'       => __( 'Unique identifier for the event.', 'nemtly-booking' ),
                        'type'              => 'integer',
                        'validate_callback' => 'rest_validate_request_arg',
                    ],
                ],
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_event_slots' ],
                    'permission_callback' => [ $this, 'check_public_permissions' ],
                    'args'                => [
                        'date' => [
                            'required'          => true,
                            'type'              => 'string',
                            'format'            => 'date',
                            'description'       => __( 'Date to fetch slots for (Y-m-d).', 'nemtly-booking' ),
                            'validate_callback' => 'rest_validate_request_arg',
                        ],
                    ],
                ],
            ]
        );

        // Bookable dates endpoint — returns upcoming dates with availability
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<id>[\d]+)/dates',
            [
                'args' => [
                    'id' => [
                        'description'       => __( 'Unique identifier for the event.', 'nemtly-booking' ),
                        'type'              => 'integer',
                        'validate_callback' => 'rest_validate_request_arg',
                    ],
                ],
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_bookable_dates' ],
                    'permission_callback' => [ $this, 'check_public_permissions' ],
                    'args'                => [
                        'limit' => [
                            'type'              => 'integer',
                            'default'           => 30,
                            'minimum'           => 1,
                            'maximum'           => 90,
                            'sanitize_callback' => 'absint',
                            'validate_callback' => 'rest_validate_request_arg',
                        ],
                    ],
                ],
            ]
        );

        // Calendar endpoint — returns all events (including recurring instances) for a date window
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/calendar',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_calendar_events' ],
                    'permission_callback' => [ $this, 'check_public_permissions' ],
                    'args'                => [
                        'date_from'  => [
                            'type'              => 'string',
                            'required'          => true,
                            'description'       => __( 'Start date of the visible window (Y-m-d).', 'nemtly-booking' ),
                            'validate_callback' => function ( $value ) {
                                return (bool) preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value );
                            },
                            'sanitize_callback' => 'sanitize_text_field',
                        ],
                        'date_to'    => [
                            'type'              => 'string',
                            'required'          => true,
                            'description'       => __( 'End date of the visible window (Y-m-d).', 'nemtly-booking' ),
                            'validate_callback' => function ( $value ) {
                                return (bool) preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value );
                            },
                            'sanitize_callback' => 'sanitize_text_field',
                        ],
                        'categories' => [
                            'type'        => 'array',
                            'required'    => false,
                            'description' => __( 'Filter by category term IDs.', 'nemtly-booking' ),
                            'items'       => [ 'type' => 'integer' ],
                            'default'     => [],
                        ],
                        'tags'       => [
                            'type'        => 'array',
                            'required'    => false,
                            'description' => __( 'Filter by tag term IDs.', 'nemtly-booking' ),
                            'items'       => [ 'type' => 'integer' ],
                            'default'     => [],
                        ],
                        'event_type' => [
                            'type'              => 'string',
                            'required'          => false,
                            'description'       => __( 'Filter by event type slug.', 'nemtly-booking' ),
                            'sanitize_callback' => 'sanitize_text_field',
                            'default'           => '',
                        ],
                    ],
                ],
            ]
        );

        // Public nonce endpoint for frontend security
        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/public-nonce',
            [
                [
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => [ $this, 'get_public_nonce_endpoint' ],
                    'permission_callback' => '__return_true', // Always allow access to get nonce
                ],
            ]
        );
    }

    /**
     * Get a collection of events
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_items( $request ) {
        $per_page     = ! empty( $request->get_param( 'per_page' ) ) ? intval(
            $request->get_param( 'per_page' )
        ) : 10;
        $page         = ! empty( $request->get_param( 'page' ) ) ? intval( $request->get_param( 'page' ) ) : 1;
        $status_param = ! empty( $request->get_param( 'status' ) ) ? $request->get_param( 'status' ) : 'any';
        // Handle multiple statuses (comma-separated or array)
        if ( is_array( $status_param ) ) {
            $statuses = array_map( 'sanitize_text_field', $status_param );
        } elseif ( strpos( $status_param, ',' ) !== false ) {
            // Split first, then sanitize each status individually
            $statuses = array_map(
                'sanitize_text_field',
                array_map( 'trim', explode( ',', $status_param ) )
            );
        } else {
            $statuses = [ sanitize_text_field( $status_param ) ];
        }
		$status          = in_array( 'any', $statuses, true ) ? 'any' : $statuses;
		$search          = ! empty( $request->get_param( 'search' ) ) ? sanitize_text_field(
			$request->get_param( 'search' )
		) : '';
		$include_virtual = $request->get_param( 'include_virtual' ) ?? false;

		// Sorting. 'date'/'meta_value' sort by the event-date UTC meta; 'title'/'modified' use native columns.
		$orderby_raw = $request->get_param( 'orderby' ) ? sanitize_text_field( $request->get_param( 'orderby' ) ) : 'meta_value';
		$order_raw   = $request->get_param( 'order' ) ? strtoupper( sanitize_text_field( $request->get_param( 'order' ) ) ) : 'ASC';
		$event_type  = $request->get_param( 'event_type' ) ? sanitize_text_field( $request->get_param( 'event_type' ) ) : '';

        // Handle date range for virtual instances
        $start_date = $request->get_param( 'start_date' ) ?? gmdate( 'Y-m-d' );
        $end_date   = $request->get_param( 'end_date' ) ?? gmdate( 'Y-m-d', strtotime( '+30 days' ) );

        if ( $include_virtual ) {
            // Use Recurrence_Calculator for mixed results (master + virtual)
            $calculator    = new Recurrence_Calculator();
            $all_instances = $calculator->get_instances_for_range( $start_date, $end_date );
            // Apply pagination
            $total_items = count( $all_instances );
            $offset      = ( $page - 1 ) * $per_page;
            $events      = array_slice( $all_instances, $offset, $per_page );
		} else {
			// Legacy mode: only return master events (no virtual instances)
			$offset     = ( $page - 1 ) * $per_page;
			$query_args = [
				'post_type'      => 'nemtly_event',
				'posts_per_page' => $per_page,
				'offset'         => $offset,
			];

			// Apply sorting. Event date ('meta_value'|'date') uses the UTC meta key; others are native columns.
			$valid_orderby = [ 'meta_value', 'date', 'title', 'modified' ];
			$orderby       = in_array( $orderby_raw, $valid_orderby, true ) ? $orderby_raw : 'meta_value';
			$order         = in_array( $order_raw, [ 'ASC', 'DESC' ], true ) ? $order_raw : 'ASC';
			if ( in_array( $orderby, [ 'meta_value', 'date' ], true ) ) {
				$query_args['orderby']  = 'meta_value';
				$query_args['meta_key'] = Event_Creator::META_EVENT_DATE_UTC; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			} else {
				$query_args['orderby'] = $orderby;
			}
			$query_args['order'] = $order;

			// Filter by event type (one-on-one, group, event).
			if ( ! empty( $event_type ) ) {
				$query_args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					[
						'key'     => Event_Creator::META_EVENT_TYPE,
						'value'   => $event_type,
						'compare' => '=',
					],
				];
			}
            // Add search if provided
            if ( $search ) {
                $query_args['s'] = $search;
            }
            // Add status filter if provided
            if ( $status && 'any' !== $status ) {
                // WordPress WP_Query supports both single status and array of statuses
                $query_args['post_status'] = is_array( $status ) ? $status : $status;
            }
            // Add taxonomy filters if provided
            $tax_query      = [];
            $category_param = $request->get_param( 'category' );
            if ( ! empty( $category_param ) ) {
                $tax_query[] = [
                    'taxonomy' => 'nemtly_event_category',
                    'field'    => 'slug',
                    'terms'    => array_map( 'sanitize_title', array_map( 'trim', explode( ',', $category_param ) ) ),
                ];
            }
            $tag_param = $request->get_param( 'tag' );
            if ( ! empty( $tag_param ) ) {
                $tax_query[] = [
                    'taxonomy' => 'nemtly_event_tag',
                    'field'    => 'slug',
                    'terms'    => array_map( 'sanitize_title', array_map( 'trim', explode( ',', $tag_param ) ) ),
                ];
            }
            if ( ! empty( $tax_query ) ) {
                $tax_query['relation']   = 'AND';
                $query_args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
            }
            $query  = new WP_Query( $query_args );
            $events = [];
            if ( $query->have_posts() ) {
                while ( $query->have_posts() ) {
                    $query->the_post();
                    $post       = get_post();
                    $event_data = $this->prepare_item_for_response( $post, $request );
                    $events[]   = $this->prepare_response_for_collection( $event_data );
                }
            }
            wp_reset_postdata();
            $total_items = $query->found_posts;
        }

        $total_pages = $per_page > 0 ? (int) ceil( $total_items / $per_page ) : 1;

        return rest_ensure_response(
            [
                'success'    => true,
                'result'     => $events,
                'pagination' => [
                    'total_pages'  => $total_pages,
                    'total_items'  => $total_items,
                    'per_page'     => $per_page,
                    'current_page' => $page,
                ],
            ]
        );
    }

    /**
     * Get a single event
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_item( $request ) {
        $id = $request->get_param( 'id' );
        // Check if this is a virtual instance ID (contains underscore)
        if ( strpos( $id, '_' ) !== false ) {
            $calculator       = new Recurrence_Calculator();
            $virtual_instance = $calculator->get_virtual_instance( $id );
            if ( ! $virtual_instance ) {
                return new WP_Error(
                    'nemtly_booking_virtual_instance_not_found',
                    __( 'Virtual event instance not found.', 'nemtly-booking' ),
                    [ 'status' => 404 ]
                );
            }

            return rest_ensure_response( $virtual_instance->prepare_for_response() );
        }
        // Handle regular master event
        $post = get_post( $id );
        if ( ! $post || 'nemtly_event' !== $post->post_type ) {
            return new WP_Error(
                'nemtly_booking_event_not_found',
                __( 'Event not found.', 'nemtly-booking' ),
                [ 'status' => 404 ]
            );
        }
        $response = $this->prepare_item_for_response( $post, $request );
        $data     = $response->get_data();

        return rest_ensure_response(
            [
                'success' => true,
                'result'  => $data,
            ]
        );
    }

    /**
     * Delete a single event
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function delete_item( $request ) {
        $post = get_post( $request->get_param( 'id' ) );
        if ( ! $post || 'nemtly_event' !== $post->post_type ) {
            return new WP_Error(
                'nemtly_booking_event_not_found',
                __( 'Event not found.', 'nemtly-booking' ),
                [ 'status' => 404 ]
            );
        }
        $force = $request->get_param( 'force' );
        if ( $force ) {
            $result = wp_delete_post( $post->ID, true );
        } else {
            $result = wp_trash_post( $post->ID );
        }
        if ( ! $result ) {
            return new WP_Error(
                'nemtly_booking_cannot_delete',
                __( 'The event cannot be deleted.', 'nemtly-booking' ),
                [ 'status' => 500 ]
            );
        }
        $data = $this->prepare_item_for_response( $post, $request );

        return rest_ensure_response( $data );
    }

    /**
     * Get frontend events
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error Response object or WP_Error.
     */
    public function get_frontend_events( $request ) {
        $filter = sanitize_text_field( $request->get_param( 'filter' ) ?? 'all' );
        $page   = absint( $request->get_param( 'page' ) ?? 1 );
        $limit  = absint( $request->get_param( 'limit' ) ?? 12 );
        $search = sanitize_text_field( $request->get_param( 'search' ) ?? '' );

        // Validate filter
        $valid_filters = [ 'all', 'live', 'upcoming' ];
        if ( ! in_array( $filter, $valid_filters, true ) ) {
            return new WP_Error(
                'nemtly_booking_invalid_filter',
                __( 'Invalid filter parameter.', 'nemtly-booking' ),
                [ 'status' => 400 ]
            );
        }

        // Validate pagination parameters
        if ( $page < 1 ) {
            $page = 1;
        }
        if ( $limit < 1 || $limit > 100 ) {
            $limit = 12; // Default to 12, max 100
        }

        // Get events based on filter (also sets $this->filtered_total)
        $events = $this->get_filtered_events( $filter, $page, $limit, $search );

        // Format events for frontend
        $formatted_events = array_map( [ $this, 'format_event_for_frontend' ], $events );
        $total_items      = $this->filtered_total ?? 0;
        $total_pages      = (int) ceil( $total_items / $limit );

        return [
            'success' => true,
            'data'    => [
                'events'     => $formatted_events,
                'pagination' => [
                    'current_page' => $page,
                    'total_pages'  => $total_pages,
                    'total_items'  => $total_items,
                ],
            ],
        ];
    }

    /**
     * Get events for the calendar view within a specific date window.
     *
     * Fetches both regular events and virtual recurring instances that fall within
     * date_from..date_to, with optional taxonomy and event-type filtering.
     * The date range is capped at 90 days to protect against expensive recurrence expansion.
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Request object with date_from, date_to, categories, tags, event_type params.
     *
     * @return WP_REST_Response|WP_Error Response with events array on success, WP_Error on failure.
     */
    public function get_calendar_events( $request ) {
        $date_from  = $request->get_param( 'date_from' );
        $date_to    = $request->get_param( 'date_to' );
        $categories = array_map( 'absint', (array) $request->get_param( 'categories' ) );
        $tags       = array_map( 'absint', (array) $request->get_param( 'tags' ) );
        $event_type = $request->get_param( 'event_type' );

        // Enforce a 90-day maximum window to limit recurrence expansion cost.
        $from_ts = strtotime( $date_from );
        $to_ts   = strtotime( $date_to );
        if ( false === $from_ts || false === $to_ts || $to_ts < $from_ts ) {
            return new WP_Error(
                'nemtly_booking_invalid_date_range',
                __( 'Invalid date range: date_from must be before date_to.', 'nemtly-booking' ),
                [ 'status' => 400 ]
            );
        }

        $max_days = 90;
        if ( ( $to_ts - $from_ts ) > ( $max_days * DAY_IN_SECONDS ) ) {
            return new WP_Error(
                'nemtly_booking_date_range_too_large',
                sprintf(
                    /* translators: %d: maximum number of days */
                    __( 'Date range cannot exceed %d days.', 'nemtly-booking' ),
                    $max_days
                ),
                [ 'status' => 400 ]
            );
        }

        // Build taxonomy query filter for WP_Query.
        $tax_query = [];
        if ( ! empty( $categories ) ) {
            $tax_query[] = [
                'taxonomy' => 'nemtly_event_category',
                'field'    => 'term_id',
                'terms'    => $categories,
            ];
        }
        if ( ! empty( $tags ) ) {
            $tax_query[] = [
                'taxonomy' => 'nemtly_event_tag',
                'field'    => 'term_id',
                'terms'    => $tags,
            ];
        }
        if ( count( $tax_query ) > 1 ) {
            $tax_query['relation'] = 'AND';
        }

        // Fetch non-recurring events in range. Recurring events (including
        // availability-based) are handled entirely by the recurrence calculator
        // below, so we exclude them here to avoid duplicates on the calendar.
        $query_args = [
            'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
                'relation' => 'AND',
                [
                    'key'     => Event_Creator::META_EVENT_DATE_UTC,
                    'value'   => [ $date_from, $date_to ],
                    'compare' => 'BETWEEN',
                    'type'    => 'DATE',
                ],
                [
                    'relation' => 'OR',
                    [
                        'key'     => Event_Creator::META_RECURRENCE_PATTERN,
                        'value'   => 'none',
                        'compare' => '=',
                    ],
                    [
                        'key'     => Event_Creator::META_RECURRENCE_PATTERN,
                        'compare' => 'NOT EXISTS',
                    ],
                    [
                        'key'     => Event_Creator::META_RECURRENCE_PATTERN,
                        'value'   => '',
                        'compare' => '=',
                    ],
                ],
            ],
        ];
        if ( ! empty( $tax_query ) ) {
            $query_args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
        }
        $event_repository = new Event_Repository();
        $regular_events   = $event_repository->find_all( $query_args );

        // Also get recurring master events whose window may overlap the requested range,
        // then expand virtual instances.
        $calculator        = new Recurrence_Calculator();
        $all_instances     = $calculator->get_instances_for_range( $date_from, $date_to );
        $virtual_instances = [];

        foreach ( $all_instances as $instance ) {
            // get_instances_for_range returns arrays; skip master-event entries that
            // are already covered by $regular_events.
            if ( ! isset( $instance['is_virtual'] ) || ! $instance['is_virtual'] ) {
                continue;
            }
            $virtual_instances[] = $instance;
        }

        // Format regular events for the calendar response shape.
        $formatted = [];
        foreach ( $regular_events as $event ) {
            // Apply event_type filter if requested.
            if ( ! empty( $event_type ) ) {
                $stored_type = get_post_meta( $event->get_id(), Event_Creator::META_RECURRENCE_PATTERN, true );
                // event_type is stored on a dedicated meta via Event_Types_Service; use the post's
                // assigned event-type slug if available (falls back gracefully when not set).
                $post_event_type = get_post_meta( $event->get_id(), 'nemtly_event_type', true );
                if ( $post_event_type !== $event_type ) {
                    continue;
                }
            }
            $formatted[] = $this->format_event_for_calendar( $event );
        }

        // Append virtual recurring instances, formatted to match the calendar response shape.
        foreach ( $virtual_instances as $instance ) {
            $formatted[] = $this->format_virtual_instance_for_calendar( $instance );
        }

        // Sort merged results by date ascending.
        usort(
            $formatted,
            function ( $a, $b ) {
                $date_a = isset( $a['date_utc'] ) ? $a['date_utc'] : ( $a['date'] ?? '' );
                $date_b = isset( $b['date_utc'] ) ? $b['date_utc'] : ( $b['date'] ?? '' );
                $time_a = isset( $a['time_utc'] ) ? $a['time_utc'] : ( $a['time'] ?? '' );
                $time_b = isset( $b['time_utc'] ) ? $b['time_utc'] : ( $b['time'] ?? '' );

                return strcmp( $date_a . $time_a, $date_b . $time_b );
            }
        );

        return rest_ensure_response(
            [
                'success' => true,
                'data'    => $formatted,
            ]
        );
    }

    /**
     * Format a single event for the calendar endpoint response.
     *
     * Returns raw date/time/duration fields (in addition to the display-ready values
     * returned by format_event_for_frontend) so the calendar can position events
     * accurately in time-slot views.
     *
     * @since PBOOKING_SINCE
     *
     * @param \NemtlyBooking\Models\Event $event Event model object.
     *
     * @return array Formatted event data for calendar consumption.
     */
    private function format_event_for_calendar( $event ) {
        $post              = get_post( $event->get_id() );
        $featured_image_id = get_post_thumbnail_id( $event->get_id() );
        $image_url         = '';

        if ( $featured_image_id ) {
            $image_data = wp_get_attachment_image_src( $featured_image_id, 'medium' );
            if ( $image_data ) {
                $image_url = $image_data[0];
            }
        }

        // Fetch assigned taxonomy terms.
        $category_terms = get_the_terms( $event->get_id(), 'nemtly_event_category' );
        $tag_terms      = get_the_terms( $event->get_id(), 'nemtly_event_tag' );
        $category_ids   = ( ! empty( $category_terms ) && ! is_wp_error( $category_terms ) )
            ? wp_list_pluck( $category_terms, 'term_id' )
            : [];
        $tag_ids        = ( ! empty( $tag_terms ) && ! is_wp_error( $tag_terms ) )
            ? wp_list_pluck( $tag_terms, 'term_id' )
            : [];

        // Determine booking availability.
        $max_attendees    = (int) $event->get_max_attendees();
        $booking_repo     = new \NemtlyBooking\Repositories\Booking_Repository();
        $current_bookings = $booking_repo->count_bookings_for_event( $event->get_id() );
        $is_unlimited     = ( 0 === $max_attendees );
        $available_spots  = $is_unlimited ? null : ( $max_attendees - $current_bookings );
        $is_past          = strtotime( $event->get_date_utc() . ' ' . $event->get_time_utc() . ' UTC' ) < time();

        return [
            'id'               => $event->get_id(),
            'title'            => $event->get_title(),
            // Raw date/time for slot positioning.
            'date'             => $event->get_date(),
            'time'             => $event->get_time(),
            'end_date'         => $event->get_end_date(),
            'end_time'         => $event->get_end_time(),
            'date_utc'         => $event->get_date_utc(),
            'time_utc'         => $event->get_time_utc(),
            'duration'         => (int) $event->get_duration(),
            'timezone'         => $event->get_timezone(),
            // Display helpers.
            'location'         => $event->get_location(),
            'image_url'        => $image_url,
            'slug'             => $post ? $post->post_name : '',
            'url'              => $post ? get_permalink( $post ) : '',
            'price'            => (float) $event->get_price(),
            'currency'         => $this->container->get( 'settings_service' )->get_setting( 'currency', 'USD' ),
            'currency_symbol'  => get_option( 'nemtly_currency_symbol', '$' ),
            // Taxonomy assignments for filtering and colour coding.
            'categories'       => $category_ids,
            'tags'             => $tag_ids,
            'event_type'       => get_post_meta( $event->get_id(), 'nemtly_event_type', true ),
            // Booking state.
            'is_bookable'      => ( 'publish' === $event->get_status() ) && ! $is_past,
            'is_past'          => $is_past,
            'available_spots'  => $available_spots,
            'total_capacity'   => $is_unlimited ? 0 : $max_attendees,
            'current_bookings' => $current_bookings,
            'is_unlimited'     => $is_unlimited,
            // Recurrence flags.
            'is_recurring'     => (bool) $event->get_is_recurring(),
            'is_virtual'       => false,
            'master_event_id'  => (int) ( $event->get_parent_event_id() ? $event->get_parent_event_id() : $event->get_id() ),
        ];
    }

    /**
     * Format a virtual recurring instance for the calendar response shape.
     *
     * Virtual instances from the Recurrence_Calculator have a different data shape
     * than regular events. This normalises them so the frontend JS receives a
     * consistent object regardless of whether the event is a master or a virtual instance.
     *
     * @since PBOOKING_SINCE
     *
     * @param array $instance Virtual instance data from Virtual_Instance::to_array().
     *
     * @return array Formatted data matching format_event_for_calendar() shape.
     */
    private function format_virtual_instance_for_calendar( $instance ) {
        $master_id = $instance['master_event_id'] ?? 0;
        $post      = get_post( $master_id );
        $date      = $instance['instance_date'] ?? ( $instance['event_date'] ?? '' );
        $time      = $instance['event_time'] ?? '';
        $duration  = (int) ( $instance['event_duration'] ?? 60 );
        $timezone  = $instance['timezone'] ?? wp_timezone_string();

        // Compute UTC date/time from local date/time + timezone.
        $date_utc = $date;
        $time_utc = $time;
        if ( $date && $time && $timezone ) {
            try {
                $local_dt = new \DateTime( $date . ' ' . $time, new \DateTimeZone( $timezone ) );
                $local_dt->setTimezone( new \DateTimeZone( 'UTC' ) );
                $date_utc = $local_dt->format( 'Y-m-d' );
                $time_utc = $local_dt->format( 'H:i' );
            } catch ( \Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
                // Fall back to local values if timezone conversion fails.
            }
        }

        // Featured image from master event.
        $image_url = '';

        $featured_image_id = get_post_thumbnail_id( $master_id );
        if ( $featured_image_id ) {
            $image_data = wp_get_attachment_image_src( $featured_image_id, 'medium' );
            if ( $image_data ) {
                $image_url = $image_data[0];
            }
        }

        // Taxonomy terms from master event.
        $category_terms = get_the_terms( $master_id, 'nemtly_event_category' );
        $tag_terms      = get_the_terms( $master_id, 'nemtly_event_tag' );
        $category_ids   = ( ! empty( $category_terms ) && ! is_wp_error( $category_terms ) )
            ? wp_list_pluck( $category_terms, 'term_id' )
            : [];
        $tag_ids        = ( ! empty( $tag_terms ) && ! is_wp_error( $tag_terms ) )
            ? wp_list_pluck( $tag_terms, 'term_id' )
            : [];

        // Booking availability for this specific instance date.
        $max_attendees = (int) ( $instance['max_attendees'] ?? 0 );
        $is_unlimited  = ( 0 === $max_attendees );
        $is_past       = $date_utc && $time_utc
            ? strtotime( $date_utc . ' ' . $time_utc . ' UTC' ) < time()
            : false;

        // Extract title — Virtual_Instance returns { rendered: "..." }.
        $title = '';
        if ( is_array( $instance['title'] ?? null ) ) {
            $title = $instance['title']['rendered'] ?? '';
        } elseif ( is_string( $instance['title'] ?? null ) ) {
            $title = $instance['title'];
        } elseif ( $post ) {
            $title = $post->post_title;
        }

        return [
            'id'               => $instance['id'] ?? $instance['instance_id'] ?? $master_id,
            'title'            => $title,
            'date'             => $date,
            'time'             => $time,
            'end_date'         => $instance['end_date'] ?? $date,
            'end_time'         => $instance['end_time'] ?? '',
            'date_utc'         => $date_utc,
            'time_utc'         => $time_utc,
            'duration'         => $duration,
            'timezone'         => $timezone,
            'location'         => $instance['location'] ?? '',
            'image_url'        => $image_url,
            'slug'             => $post ? $post->post_name : '',
            'url'              => $post ? get_permalink( $post ) : '',
            'price'            => (float) get_post_meta( $master_id, Event_Creator::META_PRICE, true ),
            'currency'         => $this->container->get( 'settings_service' )->get_setting( 'currency', 'USD' ),
            'currency_symbol'  => get_option( 'nemtly_currency_symbol', '$' ),
            'categories'       => $category_ids,
            'tags'             => $tag_ids,
            'event_type'       => get_post_meta( $master_id, 'nemtly_event_type', true ),
            'is_bookable'      => ! $is_past && ( $post && 'publish' === $post->post_status ),
            'is_past'          => $is_past,
            'available_spots'  => $is_unlimited ? null : $max_attendees,
            'total_capacity'   => $is_unlimited ? 0 : $max_attendees,
            'current_bookings' => 0,
            'is_unlimited'     => $is_unlimited,
            'is_recurring'     => true,
            'is_virtual'       => true,
            'master_event_id'  => $master_id,
        ];
    }

    /**
     * Get filtered events for frontend
     *
     * @param string $filter Filter type.
     * @param int    $page   Page number.
     * @param int    $limit  Items per page.
     *
     * @return array Array of events.
     */
    /**
     * Build the base WP_Query args for frontend event queries.
     *
     * Centralises the status-based and search filtering so that
     * get_filtered_events() and get_total_items() stay in sync.
     *
     * For "all" and "upcoming", a simple meta_query on the start/end date
     * is sufficient. For "live", we fetch events whose start date is <= today
     * AND end date is >= today; the exact time comparison happens in PHP
     * inside get_filtered_events().
     *
     * @since PBOOKING_SINCE
     *
     * @param string $filter Status filter (all|live|upcoming).
     * @param string $search Optional search term.
     *
     * @return array WP_Query args array.
     */
    private function build_frontend_query_args( $filter, $search = '' ) {
        $today = current_time( 'Y-m-d' );
        $args  = [
            'post_type'   => 'nemtly_event',
            'post_status' => 'publish',
            'meta_key'    => 'nemtly_event_date',
            'orderby'     => 'meta_value',
            'order'       => 'ASC',
        ];

        // One-on-one and group events use recurring availability slots,
        // so they should always appear regardless of their fixed date.
        // We include them via an OR condition alongside the date filter.
        $slot_based_types = [
            'key'     => Event_Creator::META_EVENT_TYPE,
            'value'   => [ 'one-on-one', 'group' ],
            'compare' => 'IN',
        ];

        switch ( $filter ) {
            case 'live':
                // "Type" events whose date range spans today, OR slot-based types.
                $args['meta_query'] = [
                    'relation' => 'OR',
                    $slot_based_types,
                    [
                        'relation' => 'AND',
                        [
                            'key'     => Event_Creator::META_EVENT_DATE_UTC,
                            'value'   => $today,
                            'compare' => '<=',
                            'type'    => 'DATE',
                        ],
                        [
                            'relation' => 'OR',
                            [
                                'key'     => Event_Creator::META_EVENT_END_DATE,
                                'value'   => $today,
                                'compare' => '>=',
                                'type'    => 'DATE',
                            ],
                            [
                                'key'     => Event_Creator::META_EVENT_END_DATE,
                                'compare' => 'NOT EXISTS',
                            ],
                        ],
                    ],
                ];
                break;

            case 'upcoming':
                // Future "type" events OR slot-based types.
                $args['meta_query'] = [
                    'relation' => 'OR',
                    $slot_based_types,
                    [
                        'key'     => Event_Creator::META_EVENT_DATE_UTC,
                        'value'   => $today,
                        'compare' => '>=',
                        'type'    => 'DATE',
                    ],
                ];
                break;

            default: // 'all' — live + upcoming + slot-based types
                $args['meta_query'] = [
                    'relation' => 'OR',
                    $slot_based_types,
                    [
                        'key'     => Event_Creator::META_EVENT_DATE_UTC,
                        'value'   => $today,
                        'compare' => '>=',
                        'type'    => 'DATE',
                    ],
                    [
                        'key'     => Event_Creator::META_EVENT_END_DATE,
                        'value'   => $today,
                        'compare' => '>=',
                        'type'    => 'DATE',
                    ],
                ];
                break;
        }

        // Add search filtering
        if ( ! empty( $search ) ) {
            $args['s'] = $search;
        }

        return $args;
    }

    /**
     * Get filtered events for the frontend display.
     *
     * Fetches candidate events via WP_Query (date-level filtering), then
     * applies exact time-level filtering in PHP for "live" and "upcoming".
     * Pagination is handled after filtering to ensure correct counts.
     *
     * @since PBOOKING_SINCE
     *
     * @param string $filter Status filter (all|live|upcoming).
     * @param int    $page   Page number.
     * @param int    $limit  Items per page.
     * @param string $search Optional search term.
     *
     * @return array Array of Event model objects.
     */
    private function get_filtered_events( $filter, $page, $limit, $search = '' ) {
        $args                   = $this->build_frontend_query_args( $filter, $search );
        $args['posts_per_page'] = -1; // Fetch all candidates, paginate after PHP filter

        $query  = new WP_Query( $args );
        $events = [];
        $now    = current_time( 'timestamp' );

        if ( $query->have_posts() ) {
            while ( $query->have_posts() ) {
                $query->the_post();
                $event_id = get_the_ID();

                // Build start/end timestamps for time-level filtering. Both ends use the
                // UTC meta so the comparison shares a single baseline; fall back to the
                // local meta for events saved before end UTC was stored.
                $start_date = get_post_meta( $event_id, Event_Creator::META_EVENT_DATE_UTC, true );
                $start_time = get_post_meta( $event_id, Event_Creator::META_EVENT_TIME_UTC, true );
                $start_time = ! empty( $start_time ) ? $start_time : '00:00';
                $end_date   = get_post_meta( $event_id, Event_Creator::META_EVENT_END_DATE_UTC, true );
                if ( empty( $end_date ) ) {
                    $end_date = get_post_meta( $event_id, Event_Creator::META_EVENT_END_DATE, true );
                }
                $end_time = get_post_meta( $event_id, Event_Creator::META_EVENT_END_TIME_UTC, true );
                if ( empty( $end_time ) ) {
                    $end_time = get_post_meta( $event_id, Event_Creator::META_EVENT_END_TIME, true );
                }
                $end_time = ! empty( $end_time ) ? $end_time : '23:59';
                $duration = (int) get_post_meta( $event_id, Event_Creator::META_EVENT_DURATION, true );
                $price    = get_post_meta( $event_id, Event_Creator::META_PRICE, true );

                $start_ts = strtotime( "$start_date $start_time" );

                // Calculate end timestamp: use end_date if set, otherwise start + duration
                if ( ! empty( $end_date ) ) {
                    $end_ts = strtotime( "$end_date $end_time" );
                } elseif ( $duration > 0 ) {
                    $end_ts = $start_ts + ( $duration * 60 );
                } else {
                    // No end date or duration — treat as ending at end of start day
                    $end_ts = strtotime( "$start_date 23:59:59" );
                }

                $skip_reason = '';
                $event_type  = get_post_meta( $event_id, Event_Creator::META_EVENT_TYPE, true );

                // Slot-based event types (one-on-one, group) are always included
                // since they use recurring availability, not fixed dates.
                $is_slot_based = in_array( $event_type, [ 'one-on-one', 'group' ], true );

                if ( ! $is_slot_based ) {
                    // Apply time-level filter for fixed-date events only
                    if ( 'live' === $filter && ( $now < $start_ts || $now > $end_ts ) ) {
                        $skip_reason = 'not live';
                    }

                    if ( 'upcoming' === $filter && $now >= $start_ts ) {
                        $skip_reason = 'already started';
                    }

                    // For "all", also exclude fully past events (end < now)
                    if ( 'all' === $filter && $end_ts < $now && $start_ts < $now ) {
                        $skip_reason = 'fully past';
                    }
                }

                if ( $skip_reason ) {
                    continue;
                }

                $events[] = new \NemtlyBooking\Models\Event(
                    [
                        'id'            => $event_id,
                        'title'         => get_the_title(),
                        'description'   => get_the_content(),
                        'date'          => get_post_meta( $event_id, Event_Creator::META_EVENT_DATE, true ),
                        'time'          => get_post_meta( $event_id, Event_Creator::META_EVENT_TIME, true ),
                        'date_utc'      => $start_date,
                        'time_utc'      => $start_time,
                        'timezone'      => get_post_meta( $event_id, Event_Creator::META_EVENT_TIMEZONE, true ),
                        'duration'      => $duration,
                        'location'      => get_post_meta( $event_id, Event_Creator::META_LOCATION, true ),
                        'max_attendees' => get_post_meta( $event_id, Event_Creator::META_MAX_ATTENDEES, true ),
                        'price'         => (float) $price,
                        'status'        => get_post_status( $event_id ),
                    ]
                );
            }
        }

        wp_reset_postdata();

        // Store total for pagination, then slice for the requested page
        $this->filtered_total = count( $events );
        $offset               = ( $page - 1 ) * $limit;

        return array_slice( $events, $offset, $limit );
    }

    /**
     * Format event for frontend display
     *
     * @param Event $event Event object.
     * @return array Formatted event data.
     */
    private function format_event_for_frontend( $event ) {
        $featured_image_id = get_post_thumbnail_id( $event->get_id() );
        $image_url         = '';

        if ( $featured_image_id ) {
            $image_data = wp_get_attachment_image_src( $featured_image_id, 'medium' );
            if ( $image_data ) {
                $image_url = $image_data[0];
            }
        }

        // Get the post object to access the slug
        $post = get_post( $event->get_id() );

        $raw_price       = $event->get_price();
        $currency        = $this->container->get( 'settings_service' )->get_setting( 'currency', 'USD' );
        $currency_symbol = get_option( 'nemtly_currency_symbol', '$' );
        $event_type      = get_post_meta( $event->get_id(), Event_Creator::META_EVENT_TYPE, true );

        $card = [
            'id'              => $event->get_id(),
            'title'           => $event->get_title(),
            'description'     => $event->get_description(),
            'date'            => $event->get_formatted_date(),
            'time'            => $event->get_formatted_time(),
            'location'        => $event->get_location(),
            'image_url'       => $image_url,
            'is_bookable'     => $event->get_status() === 'publish',
            'slug'            => $post ? $post->post_name : '',
            'price'           => (float) $raw_price,
            'currency'        => $currency,
            'currency_symbol' => $currency_symbol,
            'event_type'      => ! empty( $event_type ) ? $event_type : 'one-on-one',
            'styling'         => $event->get_styling(),
        ];

        /**
         * Filter a single event's data in the `/events/frontend` collection response.
         *
         * @since PBOOKING_SINCE
         *
         * @param array $card  The formatted event card data.
         * @param Event $event The event object.
         */
        return apply_filters( 'nemtly_booking_rest_prepare_frontend_event', $card, $event );
    }

    /**
     * Get total pages for pagination
     *
     * @param string $filter Filter type.
     * @param int    $limit  Items per page.
     * @return int Total pages.
     */
    /**
     * Get total pages for pagination.
     *
     * @since PBOOKING_SINCE
     *
     * @param string $filter Date-range filter.
     * @param int    $limit  Items per page.
     * @param string $search Optional search term.
     *
     * @return int Total pages.
     */
    private function get_total_pages( $filter, $limit, $search = '' ) {
        $total_items = $this->get_total_items( $filter, $search );
        return (int) ceil( $total_items / $limit );
    }

    /**
     * Get total items for pagination.
     *
     * @since PBOOKING_SINCE
     *
     * @param string $filter Date-range filter.
     * @param string $search Optional search term.
     *
     * @return int Total items.
     */
    private function get_total_items( $filter, $search = '' ) {
        $args                   = $this->build_frontend_query_args( $filter, $search );
        $args['posts_per_page'] = -1;
        $args['fields']         = 'ids';

        $query = new WP_Query( $args );
        return $query->found_posts;
    }

    /**
     * Get event availability
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_event_availability( $request ) {
        $event_id = (int) $request->get_param( 'id' );

        // Check if event exists and is published
        $post = get_post( $event_id );
        if ( ! $post || 'nemtly_event' !== $post->post_type || 'publish' !== $post->post_status ) {
            return new WP_Error(
                'nemtly_booking_event_not_found',
                __( 'Event not found.', 'nemtly-booking' ),
                [ 'status' => 404 ]
            );
        }

        // Get services from container
        $booking_repository = $this->container->get( 'booking_repository' );
        $event_repository   = $this->container->get( 'event_repository' );
        $event_manager      = $this->container->get( 'event_manager' );
        $booking_service    = $this->container->get( 'booking_service' );

        // Get event data
        $max_attendees  = get_post_meta( $event_id, Event_Creator::META_MAX_ATTENDEES, true );
        $booking_cutoff = get_post_meta( $event_id, Event_Creator::META_BOOKING_CUTOFF, true );
        $event_date     = get_post_meta( $event_id, Event_Creator::META_EVENT_DATE, true );
        $event_time     = get_post_meta( $event_id, Event_Creator::META_EVENT_TIME, true );

        // Count current bookings against the master event.
        $current_bookings = $booking_repository->count_bookings_for_event( $event_id );

        // Calculate availability.
        // For slot-based events, per-slot availability is checked via the /slots endpoint;
        // the general availability check here should always report available.
        $is_unlimited  = ( 0 === (int) $max_attendees );
        $is_slot_based = in_array(
            get_post_meta( $event_id, Event_Creator::META_EVENT_TYPE, true ),
            [ 'one-on-one', 'group' ],
            true
        );

        if ( $is_slot_based ) {
            // Slot-based events manage availability per-slot, not at the event level.
            $available_spots = 999;
            $is_available    = true;
        } else {
            $available_spots = $is_unlimited ? 999 : ( (int) $max_attendees - $current_bookings );
            $is_available    = $available_spots > 0 || $is_unlimited;
        }

        // Check booking deadline.
        // For slot-based events (one-on-one, group), the event_date is a placeholder —
        // cutoff checks apply per-slot, not at the general availability level.
        $can_book         = true;
        $booking_deadline = null;

        if ( ! $is_slot_based && $booking_cutoff && $event_date && $event_time ) {
            $event_timezone = get_post_meta( $event_id, Event_Creator::META_EVENT_TIMEZONE, true );

            // Use UTC values if available, otherwise compute from local + timezone
            $event_date_utc = get_post_meta( $event_id, Event_Creator::META_EVENT_DATE_UTC, true );
            $event_time_utc = get_post_meta( $event_id, Event_Creator::META_EVENT_TIME_UTC, true );

            if ( $event_date_utc && $event_time_utc ) {
                $utc_datetime = $event_date_utc . ' ' . $event_time_utc;
            } else {
                $utc_datetime = $event_date . ' ' . $event_time;
            }

            $deadline_timestamp = strtotime( $utc_datetime . ' UTC' ) - ( (int) $booking_cutoff * 3600 );
            $booking_deadline   = gmdate( 'Y-m-d\TH:i:s\Z', $deadline_timestamp );
            $can_book           = time() < $deadline_timestamp;
        }

        $availability_data = [
            'event_id'         => $event_id,
            'is_available'     => $is_available && $can_book,
            'available_spots'  => $is_unlimited ? 999 : $available_spots,
            'total_capacity'   => $is_unlimited ? 0 : (int) $max_attendees,
            'current_bookings' => $current_bookings,
            'waitlist_count'   => 0, // TODO: Implement waitlist functionality
            'booking_deadline' => $booking_deadline,
            'can_book'         => $can_book,
            'is_unlimited'     => $is_unlimited,
            'message'          => $this->get_availability_message( $is_available, $can_book, $available_spots, $is_unlimited ),
        ];

        return rest_ensure_response(
            [
                'success' => true,
                'result'  => $availability_data,
            ]
        );
    }

    /**
     * Get available time slots for an event on a specific date.
     *
     * @since PBOOKING_SINCE
     *
     * @param \WP_REST_Request $request Full details about the request.
     *
     * @return \WP_REST_Response|\WP_Error
     */
    public function get_event_slots( $request ) {
        $event_id = (int) $request->get_param( 'id' );
        $date     = sanitize_text_field( $request->get_param( 'date' ) );

        $post = get_post( $event_id );
        if ( ! $post || 'nemtly_event' !== $post->post_type ) {
            return new \WP_Error( 'not_found', __( 'Event not found.', 'nemtly-booking' ), [ 'status' => 404 ] );
        }

        $slot_service = $this->container->get( 'slot_service' );
        $slots        = $slot_service->get_slots_for_date( $event_id, $date );

        return rest_ensure_response(
            [
                'success' => true,
                'result'  => [
                    'event_id' => $event_id,
                    'date'     => $date,
                    'slots'    => $slots,
                ],
            ]
        );
    }

    /**
     * Get upcoming bookable dates for an event.
     *
     * @since PBOOKING_SINCE
     *
     * @param \WP_REST_Request $request Full details about the request.
     *
     * @return \WP_REST_Response|\WP_Error
     */
    public function get_bookable_dates( $request ) {
        $event_id = (int) $request->get_param( 'id' );
        $limit    = (int) $request->get_param( 'limit' );

        $post = get_post( $event_id );
        if ( ! $post || 'nemtly_event' !== $post->post_type ) {
            return new \WP_Error( 'not_found', __( 'Event not found.', 'nemtly-booking' ), [ 'status' => 404 ] );
        }

        $slot_service = $this->container->get( 'slot_service' );
        $dates        = $slot_service->get_bookable_dates( $event_id, $limit );

        return rest_ensure_response(
            [
                'success' => true,
                'result'  => [
                    'event_id' => $event_id,
                    'dates'    => $dates,
                ],
            ]
        );
    }

    /**
     * Check permissions for public endpoints with rate limiting
     *
     * @since PBOOKING_SINCE
     *
     * @return bool
     */
    public function check_public_permissions() {
        // Allow public access but implement rate limiting
        $ip_address = $this->get_client_ip();
        $cache_key  = 'nemtly_booking_rate_limit_' . md5( $ip_address );

        // Check rate limiting (max 1000 requests per hour per IP for development)
        $request_count = get_transient( $cache_key );
        if ( false === $request_count ) {
            $request_count = 0;
        }

        // For localhost development, allow more requests
        $max_requests = ( '127.0.0.1' === $ip_address || '::1' === $ip_address ) ? 1000 : 100;

        if ( $request_count >= $max_requests ) {
            return false; // Rate limit exceeded
        }

        // Increment request count
        set_transient( $cache_key, $request_count + 1, HOUR_IN_SECONDS );

        // For public endpoints, allow access without requiring nonces
        // Nonces are optional and won't block requests if invalid/not provided
        // This ensures the endpoint is truly public while still allowing authenticated requests

        return true;
    }

    /**
     * Clear rate limit for development
     * Call this function to reset rate limiting
     */
    public function clear_rate_limit() {
        $ip_address = $this->get_client_ip();
        $cache_key  = 'nemtly_booking_rate_limit_' . md5( $ip_address );
        delete_transient( $cache_key );
    }

    /**
     * Get client IP address
     *
     * @since PBOOKING_SINCE
     *
     * @return string
     */
    private function get_client_ip() {
        $ip_keys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ];

        foreach ( $ip_keys as $key ) {
            if ( array_key_exists( $key, $_SERVER ) === true ) {
                foreach ( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) ) as $ip ) {
                    $ip = trim( $ip );
                    if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) {
                        return $ip;
                    }
                }
            }
        }

        return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) );
    }

    /**
     * Generate a nonce for public API access
     *
     * @since PBOOKING_SINCE
     *
     * @return string
     */
    public static function get_public_nonce() {
        return wp_create_nonce( 'nemtly_booking_public' );
    }

    /**
     * Get public nonce endpoint
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response Response object.
     */
    public function get_public_nonce_endpoint( $request ) {
        return rest_ensure_response(
            [
                'success' => true,
                'nonce'   => self::get_public_nonce(),
            ]
        );
    }

    /**
     * Get availability message
     *
     * @since PBOOKING_SINCE
     *
     * @param bool $is_available Whether spots are available.
     * @param bool $can_book Whether booking is still open.
     * @param int  $available_spots Number of available spots.
     * @param bool $is_unlimited Whether the event has unlimited capacity.
     *
     * @return string
     */
    private function get_availability_message( $is_available, $can_book, $available_spots, $is_unlimited ) {
        if ( ! $can_book ) {
            return __( 'Booking deadline has passed.', 'nemtly-booking' );
        }

        if ( ! $is_available ) {
            return __( 'This event is currently full.', 'nemtly-booking' );
        }

        if ( $is_unlimited ) {
            return __( 'Open for bookings.', 'nemtly-booking' );
        }

        if ( 1 === $available_spots ) {
            return __( '1 spot remaining.', 'nemtly-booking' );
        }

        return sprintf(
            /* translators: %d: number of available spots */
            __( '%d spots remaining.', 'nemtly-booking' ),
            $available_spots
        );
    }

    /**
     * Create a single event
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function create_item( $request ) {
        // Prepare event data for Event_Creator
        $event_data = [
            'title'              => $request->get_param( 'title' ),
            'description'        => $request->get_param( 'content' ),
            'event_date'         => $request->get_param( 'event_date' ),
            'event_time'         => $request->get_param( 'event_time' ),
            'event_end_date'     => $request->get_param( 'event_end_date' ),
            'event_end_time'     => $request->get_param( 'event_end_time' ),
            'timezone'           => $request->get_param( 'timezone' ),
            'event_duration'     => $request->get_param( 'event_duration' ),
            'location'           => $request->get_param( 'location' ),
            'max_attendees'      => $request->get_param( 'max_attendees' ),
            'booking_cutoff'     => $request->get_param( 'booking_cutoff' ),
            'allow_waitlist'     => $request->get_param( 'allow_waitlist' ),
            'recurrence_pattern' => $request->get_param( 'recurrence_pattern' ),
            'recurrence_count'   => $request->get_param( 'recurrence_count' ),
            'recurrence_end'     => $request->get_param( 'recurrence_end' ),
            'is_recurring'       => $request->get_param( 'is_recurring' ),
            'post_status'        => $request->get_param( 'status' ) ?? 'draft',
            'featured_image'     => $request->get_param( 'featured_image' ),
            'price'              => $request->get_param( 'price' ),
            'buffer_before'      => $request->get_param( 'buffer_before' ),
            'buffer_after'       => $request->get_param( 'buffer_after' ),
            'event_type'         => $request->get_param( 'event_type' ),
            'availability'       => $request->get_param( 'availability' ),
            'styling'            => $request->get_param( 'styling' ),
        ];

        // Create event using Event_Creator
        $event_creator = new Event_Creator();
        $result        = $event_creator->create_event( $event_data );
        if ( is_wp_error( $result ) ) {
            return new WP_Error(
                'nemtly_booking_create_failed',
                $result->get_error_message(),
                [ 'status' => 400 ]
            );
        }

        // Assign taxonomy terms if provided
        $categories = $request->get_param( 'categories' );
        if ( ! empty( $categories ) ) {
            wp_set_post_terms( $result, array_map( 'intval', (array) $categories ), 'nemtly_event_category' );
        }
        $tags = $request->get_param( 'tags' );
        if ( ! empty( $tags ) ) {
            wp_set_post_terms( $result, array_map( 'intval', (array) $tags ), 'nemtly_event_tag' );
        }

        $post = get_post( $result );
        $data = $this->prepare_item_for_response( $post, $request );

        return rest_ensure_response( $data );
    }

    /**
     * Update a single event
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function update_item( $request ) {
        $event_id = $request->get_param( 'id' );

        // Get services from container
        $event_repository = $this->container->get( 'event_repository' );
        $event_manager    = $this->container->get( 'event_manager' );
        $event_service    = $this->container->get( 'event_service' );

        // Prepare event data for update
        $update_data = [];

        // Map request parameters to event data
        if ( $request->get_param( 'title' ) ) {
            $update_data['title'] = $request->get_param( 'title' );
        }
        if ( $request->get_param( 'content' ) ) {
            $update_data['description'] = $request->get_param( 'content' );
        }
        if ( $request->get_param( 'event_date' ) ) {
            $update_data['date'] = $request->get_param( 'event_date' );
        }
        if ( $request->get_param( 'event_time' ) ) {
            $update_data['time'] = $request->get_param( 'event_time' );
        }
        if ( $request->get_param( 'timezone' ) ) {
            $update_data['timezone'] = $request->get_param( 'timezone' );
        }

        // Recompute UTC if date, time, or timezone changed
        if ( isset( $update_data['date'] ) || isset( $update_data['time'] ) || isset( $update_data['timezone'] ) ) {
            $existing_event = $event_repository->find( $event_id );
            $date           = $update_data['date'] ?? $existing_event->get_date();
            $time           = $update_data['time'] ?? $existing_event->get_time();
            $timezone       = $update_data['timezone'] ?? $existing_event->get_timezone();
            $utc            = Event_Creator::compute_utc( $date, $time, $timezone );

            $update_data['date_utc'] = $utc['date'];
            $update_data['time_utc'] = $utc['time'];
            if ( ! isset( $update_data['timezone'] ) ) {
                $update_data['timezone'] = $timezone;
            }
        }

        if ( $request->get_param( 'event_end_date' ) !== null ) {
            $update_data['end_date'] = $request->get_param( 'event_end_date' );
        }
        if ( $request->get_param( 'event_end_time' ) !== null ) {
            $update_data['end_time'] = $request->get_param( 'event_end_time' );
        }

        // Recompute end UTC if the end date, end time, or timezone changed.
        if ( isset( $update_data['end_date'] ) || isset( $update_data['end_time'] ) || isset( $update_data['timezone'] ) ) {
            if ( ! isset( $existing_event ) ) {
                $existing_event = $event_repository->find( $event_id );
            }
            $end_date     = $update_data['end_date'] ?? $existing_event->get_end_date();
            $end_time     = $update_data['end_time'] ?? $existing_event->get_end_time();
            $end_timezone = $update_data['timezone'] ?? $existing_event->get_timezone();
            $utc_end      = Event_Creator::compute_utc( $end_date, $end_time, $end_timezone );

            $update_data['end_date_utc'] = $utc_end['date'];
            $update_data['end_time_utc'] = $utc_end['time'];
        }

        if ( $request->get_param( 'event_duration' ) ) {
            $update_data['duration'] = $request->get_param( 'event_duration' );
        }
        if ( $request->get_param( 'location' ) ) {
            $update_data['location'] = $request->get_param( 'location' );
        }
        // Use a null check (not truthy) — max_attendees = 0 means "unlimited" and is a valid value.
        if ( $request->get_param( 'max_attendees' ) !== null ) {
            $update_data['max_attendees'] = $request->get_param( 'max_attendees' );
        }
        // Use null checks — booking_cutoff = 0 means "no cutoff" and allow_waitlist = false
        // means "disabled"; both are valid values that a truthy check would silently drop.
        if ( $request->get_param( 'booking_cutoff' ) !== null ) {
            $update_data['booking_cutoff'] = $request->get_param( 'booking_cutoff' );
        }
        if ( $request->get_param( 'allow_waitlist' ) !== null ) {
            $update_data['allow_waitlist'] = $request->get_param( 'allow_waitlist' );
        }
        if ( $request->get_param( 'recurrence_pattern' ) !== null ) {
            $update_data['recurrence_pattern'] = $request->get_param( 'recurrence_pattern' );
        }
        if ( $request->get_param( 'recurrence_count' ) !== null ) {
            $update_data['recurrence_count'] = $request->get_param( 'recurrence_count' );
        }
        if ( $request->get_param( 'recurrence_end' ) !== null ) {
            $update_data['recurrence_end'] = $request->get_param( 'recurrence_end' );
        }
        if ( $request->get_param( 'is_recurring' ) !== null ) {
            $update_data['is_recurring'] = $request->get_param( 'is_recurring' );
        }
        if ( $request->get_param( 'status' ) ) {
            $update_data['status'] = $request->get_param( 'status' );
        }
        if ( $request->get_param( 'featured_image' ) !== null ) {
            $update_data['featured_image'] = $request->get_param( 'featured_image' );
        }
        if ( $request->get_param( 'price' ) !== null ) {
            $update_data['price'] = $request->get_param( 'price' );
        }
        if ( $request->get_param( 'buffer_before' ) !== null ) {
            $update_data['buffer_before'] = $request->get_param( 'buffer_before' );
        }
        if ( $request->get_param( 'buffer_after' ) !== null ) {
            $update_data['buffer_after'] = $request->get_param( 'buffer_after' );
        }
        if ( $request->get_param( 'event_type' ) !== null ) {
            $update_data['event_type'] = $request->get_param( 'event_type' );
        }
        if ( $request->get_param( 'availability' ) !== null ) {
            $update_data['availability'] = $request->get_param( 'availability' );
        }
        if ( $request->get_param( 'styling' ) !== null ) {
            $update_data['styling'] = $request->get_param( 'styling' );
        }

        try {
            // Update event using Event_Service
            $updated_event = $event_service->update_event( $event_id, $update_data );

            // Update taxonomy terms if provided
            $categories = $request->get_param( 'categories' );
            if ( null !== $categories ) {
                wp_set_post_terms( $event_id, array_map( 'intval', (array) $categories ), 'nemtly_event_category' );
            }
            $tags = $request->get_param( 'tags' );
            if ( null !== $tags ) {
                wp_set_post_terms( $event_id, array_map( 'intval', (array) $tags ), 'nemtly_event_tag' );
            }

            // Get the updated post for response preparation
            $updated_post = get_post( $updated_event->get_id() );
            $data         = $this->prepare_item_for_response( $updated_post, $request );

            return rest_ensure_response( $data );
        } catch ( \Exception $e ) {
            return new WP_Error(
                'nemtly_booking_update_failed',
                $e->getMessage(),
                [ 'status' => 400 ]
            );
        }
    }

    /**
     * Prepare a single event output for response
     *
     * @since PBOOKING_SINCE
     *
     * @param \WP_Post         $post    Event post object.
     * @param \WP_REST_Request $request Request object.
     *
     * @return \WP_REST_Response Response object.
     */
    public function prepare_item_for_response( $post, $request ) {
        // Get event meta data using standardized constants
        $event_date         = get_post_meta( $post->ID, Event_Creator::META_EVENT_DATE, true );
        $event_time         = get_post_meta( $post->ID, Event_Creator::META_EVENT_TIME, true );
        $event_date_utc     = get_post_meta( $post->ID, Event_Creator::META_EVENT_DATE_UTC, true );
        $event_time_utc     = get_post_meta( $post->ID, Event_Creator::META_EVENT_TIME_UTC, true );
        $event_timezone     = get_post_meta( $post->ID, Event_Creator::META_EVENT_TIMEZONE, true );
        $event_end_date     = get_post_meta( $post->ID, Event_Creator::META_EVENT_END_DATE, true );
        $event_end_time     = get_post_meta( $post->ID, Event_Creator::META_EVENT_END_TIME, true );
        $event_end_date_utc = get_post_meta( $post->ID, Event_Creator::META_EVENT_END_DATE_UTC, true );
        $event_end_time_utc = get_post_meta( $post->ID, Event_Creator::META_EVENT_END_TIME_UTC, true );
        $event_duration     = get_post_meta( $post->ID, Event_Creator::META_EVENT_DURATION, true );
        $location           = get_post_meta( $post->ID, Event_Creator::META_LOCATION, true );
        $max_attendees      = get_post_meta( $post->ID, Event_Creator::META_MAX_ATTENDEES, true );
        $booking_cutoff     = get_post_meta( $post->ID, Event_Creator::META_BOOKING_CUTOFF, true );
        $allow_waitlist     = get_post_meta( $post->ID, Event_Creator::META_ALLOW_WAITLIST, true );
        $recurrence_pattern = get_post_meta( $post->ID, Event_Creator::META_RECURRENCE_PATTERN, true );
        $recurrence_count   = get_post_meta( $post->ID, Event_Creator::META_RECURRENCE_COUNT, true );
        $recurrence_end     = get_post_meta( $post->ID, Event_Creator::META_RECURRENCE_END, true );
        $parent_event_id    = get_post_meta( $post->ID, Event_Creator::META_PARENT_EVENT_ID, true );
        $is_recurring       = get_post_meta( $post->ID, Event_Creator::META_IS_RECURRING, true );
        $price              = get_post_meta( $post->ID, Event_Creator::META_PRICE, true );
        $buffer_before      = get_post_meta( $post->ID, Event_Creator::META_BUFFER_BEFORE, true );
        $buffer_after       = get_post_meta( $post->ID, Event_Creator::META_BUFFER_AFTER, true );
        $event_type         = get_post_meta( $post->ID, Event_Creator::META_EVENT_TYPE, true );
        $availability_json  = get_post_meta( $post->ID, Event_Creator::META_AVAILABILITY, true );
        $availability       = ! empty( $availability_json ) ? json_decode( $availability_json, true ) : [];
        $styling_json       = get_post_meta( $post->ID, Event_Creator::META_STYLING, true );
        $styling            = ! empty( $styling_json ) ? json_decode( $styling_json, true ) : [];
        $featured_image_id  = get_post_thumbnail_id( $post->ID );
        $featured_image_url = $featured_image_id ? wp_get_attachment_url( $featured_image_id ) : '';
        $data               = [
            'id'                 => $post->ID,
            'title'              => [
                'rendered' => $post->post_title,
            ],
            'content'            => [
                'raw'      => $post->post_content,
                'rendered' => wp_kses_post( wpautop( $post->post_content ) ),
            ],
            'status'             => $post->post_status,
            'event_date'         => $event_date,
            'event_time'         => $event_time,
            'event_date_utc'     => $event_date_utc,
            'event_time_utc'     => $event_time_utc,
            'timezone'           => $event_timezone ? $event_timezone : wp_timezone_string(),
            'event_end_date'     => $event_end_date ? $event_end_date : '',
            'event_end_time'     => $event_end_time ? $event_end_time : '',
            'event_end_date_utc' => $event_end_date_utc ? $event_end_date_utc : '',
            'event_end_time_utc' => $event_end_time_utc ? $event_end_time_utc : '',
            'event_duration'     => $event_duration,
            'location'           => $location,
            'max_attendees'      => $max_attendees,
            'booking_cutoff'     => $booking_cutoff,
            'allow_waitlist'     => (bool) $allow_waitlist,
            'recurrence_pattern' => $recurrence_pattern,
            'recurrence_count'   => $recurrence_count,
            'recurrence_end'     => $recurrence_end,
            'parent_event_id'    => $parent_event_id,
            'is_recurring'       => (bool) $is_recurring,
            'price'              => (float) $price,
            'currency'           => $this->container->get( 'settings_service' )->get_setting( 'currency', 'USD' ),
            'currency_symbol'    => get_option( 'nemtly_currency_symbol', '$' ),
            'buffer_before'      => (int) $buffer_before,
            'buffer_after'       => (int) $buffer_after,
            'event_type'         => $event_type ? $event_type : 'one-on-one',
            'availability'       => is_array( $availability ) ? $availability : [],
            'styling'            => is_array( $styling ) ? $styling : [],
            'featured_image'     => $featured_image_url,
            'created_at'         => $post->post_date,
            'updated_at'         => $post->post_modified,
        ];
        if ( ! empty( $post->post_title ) ) {
            $data['title'] = [ 'rendered' => $post->post_title ];
            $data['slug']  = $post->post_name;
        }

        // Append taxonomy terms
        $data['categories'] = array_values(
            array_map(
                function ( $term ) {
                    return [
                        'id'   => $term->term_id,
                        'name' => $term->name,
                        'slug' => $term->slug,
                    ];
                },
                wp_get_post_terms( $post->ID, 'nemtly_event_category' ) ? wp_get_post_terms( $post->ID, 'nemtly_event_category' ) : []
            )
        );
        $data['tags']       = array_values(
            array_map(
                function ( $term ) {
                    return [
                        'id'   => $term->term_id,
                        'name' => $term->name,
                        'slug' => $term->slug,
                    ];
                },
                wp_get_post_terms( $post->ID, 'nemtly_event_tag' ) ? wp_get_post_terms( $post->ID, 'nemtly_event_tag' ) : []
            )
        );
        $context            = ! empty( $request['context'] ) ? $request['context'] : 'view';
        $data               = $this->add_additional_fields_to_object( $data, $request );
        $data               = $this->filter_response_by_context( $data, $context );
        $response           = rest_ensure_response( $data );

        return apply_filters( 'nemtly_booking_rest_prepare_event', $response, $post, $request );
    }

    /**
     * Get the Event schema, conforming to JSON Schema
     *
     * @since PBOOKING_SINCE
     *
     * @return array
     */
    public function get_item_schema() {
        $schema = [
            '$schema'    => 'http://json-schema.org/draft-04/schema#',
            'title'      => 'event',
            'type'       => 'object',
            'properties' => [
                'id'                 => [
                    'description' => __( 'Unique identifier for the event.', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'context'     => [ 'view', 'edit', 'embed' ],
                    'readonly'    => true,
                ],
                'title'              => [
                    'description' => __( 'The event title.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit', 'embed' ],
                    'required'    => true,
                ],
                'content'            => [
                    'description' => __( 'The event description.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                ],
                'event_date'         => [
                    'description' => __( 'Event date in Y-m-d format.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'format'      => 'date',
                    'context'     => [ 'view', 'edit' ],
                    'required'    => true,
                ],
                'event_time'         => [
                    'description' => __( 'Event time in H:i format.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'pattern'     => '^([01]?[0-9]|2[0-3]):[0-5][0-9]$',
                    'context'     => [ 'view', 'edit' ],
                    'required'    => true,
                ],
                'event_date_utc'     => [
                    'description' => __( 'Event date in UTC (Y-m-d format, computed).', 'nemtly-booking' ),
                    'type'        => 'string',
                    'format'      => 'date',
                    'context'     => [ 'view' ],
                    'readonly'    => true,
                ],
                'event_time_utc'     => [
                    'description' => __( 'Event time in UTC (H:i format, computed).', 'nemtly-booking' ),
                    'type'        => 'string',
                    'pattern'     => '^([01]?[0-9]|2[0-3]):[0-5][0-9]$',
                    'context'     => [ 'view' ],
                    'readonly'    => true,
                ],
                'timezone'           => [
                    'description' => __( 'Event timezone (IANA identifier, e.g. America/New_York).', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                ],
                'event_end_date'     => [
                    'description' => __( 'Event end date in Y-m-d format.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'default'     => '',
                ],
                'event_end_time'     => [
                    'description' => __( 'Event end time in H:i format.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'default'     => '',
                ],
                'event_end_date_utc' => [
                    'description' => __( 'Event end date in UTC (Y-m-d format, computed).', 'nemtly-booking' ),
                    'type'        => 'string',
                    'format'      => 'date',
                    'context'     => [ 'view' ],
                    'readonly'    => true,
                ],
                'event_end_time_utc' => [
                    'description' => __( 'Event end time in UTC (H:i format, computed).', 'nemtly-booking' ),
                    'type'        => 'string',
                    'pattern'     => '^([01]?[0-9]|2[0-3]):[0-5][0-9]$',
                    'context'     => [ 'view' ],
                    'readonly'    => true,
                ],
                'event_duration'     => [
                    'description' => __( 'Event duration in minutes.', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'minimum'     => 15,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 60,
                ],
                'location'           => [
                    'description' => __( 'Event location.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                ],
                'max_attendees'      => [
                    'description' => __( 'Maximum number of attendees (0 = unlimited).', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 0,
                ],
                'booking_cutoff'     => [
                    'description' => __( 'Stop bookings X hours before event (0 = no cutoff).', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 0,
                ],
                'allow_waitlist'     => [
                    'description' => __( 'Allow waitlist when event is full.', 'nemtly-booking' ),
                    'type'        => 'boolean',
                    'context'     => [ 'view', 'edit' ],
                    'default'     => false,
                ],
                'recurrence_pattern' => [
                    'description' => __( 'Recurrence pattern for repeating events.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'enum'        => [ 'none', 'availability', 'daily', 'weekly', 'monthly', 'yearly' ],
                    'context'     => [ 'view', 'edit' ],
                    'default'     => '',
                ],
                'recurrence_count'   => [
                    'description' => __( 'Number of times to repeat event. 0 = no limit.', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'maximum'     => 365,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 1,
                ],
                'recurrence_end'     => [
                    'description' => __( 'End date for recurring events.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'format'      => 'date',
                    'context'     => [ 'view', 'edit' ],
                ],
                'parent_event_id'    => [
                    'description' => __( 'Parent event ID for recurring events.', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'context'     => [ 'view', 'edit' ],
                    'readonly'    => true,
                ],
                'is_recurring'       => [
                    'description' => __( 'Whether this is a recurring event instance.', 'nemtly-booking' ),
                    'type'        => 'boolean',
                    'context'     => [ 'view', 'edit' ],
                    'readonly'    => true,
                ],
                'created_at'         => [
                    'description' => __( 'Event creation date.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'format'      => 'date-time',
                    'context'     => [ 'view', 'edit' ],
                    'readonly'    => true,
                ],
                'updated_at'         => [
                    'description' => __( 'Event last modified date.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'format'      => 'date-time',
                    'context'     => [ 'view', 'edit' ],
                    'readonly'    => true,
                ],
                'price'              => [
                    'description' => __( 'Event price.', 'nemtly-booking' ),
                    'type'        => 'number',
                    'minimum'     => 0,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 0,
                ],
                'buffer_before'      => [
                    'description' => __( 'Minutes blocked before the event start.', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 0,
                ],
                'buffer_after'       => [
                    'description' => __( 'Minutes blocked after the event end.', 'nemtly-booking' ),
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 0,
                ],
                'categories'         => [
                    'description' => __( 'Assigned category term IDs.', 'nemtly-booking' ),
                    'type'        => 'array',
                    'items'       => [ 'type' => 'integer' ],
                    'context'     => [ 'view', 'edit' ],
                    'default'     => [],
                ],
                'tags'               => [
                    'description' => __( 'Assigned tag term IDs.', 'nemtly-booking' ),
                    'type'        => 'array',
                    'items'       => [ 'type' => 'integer' ],
                    'context'     => [ 'view', 'edit' ],
                    'default'     => [],
                ],
                'event_type'         => [
                    'description' => __( 'Event type slug (one-on-one, group, event).', 'nemtly-booking' ),
                    'type'        => 'string',
                    'enum'        => [ 'one-on-one', 'group', 'event' ],
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 'one-on-one',
                ],
                'availability'       => [
                    'description' => __( 'Weekly availability schedule keyed by day.', 'nemtly-booking' ),
                    'type'        => 'object',
                    'context'     => [ 'view', 'edit' ],
                    'default'     => new \stdClass(),
                ],
                'styling'            => [
                    'description' => __( 'Per-event visual styling tokens. Empty values inherit the host theme/plugin default.', 'nemtly-booking' ),
                    'type'        => 'object',
                    'context'     => [ 'view', 'edit' ],
                    'properties'  => [
						'template'          => [
							'description' => __( 'Selected template id (e.g. "warm"). Empty when no template is active.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'header_background' => [
							'description' => __( 'Header band background color (also tints the banner image).', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'header_title_text' => [
							'description' => __( 'Event title text color (in the header band).', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'tag_text'          => [
							'description' => __( 'Tag/pill label text color (e.g. the event-type pill).', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'tag_background'    => [
							'description' => __( 'Tag/pill background color.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'detail_text'       => [
							'description' => __( 'Date/time/location meta text color.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'price_text'        => [
							'description' => __( 'Price text color.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'button_background' => [
							'description' => __( '"Book now" button background color.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'button_text'       => [
							'description' => __( '"Book now" button label color.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'card_background'   => [
							'description' => __( 'Card and form-field background color.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'border_color'      => [
							'description' => __( 'Border color for cards and dividers.', 'nemtly-booking' ),
							'type'        => 'string',
							'default'     => '',
						],
						'card_radius'       => [
							'description' => __( 'Corner radius preset for cards and inputs.', 'nemtly-booking' ),
							'type'        => 'string',
							'enum'        => [ '', 'sharp', 'rounded', 'pill' ],
							'default'     => '',
						],
						'button_radius'     => [
							'description' => __( 'Corner radius preset for buttons.', 'nemtly-booking' ),
							'type'        => 'string',
							'enum'        => [ '', 'sharp', 'rounded', 'pill' ],
							'default'     => '',
						],
						'shadow'            => [
							'description' => __( 'Card shadow/elevation preset.', 'nemtly-booking' ),
							'type'        => 'string',
							'enum'        => [ '', 'none', 'low', 'medium', 'high' ],
							'default'     => '',
						],
						'spacing'           => [
							'description' => __( 'Spacing density preset.', 'nemtly-booking' ),
							'type'        => 'string',
							'enum'        => [ '', 'compact', 'normal', 'loose' ],
							'default'     => '',
						],
						'heading_size'      => [
							'description' => __( 'Heading font-size scale.', 'nemtly-booking' ),
							'type'        => 'string',
							'enum'        => [ '', 'sm', 'md', 'lg' ],
							'default'     => '',
						],
						'heading_weight'    => [
							'description' => __( 'Heading font-weight.', 'nemtly-booking' ),
							'type'        => 'string',
							'enum'        => [ '', 'normal', 'medium', 'semibold', 'bold' ],
							'default'     => '',
						],
                    ],
                    'default'     => new \stdClass(),
                ],
                'featured_image'     => [
                    'description' => __( 'Featured image URL or attachment ID.', 'nemtly-booking' ),
                    'type'        => [ 'string', 'integer' ],
                    'context'     => [ 'view', 'edit' ],
                    'default'     => '',
                ],
                'status'             => [
                    'description' => __( 'Event publish status.', 'nemtly-booking' ),
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                    'default'     => 'draft',
                ],
            ],
        ];

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

    /**
     * Get the query params for collections
     *
     * @since PBOOKING_SINCE
     *
     * @return array Collection parameters.
     */
    public function get_collection_params() {
        $params = parent::get_collection_params();
        // Pagination parameters
        $params['page']     = [
            'description'       => __( 'Current page of the collection.', 'nemtly-booking' ),
            'type'              => 'integer',
            'default'           => 1,
            'sanitize_callback' => 'absint',
            'validate_callback' => 'rest_validate_request_arg',
            'minimum'           => 1,
        ];
        $params['per_page'] = [
            'description'       => __( 'Maximum number of items to be returned in result set.', 'nemtly-booking' ),
            'type'              => 'integer',
            'default'           => 10,
            'minimum'           => 1,
            'maximum'           => 100,
            'sanitize_callback' => 'absint',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        // Search parameter
        $params['search'] = [
            'description'       => __( 'Limit results to those matching a string.', 'nemtly-booking' ),
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        // Ordering parameters
        $params['orderby'] = [
            'description'       => __( 'Sort collection by event attribute.', 'nemtly-booking' ),
            'type'              => 'string',
            'default'           => 'meta_value',
            'enum'              => [
                'date',
                'title',
                'modified',
                'meta_value',
            ],
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        $params['order']   = [
            'description'       => __( 'Order sort attribute ascending or descending.', 'nemtly-booking' ),
            'type'              => 'string',
            'default'           => 'ASC',
            'enum'              => [ 'ASC', 'DESC' ],
            'sanitize_callback' => function ( $value ) {
                return strtoupper( sanitize_text_field( $value ) );
            },
            'validate_callback' => 'rest_validate_request_arg',
        ];
        // Event status filter (using WordPress post_status)
        // Allow single status or comma-separated statuses (e.g., 'publish', 'draft', 'publish,draft')
        $params['status'] = [
            'description'       => __( 'Limit result set to events with specific status(es). Can be a single status or comma-separated values (e.g., publish,draft).', 'nemtly-booking' ),
            'type'              => 'string',
            'sanitize_callback' => function ( $value ) {
                return sanitize_text_field( $value );
            },
            'validate_callback' => function ( $value, $request, $param ) {
                $valid_statuses = [ 'draft', 'publish', 'future', 'trash', 'any' ];
                // Split comma-separated values
                $statuses = array_map( 'trim', explode( ',', $value ) );
                // Check if all statuses are valid
                foreach ( $statuses as $status ) {
                    if ( ! in_array( $status, $valid_statuses, true ) ) {
                        return false;
                    }
                }
                return true;
            },
        ];
        // Virtual instances parameters
        $params['include_virtual'] = [
            'description'       => __( 'Include virtual recurring event instances.', 'nemtly-booking' ),
            'type'              => 'boolean',
            'default'           => false,
            'sanitize_callback' => 'rest_sanitize_boolean',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        $params['start_date']      = [
            'description'       => __( 'Start date for event range (Y-m-d format).', 'nemtly-booking' ),
            'type'              => 'string',
            'format'            => 'date',
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        $params['end_date']        = [
            'description'       => __( 'End date for event range (Y-m-d format).', 'nemtly-booking' ),
            'type'              => 'string',
            'format'            => 'date',
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        // Taxonomy filter parameters
        $params['category'] = [
            'description'       => __( 'Limit results to events in specific category slugs (comma-separated).', 'nemtly-booking' ),
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        $params['tag']      = [
            'description'       => __( 'Limit results to events with specific tag slugs (comma-separated).', 'nemtly-booking' ),
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];
        // Event type filter (one-on-one, group, event).
        $params['event_type'] = [
            'description'       => __( 'Limit results to events of a specific type.', 'nemtly-booking' ),
            'type'              => 'string',
            'enum'              => [ 'one-on-one', 'group', 'event' ],
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => 'rest_validate_request_arg',
        ];

        return $params;
    }

    /**
     * Duplicate a single event
     *
     * @since PBOOKING_SINCE
     *
     * @param WP_REST_Request $request Full details about the request.
     *
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function duplicate_item( $request ) {
        $post = get_post( $request->get_param( 'id' ) );
        if ( ! $post || 'nemtly_event' !== $post->post_type ) {
            return new WP_Error(
                'nemtly_booking_event_not_found',
                __( 'Event not found.', 'nemtly-booking' ),
                [ 'status' => 404 ]
            );
        }
        // Get all event meta data
        $event_data = [
            'title'              => $post->post_title . ' (Copy)',
            'description'        => $post->post_content,
            'event_date'         => get_post_meta( $post->ID, Event_Creator::META_EVENT_DATE, true ),
            'event_time'         => get_post_meta( $post->ID, Event_Creator::META_EVENT_TIME, true ),
            'event_duration'     => get_post_meta( $post->ID, Event_Creator::META_EVENT_DURATION, true ),
            'location'           => get_post_meta( $post->ID, Event_Creator::META_LOCATION, true ),
            'max_attendees'      => get_post_meta( $post->ID, Event_Creator::META_MAX_ATTENDEES, true ),
            'booking_cutoff'     => get_post_meta( $post->ID, Event_Creator::META_BOOKING_CUTOFF, true ),
            'allow_waitlist'     => get_post_meta( $post->ID, Event_Creator::META_ALLOW_WAITLIST, true ),
            'recurrence_pattern' => get_post_meta( $post->ID, Event_Creator::META_RECURRENCE_PATTERN, true ),
            'recurrence_count'   => get_post_meta( $post->ID, Event_Creator::META_RECURRENCE_COUNT, true ),
            'recurrence_end'     => get_post_meta( $post->ID, Event_Creator::META_RECURRENCE_END, true ),
            'price'              => get_post_meta( $post->ID, Event_Creator::META_PRICE, true ),
            'post_status'        => 'draft', // Always create duplicates as drafts
        ];
        // Create the duplicate event
        $event_creator = new Event_Creator();
        $result        = $event_creator->create_event( $event_data );
        if ( is_wp_error( $result ) ) {
            return new WP_Error(
                'nemtly_booking_duplicate_failed',
                $result->get_error_message(),
                [ 'status' => 400 ]
            );
        }
        $duplicated_post = get_post( $result );
        $data            = $this->prepare_item_for_response( $duplicated_post, $request );

        return rest_ensure_response( $data );
    }
}
