<?php
/**
 * Class Management Class
 *
 * Handles CRUD operations for karate classes
 *
 * @package    Karate_Club_Manager
 * @subpackage Karate_Club_Manager/includes/classes
 * @since      1.0.0
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class MACM_Class
 *
 * Manages karate class schedules, enrollment, and attendance
 *
 * @since 1.0.0
 */
class MACM_Class {

	/**
	 * Create a new class
	 *
	 * @since 1.0.0
	 * @param array $data Class data.
	 * @return int|WP_Error Class ID on success, WP_Error on failure.
	 */
	public static function create( $data ) {
		global $wpdb;

		// Validate required fields.
		if ( empty( $data['name'] ) ) {
			return new WP_Error( 'missing_name', __( 'Class name is required.', 'martial-arts-club-manager' ) );
		}

		if ( ! isset( $data['location_id'] ) || empty( $data['location_id'] ) ) {
			return new WP_Error( 'missing_location', __( 'Location is required.', 'martial-arts-club-manager' ) );
		}

		if ( ! isset( $data['day_of_week'] ) ) {
			return new WP_Error( 'missing_day', __( 'Day of week is required.', 'martial-arts-club-manager' ) );
		}

		if ( empty( $data['start_time'] ) ) {
			return new WP_Error( 'missing_start_time', __( 'Start time is required.', 'martial-arts-club-manager' ) );
		}

		if ( empty( $data['end_time'] ) ) {
			return new WP_Error( 'missing_end_time', __( 'End time is required.', 'martial-arts-club-manager' ) );
		}

		// Validate name length.
		if ( strlen( $data['name'] ) > 100 ) {
			return new WP_Error( 'name_too_long', __( 'Class name must be less than 100 characters.', 'martial-arts-club-manager' ) );
		}

		// Validate day of week.
		$day = (int) $data['day_of_week'];
		if ( $day < 0 || $day > 6 ) {
			return new WP_Error( 'invalid_day', __( 'Day of week must be between 0 (Sunday) and 6 (Saturday).', 'martial-arts-club-manager' ) );
		}

		// Validate location exists.
		$location = MACM_Location::get( $data['location_id'] );
		if ( ! $location ) {
			return new WP_Error( 'invalid_location', __( 'Invalid location ID.', 'martial-arts-club-manager' ) );
		}

		// Validate time format.
		if ( ! preg_match( '/^([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/', $data['start_time'] ) ) {
			return new WP_Error( 'invalid_start_time', __( 'Invalid start time format. Use HH:MM:SS.', 'martial-arts-club-manager' ) );
		}

		if ( ! preg_match( '/^([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/', $data['end_time'] ) ) {
			return new WP_Error( 'invalid_end_time', __( 'Invalid end time format. Use HH:MM:SS.', 'martial-arts-club-manager' ) );
		}

		// Validate end time is after start time.
		if ( strtotime( $data['end_time'] ) <= strtotime( $data['start_time'] ) ) {
			return new WP_Error( 'invalid_time_range', __( 'End time must be after start time.', 'martial-arts-club-manager' ) );
		}

		// Prepare data for insertion.
		$insert_data = array(
			'class_name'   => sanitize_text_field( $data['name'] ),
			'description'  => isset( $data['description'] ) && ! empty( $data['description'] ) ? sanitize_textarea_field( $data['description'] ) : null,
			'location_id'  => (int) $data['location_id'],
			'day_of_week'  => (int) $data['day_of_week'],
			'start_time'   => sanitize_text_field( $data['start_time'] ),
			'end_time'     => sanitize_text_field( $data['end_time'] ),
			'max_capacity' => isset( $data['max_capacity'] ) && ! empty( $data['max_capacity'] ) ? (int) $data['max_capacity'] : null,
			'sort_order'   => isset( $data['sort_order'] ) ? (int) $data['sort_order'] : 0,
			'is_archived'  => 0,
			'created_at'   => current_time( 'mysql' ),
		);

		$table_name = $wpdb->prefix . 'macm_classes';

		// Insert into database.
		$inserted = $wpdb->insert(
			$table_name,
			$insert_data,
			array( '%s', '%s', '%d', '%d', '%s', '%s', '%d', '%d', '%d', '%s' )
		);

		if ( false === $inserted ) {
			return new WP_Error( 'db_error', __( 'Failed to create class.', 'martial-arts-club-manager' ) );
		}

		$class_id = $wpdb->insert_id;

		// Fire action hook.
		do_action( 'macm_class_created', $class_id, $insert_data );

		return $class_id;
	}

	/**
	 * Get class by ID
	 *
	 * @since 1.0.0
	 * @param int $class_id Class ID.
	 * @return object|false Class object on success, false on failure.
	 */
	public static function get( $class_id ) {
		global $wpdb;

		$table_name = $wpdb->prefix . 'macm_classes';

		$class = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE id = %d',
				$table_name,
				$class_id
			)
		);

		return $class ? $class : false;
	}

	/**
	 * Get all classes with optional filtering
	 *
	 * @since 1.0.0
	 * @param array $args Query arguments.
	 * @return array Array of class objects.
	 */
	public static function get_all( $args = array() ) {
		global $wpdb;

		$defaults = array(
			'day_of_week' => null,
			'location_id' => null,
			'is_archived' => 0,
			'orderby'     => 'day_of_week',
			'order'       => 'ASC',
		);

		$args = wp_parse_args( $args, $defaults );

		$table_name = $wpdb->prefix . 'macm_classes';

		// Validate orderby and order against whitelists.
		$allowed_orderby = array( 'id', 'class_name', 'day_of_week', 'start_time', 'sort_order', 'created_at' );
		$orderby         = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'day_of_week';
		$order           = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';

		// Determine which filters are active.
		$has_day      = null !== $args['day_of_week'];
		$has_location = null !== $args['location_id'];
		$has_archived = null !== $args['is_archived'];

		// Execute query using helper method with literal SQL strings.
		// This approach satisfies WordPress Plugin Check static analysis requirements.
		$classes = self::execute_get_all_query(
			$wpdb,
			$table_name,
			$has_day,
			$has_location,
			$has_archived,
			$args['day_of_week'],
			$args['location_id'],
			$args['is_archived'],
			$orderby,
			$order
		);

		return $classes ? $classes : array();
	}

	/**
	 * Execute get_all query with literal SQL strings.
	 *
	 * Uses conditional execution with literal SQL strings to satisfy WordPress Plugin Check
	 * static analysis requirements. Each query uses a literal string in prepare().
	 *
	 * @since 1.0.290
	 * @param wpdb     $wpdb          Database object.
	 * @param string   $table_name    Table name.
	 * @param bool     $has_day       Whether day_of_week filter is set.
	 * @param bool     $has_location  Whether location_id filter is set.
	 * @param bool     $has_archived  Whether is_archived filter is set.
	 * @param int|null $day_of_week   Day of week value.
	 * @param int|null $location_id   Location ID value.
	 * @param int|null $is_archived   Is archived value.
	 * @param string   $orderby       Validated orderby column.
	 * @param string   $order         Validated order direction (ASC/DESC).
	 * @return array Array of class objects.
	 */
	private static function execute_get_all_query( $wpdb, $table_name, $has_day, $has_location, $has_archived, $day_of_week, $location_id, $is_archived, $orderby, $order ) {
		// Build filter key for conditional branching.
		$filter_key = ( $has_day ? 'd' : '' ) . ( $has_location ? 'l' : '' ) . ( $has_archived ? 'a' : '' );
		$order_key  = $orderby . '_' . $order;

		// Execute query based on filter combination and order.
		// Each branch uses literal SQL strings to satisfy static analysis.
		// Filter combinations: dla, dl, da, la, d, l, a, (none).

		switch ( $filter_key ) {
			case 'dla': // Day + location + archived.
				return self::query_classes_dla( $wpdb, $table_name, $day_of_week, $location_id, $is_archived, $order_key );
			case 'dl':  // Day + location.
				return self::query_classes_dl( $wpdb, $table_name, $day_of_week, $location_id, $order_key );
			case 'da':  // Day + archived.
				return self::query_classes_da( $wpdb, $table_name, $day_of_week, $is_archived, $order_key );
			case 'la':  // Location + archived.
				return self::query_classes_la( $wpdb, $table_name, $location_id, $is_archived, $order_key );
			case 'd':   // Day only.
				return self::query_classes_d( $wpdb, $table_name, $day_of_week, $order_key );
			case 'l':   // Location only.
				return self::query_classes_l( $wpdb, $table_name, $location_id, $order_key );
			case 'a':   // Archived only.
				return self::query_classes_a( $wpdb, $table_name, $is_archived, $order_key );
			default:    // No filters.
				return self::query_classes_none( $wpdb, $table_name, $order_key );
		}
	}

	/**
	 * Query classes with day + location + archived filters.
	 *
	 * @param wpdb   $wpdb        Database object.
	 * @param string $table_name  Table name.
	 * @param int    $day         Day of week.
	 * @param int    $location    Location ID.
	 * @param int    $archived    Is archived.
	 * @param string $order_key   Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_dla( $wpdb, $table_name, $day, $location, $archived, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY id ASC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY id DESC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY class_name ASC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY class_name DESC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY start_time ASC', $table_name, $day, $location, $archived ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY start_time DESC', $table_name, $day, $location, $archived ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY created_at ASC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY created_at DESC, start_time ASC', $table_name, $day, $location, $archived ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $day, $location, $archived ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d AND is_archived = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $day, $location, $archived ) );
		}
	}

	/**
	 * Query classes with day + location filters.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param int    $day        Day of week.
	 * @param int    $location   Location ID.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_dl( $wpdb, $table_name, $day, $location, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY id ASC, start_time ASC', $table_name, $day, $location ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY id DESC, start_time ASC', $table_name, $day, $location ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY class_name ASC, start_time ASC', $table_name, $day, $location ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY class_name DESC, start_time ASC', $table_name, $day, $location ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY start_time ASC', $table_name, $day, $location ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY start_time DESC', $table_name, $day, $location ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $day, $location ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $day, $location ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY created_at ASC, start_time ASC', $table_name, $day, $location ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY created_at DESC, start_time ASC', $table_name, $day, $location ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $day, $location ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND location_id = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $day, $location ) );
		}
	}

	/**
	 * Query classes with day + archived filters.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param int    $day        Day of week.
	 * @param int    $archived   Is archived.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_da( $wpdb, $table_name, $day, $archived, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY id ASC, start_time ASC', $table_name, $day, $archived ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY id DESC, start_time ASC', $table_name, $day, $archived ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY class_name ASC, start_time ASC', $table_name, $day, $archived ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY class_name DESC, start_time ASC', $table_name, $day, $archived ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY start_time ASC', $table_name, $day, $archived ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY start_time DESC', $table_name, $day, $archived ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $day, $archived ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $day, $archived ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY created_at ASC, start_time ASC', $table_name, $day, $archived ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY created_at DESC, start_time ASC', $table_name, $day, $archived ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $day, $archived ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d AND is_archived = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $day, $archived ) );
		}
	}

	/**
	 * Query classes with location + archived filters.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param int    $location   Location ID.
	 * @param int    $archived   Is archived.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_la( $wpdb, $table_name, $location, $archived, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY id ASC, start_time ASC', $table_name, $location, $archived ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY id DESC, start_time ASC', $table_name, $location, $archived ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY class_name ASC, start_time ASC', $table_name, $location, $archived ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY class_name DESC, start_time ASC', $table_name, $location, $archived ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY start_time ASC', $table_name, $location, $archived ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY start_time DESC', $table_name, $location, $archived ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $location, $archived ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $location, $archived ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY created_at ASC, start_time ASC', $table_name, $location, $archived ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY created_at DESC, start_time ASC', $table_name, $location, $archived ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $location, $archived ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d AND is_archived = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $location, $archived ) );
		}
	}

	/**
	 * Query classes with day filter only.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param int    $day        Day of week.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_d( $wpdb, $table_name, $day, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY id ASC, start_time ASC', $table_name, $day ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY id DESC, start_time ASC', $table_name, $day ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY class_name ASC, start_time ASC', $table_name, $day ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY class_name DESC, start_time ASC', $table_name, $day ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY start_time ASC', $table_name, $day ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY start_time DESC', $table_name, $day ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $day ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $day ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY created_at ASC, start_time ASC', $table_name, $day ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY created_at DESC, start_time ASC', $table_name, $day ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $day ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE day_of_week = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $day ) );
		}
	}

	/**
	 * Query classes with location filter only.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param int    $location   Location ID.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_l( $wpdb, $table_name, $location, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY id ASC, start_time ASC', $table_name, $location ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY id DESC, start_time ASC', $table_name, $location ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY class_name ASC, start_time ASC', $table_name, $location ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY class_name DESC, start_time ASC', $table_name, $location ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY start_time ASC', $table_name, $location ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY start_time DESC', $table_name, $location ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $location ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $location ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY created_at ASC, start_time ASC', $table_name, $location ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY created_at DESC, start_time ASC', $table_name, $location ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $location ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE location_id = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $location ) );
		}
	}

	/**
	 * Query classes with archived filter only.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param int    $archived   Is archived.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_a( $wpdb, $table_name, $archived, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY id ASC, start_time ASC', $table_name, $archived ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY id DESC, start_time ASC', $table_name, $archived ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY class_name ASC, start_time ASC', $table_name, $archived ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY class_name DESC, start_time ASC', $table_name, $archived ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY start_time ASC', $table_name, $archived ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY start_time DESC', $table_name, $archived ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY sort_order ASC, start_time ASC', $table_name, $archived ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY sort_order DESC, start_time ASC', $table_name, $archived ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY created_at ASC, start_time ASC', $table_name, $archived ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY created_at DESC, start_time ASC', $table_name, $archived ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY day_of_week DESC, start_time ASC', $table_name, $archived ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE is_archived = %d ORDER BY day_of_week ASC, start_time ASC', $table_name, $archived ) );
		}
	}

	/**
	 * Query classes with no filters.
	 *
	 * @param wpdb   $wpdb       Database object.
	 * @param string $table_name Table name.
	 * @param string $order_key  Order key (orderby_order).
	 * @return array Results.
	 */
	private static function query_classes_none( $wpdb, $table_name, $order_key ) {
		switch ( $order_key ) {
			case 'id_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id ASC, start_time ASC', $table_name ) );
			case 'id_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id DESC, start_time ASC', $table_name ) );
			case 'class_name_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY class_name ASC, start_time ASC', $table_name ) );
			case 'class_name_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY class_name DESC, start_time ASC', $table_name ) );
			case 'start_time_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY start_time ASC', $table_name ) );
			case 'start_time_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY start_time DESC', $table_name ) );
			case 'sort_order_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY sort_order ASC, start_time ASC', $table_name ) );
			case 'sort_order_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY sort_order DESC, start_time ASC', $table_name ) );
			case 'created_at_ASC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY created_at ASC, start_time ASC', $table_name ) );
			case 'created_at_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY created_at DESC, start_time ASC', $table_name ) );
			case 'day_of_week_DESC':
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY day_of_week DESC, start_time ASC', $table_name ) );
			default: // day_of_week_ASC.
				return $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY day_of_week ASC, start_time ASC', $table_name ) );
		}
	}

	/**
	 * Update class
	 *
	 * @since 1.0.0
	 * @param int   $class_id Class ID.
	 * @param array $data Class data to update.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function update( $class_id, $data ) {
		global $wpdb;

		// Check if class exists.
		$class = self::get( $class_id );
		if ( ! $class ) {
			return new WP_Error( 'not_found', __( 'Class not found.', 'martial-arts-club-manager' ) );
		}

		// Validate fields if provided.
		if ( isset( $data['name'] ) && empty( $data['name'] ) ) {
			return new WP_Error( 'missing_name', __( 'Class name is required.', 'martial-arts-club-manager' ) );
		}

		if ( isset( $data['day_of_week'] ) ) {
			$day = (int) $data['day_of_week'];
			if ( $day < 0 || $day > 6 ) {
				return new WP_Error( 'invalid_day', __( 'Day of week must be between 0 and 6.', 'martial-arts-club-manager' ) );
			}
		}

		if ( isset( $data['location_id'] ) ) {
			$location = MACM_Location::get( $data['location_id'] );
			if ( ! $location ) {
				return new WP_Error( 'invalid_location', __( 'Invalid location ID.', 'martial-arts-club-manager' ) );
			}
		}

		// Prepare data for update.
		$update_data = array();
		$format      = array();

		if ( isset( $data['name'] ) ) {
			$update_data['class_name'] = sanitize_text_field( $data['name'] );
			$format[]                  = '%s';
		}

		if ( isset( $data['location_id'] ) ) {
			$update_data['location_id'] = (int) $data['location_id'];
			$format[]                   = '%d';
		}

		if ( isset( $data['day_of_week'] ) ) {
			$update_data['day_of_week'] = (int) $data['day_of_week'];
			$format[]                   = '%d';
		}

		if ( isset( $data['start_time'] ) ) {
			$update_data['start_time'] = sanitize_text_field( $data['start_time'] );
			$format[]                  = '%s';
		}

		if ( isset( $data['end_time'] ) ) {
			$update_data['end_time'] = sanitize_text_field( $data['end_time'] );
			$format[]                = '%s';
		}

		if ( isset( $data['description'] ) ) {
			$update_data['description'] = ! empty( $data['description'] ) ? sanitize_textarea_field( $data['description'] ) : null;
			$format[]                   = '%s';
		}

		if ( isset( $data['max_capacity'] ) ) {
			$update_data['max_capacity'] = ! empty( $data['max_capacity'] ) ? (int) $data['max_capacity'] : null;
			$format[]                    = '%d';
		}

		if ( isset( $data['sort_order'] ) ) {
			$update_data['sort_order'] = (int) $data['sort_order'];
			$format[]                  = '%d';
		}

		if ( empty( $update_data ) ) {
			return new WP_Error( 'no_data', __( 'No data to update.', 'martial-arts-club-manager' ) );
		}

		$table_name = $wpdb->prefix . 'macm_classes';

		// Update database.
		$updated = $wpdb->update(
			$table_name,
			$update_data,
			array( 'id' => $class_id ),
			$format,
			array( '%d' )
		);

		if ( false === $updated ) {
			return new WP_Error( 'db_error', __( 'Failed to update class.', 'martial-arts-club-manager' ) );
		}

		// Fire action hook.
		do_action( 'macm_class_updated', $class_id, $update_data );

		return true;
	}

	/**
	 * Archive a class (soft delete)
	 *
	 * @since 1.0.0
	 * @param int $class_id Class ID.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function archive( $class_id ) {
		global $wpdb;

		$class = self::get( $class_id );
		if ( ! $class ) {
			return new WP_Error( 'not_found', __( 'Class not found.', 'martial-arts-club-manager' ) );
		}

		$table_name = $wpdb->prefix . 'macm_classes';

		$updated = $wpdb->update(
			$table_name,
			array( 'is_archived' => 1 ),
			array( 'id' => $class_id ),
			array( '%d' ),
			array( '%d' )
		);

		if ( false === $updated ) {
			return new WP_Error( 'db_error', __( 'Failed to archive class.', 'martial-arts-club-manager' ) );
		}

		do_action( 'macm_class_archived', $class_id );

		return true;
	}

	/**
	 * Unarchive a class
	 *
	 * @since 1.0.0
	 * @param int $class_id Class ID.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function unarchive( $class_id ) {
		global $wpdb;

		$class = self::get( $class_id );
		if ( ! $class ) {
			return new WP_Error( 'not_found', __( 'Class not found.', 'martial-arts-club-manager' ) );
		}

		$table_name = $wpdb->prefix . 'macm_classes';

		$updated = $wpdb->update(
			$table_name,
			array( 'is_archived' => 0 ),
			array( 'id' => $class_id ),
			array( '%d' ),
			array( '%d' )
		);

		if ( false === $updated ) {
			return new WP_Error( 'db_error', __( 'Failed to unarchive class.', 'martial-arts-club-manager' ) );
		}

		do_action( 'macm_class_unarchived', $class_id );

		return true;
	}

	/**
	 * Permanently delete a class
	 *
	 * @since 1.0.0
	 * @param int $class_id Class ID.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function delete( $class_id ) {
		global $wpdb;

		$class = self::get( $class_id );
		if ( ! $class ) {
			return new WP_Error( 'not_found', __( 'Class not found.', 'martial-arts-club-manager' ) );
		}

		// Delete related enrollments.
		$enrollments_table = $wpdb->prefix . 'macm_class_enrollments';
		$wpdb->delete(
			$enrollments_table,
			array( 'class_id' => $class_id ),
			array( '%d' )
		);

		// Delete related attendance records.
		$attendance_table = $wpdb->prefix . 'macm_attendance';
		$wpdb->delete(
			$attendance_table,
			array( 'class_id' => $class_id ),
			array( '%d' )
		);

		// Delete the class.
		$table_name = $wpdb->prefix . 'macm_classes';
		$deleted    = $wpdb->delete(
			$table_name,
			array( 'id' => $class_id ),
			array( '%d' )
		);

		if ( false === $deleted ) {
			return new WP_Error( 'db_error', __( 'Failed to delete class.', 'martial-arts-club-manager' ) );
		}

		do_action( 'macm_class_deleted', $class_id );

		return true;
	}

	/**
	 * Get enrolled members for a class
	 *
	 * @since 1.0.0
	 * @param int  $class_id Class ID.
	 * @param bool $active_only Whether to return only active enrollments and active members.
	 * @return array Array of enrollment objects with member data.
	 */
	public static function get_enrolled_members( $class_id, $active_only = true ) {
		global $wpdb;

		$enrollments_table = $wpdb->prefix . 'macm_class_enrollments';
		$members_table     = $wpdb->prefix . 'macm_members';

		if ( $active_only ) {
			$enrollments = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT e.*, m.full_name, m.belt_color, m.group_id
					FROM %i e
					LEFT JOIN %i m ON e.member_id = m.id
					WHERE e.class_id = %d AND e.removed_at IS NULL AND m.status = 'active'
					ORDER BY e.enrolled_at DESC",
					$enrollments_table,
					$members_table,
					$class_id
				)
			);
		} else {
			$enrollments = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT e.*, m.full_name, m.belt_color, m.group_id
					FROM %i e
					LEFT JOIN %i m ON e.member_id = m.id
					WHERE e.class_id = %d
					ORDER BY e.enrolled_at DESC',
					$enrollments_table,
					$members_table,
					$class_id
				)
			);
		}

		return $enrollments ? $enrollments : array();
	}

	/**
	 * Enroll members in a class
	 *
	 * @since 1.0.0
	 * @param int   $class_id Class ID.
	 * @param array $member_ids Array of member IDs to enroll.
	 * @return int|WP_Error Number of members enrolled, WP_Error on failure.
	 */
	public static function enroll_members( $class_id, $member_ids ) {
		global $wpdb;

		// Validate class exists.
		$class = self::get( $class_id );
		if ( ! $class ) {
			return new WP_Error( 'invalid_class', __( 'Invalid class ID.', 'martial-arts-club-manager' ) );
		}

		if ( empty( $member_ids ) || ! is_array( $member_ids ) ) {
			return new WP_Error( 'no_members', __( 'No members provided.', 'martial-arts-club-manager' ) );
		}

		$enrollments_table = $wpdb->prefix . 'macm_class_enrollments';
		$enrolled_count    = 0;

		foreach ( $member_ids as $member_id ) {
			$member_id = (int) $member_id;

			// Check if member exists.
			$member = MACM_Member::get( $member_id );
			if ( ! $member ) {
				continue;
			}

			// Check if already enrolled and active.
			$existing = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT * FROM %i
                     WHERE class_id = %d AND member_id = %d AND removed_at IS NULL',
					$enrollments_table,
					$class_id,
					$member_id
				)
			);

			if ( $existing ) {
				continue; // Already enrolled.
			}

			// Enroll member.
			$inserted = $wpdb->insert(
				$enrollments_table,
				array(
					'class_id'    => $class_id,
					'member_id'   => $member_id,
					'enrolled_at' => current_time( 'mysql' ),
				),
				array( '%d', '%d', '%s' )
			);

			if ( $inserted ) {
				++$enrolled_count;
				do_action( 'macm_member_enrolled', $class_id, $member_id );
			}
		}

		return $enrolled_count;
	}

	/**
	 * Remove member from class
	 *
	 * @since 1.0.0
	 * @param int $class_id Class ID.
	 * @param int $member_id Member ID.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function remove_member( $class_id, $member_id ) {
		global $wpdb;

		$enrollments_table = $wpdb->prefix . 'macm_class_enrollments';

		// Set removed_at timestamp instead of deleting.
		$updated = $wpdb->update(
			$enrollments_table,
			array( 'removed_at' => current_time( 'mysql' ) ),
			array(
				'class_id'   => $class_id,
				'member_id'  => $member_id,
				'removed_at' => null,
			),
			array( '%s' ),
			array( '%d', '%d', '%s' )
		);

		if ( false === $updated ) {
			return new WP_Error( 'db_error', __( 'Failed to remove member from class.', 'martial-arts-club-manager' ) );
		}

		if ( 0 === $updated ) {
			return new WP_Error( 'not_enrolled', __( 'Member is not enrolled in this class.', 'martial-arts-club-manager' ) );
		}

		do_action( 'macm_member_removed', $class_id, $member_id );

		return true;
	}

	/**
	 * Get day name from day number
	 *
	 * @since 1.0.0
	 * @param int $day_number Day number (0-6).
	 * @return string Day name.
	 */
	public static function get_day_name( $day_number ) {
		$days = array(
			0 => __( 'Sunday', 'martial-arts-club-manager' ),
			1 => __( 'Monday', 'martial-arts-club-manager' ),
			2 => __( 'Tuesday', 'martial-arts-club-manager' ),
			3 => __( 'Wednesday', 'martial-arts-club-manager' ),
			4 => __( 'Thursday', 'martial-arts-club-manager' ),
			5 => __( 'Friday', 'martial-arts-club-manager' ),
			6 => __( 'Saturday', 'martial-arts-club-manager' ),
		);

		return isset( $days[ $day_number ] ) ? $days[ $day_number ] : '';
	}

	/**
	 * Format time range for display
	 *
	 * @since 1.0.0
	 * @param string $start_time Start time (HH:MM:SS).
	 * @param string $end_time End time (HH:MM:SS).
	 * @return string Formatted time range.
	 */
	public static function format_time_range( $start_time, $end_time ) {
		$start = wp_date( 'g:i A', strtotime( $start_time ) );
		$end   = wp_date( 'g:i A', strtotime( $end_time ) );

		return sprintf( '%s - %s', $start, $end );
	}

	/**
	 * Get enrollment count for a class
	 *
	 * @since 1.0.0
	 * @param int $class_id Class ID.
	 * @return int Enrollment count (only active members).
	 */
	public static function get_enrollment_count( $class_id ) {
		global $wpdb;

		$enrollments_table = $wpdb->prefix . 'macm_class_enrollments';
		$members_table     = $wpdb->prefix . 'macm_members';

		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM %i e
                 LEFT JOIN %i m ON e.member_id = m.id
                 WHERE e.class_id = %d AND e.removed_at IS NULL AND m.status = 'active'",
				$enrollments_table,
				$members_table,
				$class_id
			)
		);

		return (int) $count;
	}

	/**
	 * Build ORDER BY SQL clause from validated values.
	 *
	 * Returns literal SQL string based on pre-validated orderby and order parameters.
	 * Both parameters MUST be validated against whitelists before calling.
	 * Includes secondary sort by start_time ASC for consistent ordering.
	 *
	 * @since 1.0.289
	 * @param string $orderby Validated orderby column (id, class_name, day_of_week, start_time, sort_order, created_at).
	 * @param string $order   Validated order direction (ASC, DESC).
	 * @return string ORDER BY SQL clause.
	 */
	private static function build_order_sql( $orderby, $order ) {
		// Map validated values to literal SQL strings.
		// Each combination returns a complete ORDER BY clause with secondary sort.
		$order_map = array(
			'id_ASC'           => 'ORDER BY id ASC, start_time ASC',
			'id_DESC'          => 'ORDER BY id DESC, start_time ASC',
			'class_name_ASC'   => 'ORDER BY class_name ASC, start_time ASC',
			'class_name_DESC'  => 'ORDER BY class_name DESC, start_time ASC',
			'day_of_week_ASC'  => 'ORDER BY day_of_week ASC, start_time ASC',
			'day_of_week_DESC' => 'ORDER BY day_of_week DESC, start_time ASC',
			'start_time_ASC'   => 'ORDER BY start_time ASC',
			'start_time_DESC'  => 'ORDER BY start_time DESC',
			'sort_order_ASC'   => 'ORDER BY sort_order ASC, start_time ASC',
			'sort_order_DESC'  => 'ORDER BY sort_order DESC, start_time ASC',
			'created_at_ASC'   => 'ORDER BY created_at ASC, start_time ASC',
			'created_at_DESC'  => 'ORDER BY created_at DESC, start_time ASC',
		);

		$key = $orderby . '_' . $order;
		return isset( $order_map[ $key ] ) ? $order_map[ $key ] : 'ORDER BY day_of_week ASC, start_time ASC';
	}
}
