<?php
/**
 * REST API v2 for appointments.
 *
 * Extends the existing appointments controller to use the v2 namespace.
 * Index queries are provided by a dedicated v2 index controller and are
 * no longer handled by this CRUD controller.
 *
 * @package WooCommerce\Appointments\Rest\Controller\V2
 */

/**
 * V2 Appointments controller.
 *
 * Endpoints (same as v1 under v2 namespace)
 * - `GET /{namespace}/appointments`
 * - `GET /{namespace}/appointments/{id}`
 * - `POST /{namespace}/appointments`
 * - `PUT|PATCH /{namespace}/appointments/{id}`
 * - `DELETE /{namespace}/appointments/{id}`
 * - `POST /{namespace}/appointments/batch`
 *
 * Permissions
 * - Read: Published/public statuses readable; permission trait mirrors v1.
 * - Mutations: `manage_woocommerce` capability.
 *
 * Notes
 * - For occurrence reads at scale, prefer `GET /wc-appointments/v2/index`.
 *
 * Notes:
 * - Inherits full CRUD from WC_Appointments_REST_Appointments_Controller.
 * - Uses `wc-appointments/v2` only; no separate indexed namespace.
 * - For list queries with a date range inside the horizon, it restricts
 *   the WP_Query via `post__in` using cached appointment IDs for speed.
 * - Extends response to include cal_color from product and customer name fields
 *   to optimize admin calendar initial load performance.
 */
class WC_Appointments_REST_V2_Appointments_Controller extends WC_Appointments_REST_Appointments_Controller {

	/**
	 * Set the v2 namespace.
	 */
	public function __construct() {
		$this->namespace = WC_Appointments_REST_API::V2_NAMESPACE;
	}

	/**
	 * Prepare objects query - optimized to use indexed cache when available.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array
	 */
	protected function prepare_objects_query( $request ): array {
		// Normalize date parameters for parent query (strip '@' prefix and convert timestamps to date strings)
		// The parent query uses strtotime which may not handle '@' prefix or pure numeric timestamps correctly
		$normalized_request = $request;
		if ( isset( $request['date_from'] ) && ! empty( $request['date_from'] ) ) {
			$date_from_clean = ltrim( $request['date_from'], '@' );
			// If it's a numeric timestamp, convert to date string for strtotime
			if ( is_numeric( $date_from_clean ) ) {
				$normalized_request['date_from'] = date( 'Y-m-d H:i:s', absint( $date_from_clean ) );
			} else {
				$normalized_request['date_from'] = $date_from_clean;
			}
		}
		if ( isset( $request['date_to'] ) && ! empty( $request['date_to'] ) ) {
			$date_to_clean = ltrim( $request['date_to'], '@' );
			// If it's a numeric timestamp, convert to date string for strtotime
			if ( is_numeric( $date_to_clean ) ) {
				$normalized_request['date_to'] = date( 'Y-m-d H:i:s', absint( $date_to_clean ) );
			} else {
				$normalized_request['date_to'] = $date_to_clean;
			}
		}

		$args = parent::prepare_objects_query( $normalized_request );

		// Performance optimization: Use indexed cache table to get appointment IDs first
		// This bypasses slow meta queries by using indexed columns
		$use_indexed_cache = false;
		$indexed_appointment_ids = [];

		// Check if indexed availability is enabled
		if ( class_exists( 'WC_Appointments_Cache_Availability' ) && method_exists( 'WC_Appointments_Cache_Availability', 'is_index_enabled' ) ) {
			$use_indexed_cache = WC_Appointments_Cache_Availability::is_index_enabled();
		}

		// Only use indexed cache if enabled
		if ( $use_indexed_cache && class_exists( 'WC_Appointments_Availability_Cache_Data_Store' ) ) {
			$date_from = $request['date_from'] ?? '';
			$date_to   = $request['date_to'] ?? '';

			// Convert date strings to timestamps if provided
			// Handle '@' prefix used by calendar (e.g., '@1234567890')
			$start_ts = 0;
			$end_ts   = 0;
			if ( ! empty( $date_from ) ) {
				$date_from = ltrim( $date_from, '@' ); // Remove '@' prefix if present
				$start_ts = is_numeric( $date_from ) ? absint( $date_from ) : strtotime( $date_from );
			}
			if ( ! empty( $date_to ) ) {
				$date_to = ltrim( $date_to, '@' ); // Remove '@' prefix if present
				$end_ts = is_numeric( $date_to ) ? absint( $date_to ) : strtotime( $date_to );
			}

			// Use indexed cache if we have date filters or set reasonable defaults for admin calendar
			// Admin calendar typically loads a view range, so we optimize for that case
			if ( 0 < $start_ts || 0 < $end_ts ) {
				// Get retention period to determine what's available in the index
				$retention_days = defined( 'WC_Appointments_Cache_Availability::INDEX_RETENTION_DAYS' )
					? WC_Appointments_Cache_Availability::INDEX_RETENTION_DAYS
					: 30;
				$retention_cut = strtotime( '-' . $retention_days . ' days UTC' );
				$now = current_time( 'timestamp', true );

				// Set default range if only one bound is provided
				if ( 0 >= $start_ts ) {
					// Default to retention period start to include past appointments
					$start_ts = $retention_cut;
				}
				if ( 0 >= $end_ts ) {
					$end_ts = strtotime( '+2 years' );
				}

				// Ensure valid range
				if ( 0 < $start_ts && 0 < $end_ts && $end_ts > $start_ts ) {
					// Check cache horizon
					$horizon_months = function_exists( 'wc_appointments_get_cache_horizon_months' ) ? wc_appointments_get_cache_horizon_months() : 3;
					$horizon_ts     = strtotime( '+' . $horizon_months . ' months UTC' );

					// Determine if the query range overlaps with indexed data
					// Indexed data exists from retention_cut to horizon_ts
					// Note: retention_cut is the earliest end_ts that's kept (appointments with end_ts < retention_cut are pruned)
					// Edge case: Appointments that start before retention_cut but end after retention_cut ARE in the index
					$indexed_start = $retention_cut;
					$indexed_end = $horizon_ts;

					// Check if requested range overlaps with indexed range
					// Overlap means: requested range intersects with indexed range
					$overlaps_indexed = ( $end_ts >= $indexed_start && $start_ts <= $indexed_end );

					// Determine if query includes dates before retention period
					$includes_pre_retention = ( $start_ts < $indexed_start );

					// Strategy for handling edge cases when appointments span the retention period:
					//
					// Edge Case 1: Query range entirely within indexed bounds (start_ts >= retention_cut)
					//   - All appointments in range are in the index
					//   - Use indexed cache exclusively (fastest path)
					//
					// Edge Case 2: Query range entirely before retention_cut (end_ts < retention_cut)
					//   - No appointments in range are in the index (all pruned)
					//   - Skip indexed cache, use parent query only
					//
					// Edge Case 3: Query range spans retention boundary (start_ts < retention_cut AND end_ts >= retention_cut)
					//   - Appointments completely before retention_cut (start < retention_cut AND end < retention_cut) are NOT in index
					//   - Appointments that span boundary (start < retention_cut BUT end >= retention_cut) ARE in index
					//   - Appointments entirely after retention_cut (start >= retention_cut) ARE in index
					//   - Strategy: Clear indexed_appointment_ids to force parent query to find ALL appointments
					//     This ensures we get appointments that are completely before retention_cut
					//     The parent query will also find appointments that span the boundary (they're in the database)
					//     Trade-off: We lose index performance benefit for correctness

					if ( $overlaps_indexed && ( $end_ts <= $horizon_ts || 0 >= $horizon_ts ) ) {
						$data_store = WC_Data_Store::load( 'appointments-availability-cache' );
						if ( $data_store ) {
							// Query the index for appointments that intersect with the requested range
							// The time_between filter uses intersection logic, so it will find:
							// - Appointments that start and end within the query range
							// - Appointments that start before but end within the query range (spanning retention boundary)
							// - Appointments that start within but end after the query range
							// Only clamp the end to horizon, but use the full requested start
							// This ensures we find appointments that span the retention boundary
							$query_start_ts = $start_ts;
							$query_end_ts = min( $end_ts, $indexed_end );

							$filters = [
								'source' => 'appointment',
								'time_between' => [
									'start_ts' => $query_start_ts,
									'end_ts'   => $query_end_ts,
								],
							];

							// Add product filter if provided
							if ( isset( $request['product_id'] ) ) {
								$filters['product_id'] = absint( $request['product_id'] );
							}

							// Add staff filter if provided
							if ( isset( $request['staff_id'] ) ) {
								$filters['staff_id'] = absint( $request['staff_id'] );
							}

							// Get appointment IDs from indexed cache
							// This is much faster than meta queries
							$cache_rows = $data_store->get_items( $filters );
							if ( ! empty( $cache_rows ) && is_array( $cache_rows ) ) {
								// Extract source_id (appointment ID) from cache rows
								$indexed_appointment_ids = array_filter( array_map( fn(array $row) => absint( $row['source_id'] ?? 0 ), $cache_rows ) );
								$indexed_appointment_ids = array_unique( $indexed_appointment_ids );
							}

							// Edge case handling: If query includes dates before retention_cut:
							// - Appointments completely before retention_cut (start < retention_cut AND end < retention_cut) are NOT in index
							// - Appointments that span the boundary (start < retention_cut BUT end >= retention_cut) ARE in index
							// - We need to query both the index AND the database to get all appointments
							// Strategy: Query index for appointments that might span the boundary, but don't restrict
							// parent query to indexed IDs only - let parent query find all appointments in the range
							// WordPress will automatically dedupe results
							if ( $includes_pre_retention ) {
								// Mark that we need to query database as well (don't restrict to indexed IDs)
								// Store indexed IDs separately for potential future use, but clear main array
								// to allow parent query to run without restriction
								$indexed_appointment_ids = []; // Clear to force full parent query
							}
						}
					}
					// If query range doesn't overlap with indexed data, don't use indexed cache
					// This allows the parent query to use regular database queries for dates outside retention
				}
			}
		}

		// If we got IDs from indexed cache, use post__in to bypass meta queries
		// If we didn't get IDs (e.g., querying dates outside retention), fall back to parent query
		if ( [] !== $indexed_appointment_ids ) {
			// Remove date-related meta queries since we're using post__in from indexed cache
			if ( isset( $args['meta_query'] ) && is_array( $args['meta_query'] ) ) {
				// Keep non-date meta queries (like customer_id) but remove date/product/staff ones
				$filtered_meta_query = [];
				foreach ( $args['meta_query'] as $key => $meta_query_item ) {
					if ( is_array( $meta_query_item ) ) {
						// Keep customer_id meta query if it exists
						if ( isset( $meta_query_item['key'] ) && '_appointment_customer_id' === $meta_query_item['key'] ) {
							$filtered_meta_query[] = $meta_query_item;
						}
						// Remove date, product, and staff meta queries as they're handled by indexed cache
					} else {
						$filtered_meta_query[ $key ] = $meta_query_item;
					}
				}
				if ( [] !== $filtered_meta_query ) {
					$args['meta_query'] = $filtered_meta_query;
				} else {
					unset( $args['meta_query'] );
				}
			}

			// Use post__in with the cached IDs
			// If there's already a post__in from parent, intersect them
			if ( ! empty( $args['post__in'] ) && is_array( $args['post__in'] ) ) {
				$args['post__in'] = array_intersect( $args['post__in'], $indexed_appointment_ids );
			} else {
				$args['post__in'] = $indexed_appointment_ids;
			}

			// Set orderby to preserve order from cache if not already set
			if ( empty( $args['orderby'] ) ) {
				$args['orderby'] = 'post__in';
			}
		}

		return $args;
	}

	/**
	 * Get objects (i.e. Appointments).
	 *
	 * @param array $query_args Query args.
	 *
	 * @return array Appointments data.
	 */
	protected function get_objects( $query_args ): array {
		$objects = parent::get_objects( $query_args );

		// Performance Optimization: Prime caches for Products, Customers, and Orders.
		$product_ids  = [];
		$customer_ids = [];
		$order_ids    = [];

		foreach ( $objects as $object ) {
			if ( ! is_a( $object, 'WC_Appointment' ) ) {
				continue;
			}
			$product_ids[]  = $object->get_product_id( 'view' );
			$customer_ids[] = $object->get_customer_id( 'view' );
			$order_id       = $object->get_order_id( 'view' );
			if ( 0 < $order_id ) {
				$order_ids[] = $order_id;
			}
		}

		$product_ids  = array_unique( array_filter( $product_ids ) );
		$customer_ids = array_unique( array_filter( $customer_ids ) );
		$order_ids    = array_unique( array_filter( $order_ids ) );

		if ( [] !== $product_ids ) {
			_prime_post_caches( $product_ids );
			// Prime product meta caches to avoid individual meta queries
			update_meta_cache( 'post', $product_ids );
		}

		if ( [] !== $customer_ids ) {
			// Prime user caches.
			new WP_User_Query(
			    [
					'include' => $customer_ids,
				],
			);
		}

		if ( [] !== $order_ids ) {
			// Prime order caches to avoid loading orders individually in get_customer()
			_prime_post_caches( $order_ids );
			update_meta_cache( 'post', $order_ids );
		}

		return $objects;
	}

	/**
	 * Prepare a single appointment output for response.
	 *
	 * Extends parent to include cal_color from product and customer name fields
	 * to eliminate the need for separate products and users API calls on initial load.
	 *
	 * @param WC_Appointment  $object  Object data.
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response
	 */
	public function prepare_object_for_response( $object, $request ): WP_REST_Response {
		// Get parent response.
		$response = parent::prepare_object_for_response( $object, $request );
		$data     = $response->get_data();

		// Add cal_color and product_title from product.
		// These fields are included to optimize admin calendar performance by eliminating
		// the need for separate products API calls on initial load and when opening modal.
		$product_id = $object->get_product_id( 'view' );
		if ( 0 < $product_id ) {
			// Use cached product from primed cache to avoid database queries
			$product = wc_get_product( $product_id );
			if ( $product && is_a( $product, 'WC_Product_Appointment' ) && method_exists( $product, 'get_cal_color' ) ) {
				$cal_color = $product->get_cal_color( 'view' );
				// Use default color if empty to ensure consistent calendar display.
				if ( empty( $cal_color ) ) {
					$cal_color = 'var(--wp-admin-theme-color, #0073aa)';
				}
				$data['cal_color']     = $cal_color;
				$data['product_title'] = $product->get_title();
			} else {
				$data['cal_color']     = '';
				$data['product_title'] = '';
			}
		} else {
			$data['cal_color']     = '';
			$data['product_title'] = '';
		}

		// Add customer name fields - optimized to avoid get_customer() which does expensive queries.
		// These fields are included to optimize admin calendar performance by eliminating
		// the need for separate users API calls on initial load. Customer information is
		// retrieved from the user account (if customer_id exists) or from order billing
		// information (if order_id exists).
		$customer_id = $object->get_customer_id( 'view' );
		$order_id    = $object->get_order_id( 'view' );

		$first_name = '';
		$last_name  = '';
		$full_name  = '';
		$name       = '';

		// Try to get customer info from user first (using cached user data)
		if ( 0 < $customer_id ) {
			// Use cached user data instead of get_user_by() which does a database query
			$user = get_userdata( $customer_id );
			if ( $user ) {
				$first_name = $user->user_firstname;
				$last_name  = $user->user_lastname;
				$full_name  = trim( $first_name . ' ' . $last_name );
				$name       = $user->display_name;
			}
		}

		// Fallback to order billing info if user data not available
		if ( ( empty( $first_name ) && empty( $last_name ) ) && 0 < $order_id ) {
			// Use cached order from primed cache
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$order_first_name = $order->get_billing_first_name();
				$order_last_name  = $order->get_billing_last_name();

				if ( $order_first_name || $order_last_name ) {
					$first_name = $order_first_name ?: $order->get_shipping_first_name();
					$last_name  = $order_last_name ?: $order->get_shipping_last_name();
					$full_name  = trim( $first_name . ' ' . $last_name );
					$name       = $full_name;

					// Add (Guest) suffix if no customer ID
					if ( ! $customer_id || 0 === absint( $order->get_customer_id() ) ) {
						/* translators: %s: Guest name */
						$full_name = sprintf( _x( '%s (Guest)', 'Guest string with name from appointment order in brackets', 'woocommerce-appointments' ), $full_name );
						$name      = $full_name;
					}
				}
			}
		}

		// Set defaults if still empty
		if ( empty( $name ) ) {
			$name = __( 'Guest', 'woocommerce-appointments' );
		}
		if ( '' === $full_name || '0' === $full_name ) {
			$full_name = __( 'Guest', 'woocommerce-appointments' );
		}

		$data['customer_first_name'] = $first_name;
		$data['customer_last_name']  = $last_name;
		$data['customer_full_name']  = $full_name;
		$data['customer_name']       = $name;

		// Add customer_avatar - use gravatar from user email or order billing email
		$customer_email = '';
		if ( 0 < $customer_id ) {
			$user = get_userdata( $customer_id );
			if ( $user ) {
				$customer_email = $user->user_email;
			}
		}
		if ( empty( $customer_email ) && 0 < $order_id ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$customer_email = $order->get_billing_email();
			}
		}
		$data['customer_avatar'] = ! empty( $customer_email ) ? get_avatar_url( $customer_email, [ 'size' => 48 ] ) : '';
		
		// Add customer email and phone for contact icons
		$customer_phone = '';
		if ( 0 < $customer_id ) {
			$user = get_userdata( $customer_id );
			if ( $user ) {
				$customer_phone = $user->user_phone;
			}
		}
		if ( empty( $customer_phone ) && 0 < $order_id ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$customer_phone = $order->get_billing_phone();
			}
		}
		$data['customer_email'] = $customer_email;
		$data['customer_phone'] = $customer_phone;

		// Add staff_name and staff_avatar
		$staff_ids = $object->get_staff_ids( 'view' );
		$staff_id  = ! empty( $staff_ids ) ? absint( $staff_ids[0] ) : 0;
		if ( 0 < $staff_id ) {
			$staff_user = get_userdata( $staff_id );
			if ( $staff_user ) {
				$data['staff_name']   = $staff_user->display_name;
				$data['staff_avatar'] = get_avatar_url( $staff_user->user_email, [ 'size' => 48 ] );
			} else {
				$data['staff_name']   = '';
				$data['staff_avatar'] = '';
			}
		} else {
			$data['staff_name']   = '';
			$data['staff_avatar'] = '';
		}

		// Add order_info for modal display
		if ( 0 < $order_id ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$billing              = $order->get_address( 'billing' );

				// Format line items for addons display
				$line_items = [];
				foreach ( $order->get_items() as $item_id => $item ) {
					$meta_data = [];
					foreach ( $item->get_formatted_meta_data() as $meta_key => $meta ) {
						$meta_data[] = [
							'key'           => $meta->key,
							'value'         => $meta->value,
							'display_key'   => $meta->display_key,
							'display_value' => strip_tags( $meta->display_value ), // Strip tags for clean display
						];
					}
					$line_items[] = [
						'id'        => $item_id,
						'name'      => $item->get_name(),
						'sku'       => $item->get_product() ? $item->get_product()->get_sku() : '',
						'product_id'=> $item->get_product_id(),
						'quantity'  => $item->get_quantity(),
						'subtotal'  => $item->get_subtotal(),
						'total'     => $item->get_total(),
						'meta_data' => $meta_data,
					];
				}

				$data['order_info'] = [
					'id'             => $order_id,
					'currency'       => $order->get_currency(),
					'total'          => $order->get_total(),
					'discount_total' => $order->get_discount_total(),
					'billing'        => [
						'first_name' => $billing['first_name'] ?? '',
						'last_name'  => $billing['last_name'] ?? '',
						'email'      => $billing['email'] ?? '',
						'phone'      => $billing['phone'] ?? '',
					],
					'line_items'     => $line_items,
				];
			} else {
				$data['order_info'] = null;
			}
		} else {
			$data['order_info'] = null;
		}

		$response->set_data( $data );
		return $response;
	}

	/**
	 * Get item schema.
	 *
	 * Extends parent schema to document the additional fields.
	 *
	 * @return array
	 */
	public function get_item_schema(): array {
		$schema = parent::get_item_schema();

		if ( ! isset( $schema['properties'] ) ) {
			$schema['properties'] = [];
		}

		// Add cal_color field documentation.
		$schema['properties']['cal_color'] = [
			'description' => 'Calendar color for the appointment product. Used for visual representation in admin calendar.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		// Add customer name fields documentation.
		$schema['properties']['customer_first_name'] = [
			'description' => 'Customer first name. Retrieved from user or order billing information.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		$schema['properties']['customer_last_name'] = [
			'description' => 'Customer last name. Retrieved from user or order billing information.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		$schema['properties']['customer_full_name'] = [
			'description' => 'Customer full name (first name + last name). Retrieved from user or order billing information.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		$schema['properties']['customer_name'] = [
			'description' => 'Customer display name. Retrieved from user display_name or order billing information.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		// Add product_title field documentation.
		$schema['properties']['product_title'] = [
			'description' => 'Product title for the appointment. Used for modal display.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		// Add customer_avatar field documentation.
		$schema['properties']['customer_avatar'] = [
			'description' => 'Customer avatar URL from gravatar.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		// Add staff_name field documentation.
		$schema['properties']['staff_name'] = [
			'description' => 'Staff member display name.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		// Add staff_avatar field documentation.
		$schema['properties']['staff_avatar'] = [
			'description' => 'Staff member avatar URL from gravatar.',
			'type'        => 'string',
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
		];

		// Add order_info field documentation.
		$schema['properties']['order_info'] = [
			'description' => 'Order details for modal display.',
			'type'        => [ 'object', 'null' ],
			'context'     => [ 'view', 'edit' ],
			'readonly'    => true,
			'properties'  => [
				'id'             => [ 'type' => 'integer' ],
				'currency'       => [ 'type' => 'string' ],
				'total'          => [ 'type' => 'string' ],
				'discount_total' => [ 'type' => 'string' ],
				'billing'        => [
					'type'       => 'object',
					'properties' => [
						'first_name' => [ 'type' => 'string' ],
						'last_name'  => [ 'type' => 'string' ],
						'email'      => [ 'type' => 'string' ],
						'phone'      => [ 'type' => 'string' ],
					],
				],
				'line_items'     => [
					'type'  => 'array',
					'items' => [
						'type'       => 'object',
						'properties' => [
							'id'         => [ 'type' => 'integer' ],
							'name'       => [ 'type' => 'string' ],
							'sku'        => [ 'type' => 'string' ],
							'product_id' => [ 'type' => 'integer' ],
							'quantity'   => [ 'type' => 'integer' ],
							'subtotal'   => [ 'type' => 'string' ],
							'total'      => [ 'type' => 'string' ],
							'meta_data'  => [
								'type'  => 'array',
								'items' => [
									'type'       => 'object',
									'properties' => [
										'key'           => [ 'type' => 'string' ],
										'value'         => [ 'type' => 'string' ],
										'display_key'   => [ 'type' => 'string' ],
										'display_value' => [ 'type' => 'string' ],
									],
								],
							],
						],
					],
				],
			],
		];

		return $schema;
	}
}
