<?php
/**
 * Capacity-based group/class sessions for independent customers.
 *
 * This is deliberately a Local-mode feature. Square/Vagaro remain the source
 * of truth in their modes; a local class must never silently bypass them.
 */
if ( ! defined( 'ABSPATH' ) ) { exit;
}

class YATOON_Group_Appointments {
    const SCHEMA = '4.4.3-group-capacity-v1';

    public static function init() {
        self::install_schema();
        add_shortcode( 'yatoon_group_appointments', array( __CLASS__, 'shortcode' ) );
        add_action( 'wp_ajax_yatoon_book_group_session', array( __CLASS__, 'ajax_book' ) );
        add_action( 'wp_ajax_nopriv_yatoon_book_group_session', array( __CLASS__, 'ajax_book' ) );
        add_action( 'wp_ajax_yatoon_cancel_group_attendee', array( __CLASS__, 'ajax_cancel' ) );
        add_action( 'wp_ajax_nopriv_yatoon_cancel_group_attendee', array( __CLASS__, 'ajax_cancel' ) );
        add_action( 'admin_menu', array( __CLASS__, 'add_menu' ), 24 );
        add_action( 'admin_post_yatoon_save_group_session', array( __CLASS__, 'save_session' ) );
        add_action( 'admin_post_yatoon_cancel_group_session', array( __CLASS__, 'cancel_session' ) );
        if ( ! class_exists( 'YATOON_Feature_Modules' ) || YATOON_Feature_Modules::enabled( 'rest_api' ) ) {
            add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
        }
    }

    public static function install_schema() {
        if ( get_option( 'yatoon_group_appointments_schema', '' ) === self::SCHEMA ) { return;
        }
        global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset = $wpdb->get_charset_collate();
        dbDelta( "CREATE TABLE {$wpdb->prefix}yatoon_group_sessions (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            service_id bigint(20) NOT NULL,
            staff_id bigint(20) NOT NULL,
            location_id bigint(20) NOT NULL DEFAULT 0,
            title varchar(255) NOT NULL,
            start_time datetime NOT NULL,
            end_time datetime NOT NULL,
            booking_timezone varchar(100) DEFAULT '',
            capacity int(11) NOT NULL DEFAULT 1,
            price decimal(10,2) NOT NULL DEFAULT 0,
            status varchar(20) NOT NULL DEFAULT 'active',
            notes text DEFAULT NULL,
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            KEY upcoming (status,start_time),
            KEY staff_time (staff_id,start_time),
            KEY location_time (location_id,start_time)
        ) {$charset};" );
        dbDelta( "CREATE TABLE {$wpdb->prefix}yatoon_group_attendees (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            session_id bigint(20) NOT NULL,
            customer_name varchar(255) NOT NULL,
            customer_email varchar(255) NOT NULL,
            customer_phone varchar(60) DEFAULT '',
            seats int(11) NOT NULL DEFAULT 1,
            status varchar(20) NOT NULL DEFAULT 'confirmed',
            amount_due decimal(10,2) NOT NULL DEFAULT 0,
            payment_status varchar(30) NOT NULL DEFAULT 'unpaid',
            manage_token varchar(64) NOT NULL,
            notes text DEFAULT NULL,
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            updated_at datetime DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            UNIQUE KEY manage_token (manage_token),
            KEY session_status (session_id,status),
            KEY customer_email (customer_email)
        ) {$charset};" );
        if ( YATOON_Schema_Verifier::tables_exist( array( 'yatoon_group_sessions', 'yatoon_group_attendees' ) ) ) {
            update_option( 'yatoon_group_appointments_schema', self::SCHEMA, false );
        }
    }

    public static function seats_used( $session_id ) {
        global $wpdb;
        return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(seats),0) FROM {$wpdb->prefix}yatoon_group_attendees WHERE session_id=%d AND status IN ('pending','confirmed')", $session_id ) );
    }

    public static function availability( $session ) {
        return self::remaining_capacity( $session->capacity, self::seats_used( $session->id ) );
    }

    public static function remaining_capacity( $capacity, $used ) {
        return max( 0, max( 1, (int) $capacity ) - max( 0, (int) $used ) );
    }

    public static function staff_has_session_conflict( $staff_id, DateTimeInterface $start, DateTimeInterface $end, $exclude_session_id = 0 ) {
        if ( class_exists( 'YATOON_Feature_Modules' ) && ! YATOON_Feature_Modules::enabled( 'group_appointments' ) ) { return false;
        }
        global $wpdb;
        return (bool) $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM {$wpdb->prefix}yatoon_group_sessions WHERE staff_id=%d AND id<>%d AND status='active' AND start_time<%s AND end_time>%s LIMIT 1",
            absint( $staff_id ), absint( $exclude_session_id ), $end->format( 'Y-m-d H:i:s' ), $start->format( 'Y-m-d H:i:s' )
        ) );
    }

    /** Validate Local-mode hours, breaks, time off and external calendar busy periods. */
    private static function local_availability_error( $staff_id, DateTime $start, DateTime $end ) {
        if ( ! class_exists( 'YATOON_Business_Hours' ) ) { return false;
        }
        if ( $start->format( 'Y-m-d' ) !== $end->format( 'Y-m-d' ) ) { return new WP_Error( 'group_hours', __( 'A group session must start and finish on the same business day.', 'yatoon-booking-system' ) );
        }
        $day = YATOON_Business_Hours::get_day_hours( $start->format( 'Y-m-d' ) );
        if ( ! $day ) { return new WP_Error( 'group_hours', __( 'The business is closed on the selected day.', 'yatoon-booking-system' ) );
        }
        $open = new DateTime( $start->format( 'Y-m-d' ) . ' ' . ( $day['start'] ?? '09:00' ), $start->getTimezone() );
        $close = new DateTime( $start->format( 'Y-m-d' ) . ' ' . ( $day['end'] ?? '17:00' ), $start->getTimezone() );
        if ( $start < $open || $end > $close ) { return new WP_Error( 'group_hours', __( 'The session is outside business hours.', 'yatoon-booking-system' ) );
        }
        if ( ! empty( $day['break_start'] ) && ! empty( $day['break_end'] ) ) {
            $break_start = new DateTime( $start->format( 'Y-m-d' ) . ' ' . $day['break_start'], $start->getTimezone() );
            $break_end = new DateTime( $start->format( 'Y-m-d' ) . ' ' . $day['break_end'], $start->getTimezone() );
            if ( $start < $break_end && $end > $break_start ) { return new WP_Error( 'group_break', __( 'The session overlaps the configured business break.', 'yatoon-booking-system' ) );
            }
        }
        foreach ( YATOON_Business_Hours::get_staff_time_off_blocks( $staff_id, $start->getTimezone() ) as $block ) {
            if ( $start < $block['end'] && $end > $block['start'] ) { return new WP_Error( 'group_time_off', __( 'The staff member is unavailable during this time.', 'yatoon-booking-system' ) );
            }
        }
        if ( YATOON_Business_Hours::has_external_busy_conflict( $staff_id, $start, $end ) ) { return new WP_Error( 'group_external_busy', __( 'The staff member is busy in a connected calendar.', 'yatoon-booking-system' ) );
        }
        return false;
    }

    public static function book( $session_id, array $customer, $seats = 1 ) {
        global $wpdb;
        if ( 'local' !== sanitize_key( get_option( 'yatoon_active_platform', 'local' ) ) ) { return new WP_Error( 'group_local_only', __( 'Group capacity sessions are available in Local booking mode. Use the external platform for classes in Square/Vagaro mode.', 'yatoon-booking-system' ) );
        }
        $session_id = absint( $session_id );
$seats = max( 1, min( 20, absint( $seats ) ) );
        $name = sanitize_text_field( $customer['name'] ?? '' );
$email = sanitize_email( $customer['email'] ?? '' );
$phone = sanitize_text_field( $customer['phone'] ?? '' );
        if ( ! $session_id || ! $name || ! is_email( $email ) ) { return new WP_Error( 'invalid_attendee', __( 'Name and a valid email address are required.', 'yatoon-booking-system' ) );
        }
        $lock = 'yatoon_group_session_' . $session_id;
        if ( '1' !== (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, 8)', $lock ) ) ) { return new WP_Error( 'group_busy', __( 'Another customer is reserving the last place. Please try again.', 'yatoon-booking-system' ) );
        }
        try {
            $wpdb->query( 'START TRANSACTION' );
            $session = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_group_sessions WHERE id=%d FOR UPDATE", $session_id ) );
            try { $session_start = $session ? new DateTime( $session->start_time, new DateTimeZone( $session->booking_timezone ?: yatoon_timezone_string( $session->location_id ) ) ) : null;
} catch ( Exception $e ) { $session_start = null; }
            if ( ! $session || 'active' !== $session->status || ! $session_start || $session_start <= new DateTime( 'now', $session_start->getTimezone() ) ) { $wpdb->query( 'ROLLBACK' );
return new WP_Error( 'group_unavailable', __( 'This session is no longer available.', 'yatoon-booking-system' ) ); }
            $remaining = self::availability( $session );
            if ( $seats > $remaining ) {
                $wpdb->query( 'ROLLBACK' );
                /* translators: %d: number of seats still available. */
                return new WP_Error( 'group_capacity', sprintf( __( 'Only %d place(s) remain.', 'yatoon-booking-system' ), $remaining ) );
            }
            $token = wp_generate_password( 48, false, false );
            $inserted = $wpdb->insert( $wpdb->prefix . 'yatoon_group_attendees', array(
                'session_id' => $session_id, 'customer_name' => $name, 'customer_email' => $email, 'customer_phone' => $phone,
                'seats' => $seats, 'status' => 'confirmed', 'amount_due' => round( (float) $session->price * $seats, 2 ),
                'payment_status' => 'unpaid', 'manage_token' => $token, 'notes' => sanitize_textarea_field( $customer['notes'] ?? '' ),
                'created_at' => current_time( 'mysql' ), 'updated_at' => current_time( 'mysql' ),
            ) );
            if ( false === $inserted ) { $wpdb->query( 'ROLLBACK' );
return new WP_Error( 'group_insert', __( 'Reservation could not be saved.', 'yatoon-booking-system' ) ); }
            $attendee_id = (int) $wpdb->insert_id;
$wpdb->query( 'COMMIT' );
        YATOON_Activity_Log::record( 'group_attendee_created', __( 'Group session reservation created.', 'yatoon-booking-system' ), array( 'session_id' => $session_id, 'attendee_id' => $attendee_id, 'seats' => $seats ) );
            do_action( 'yatoon_group_attendee_created', $attendee_id, $session_id );
            $cancel_url = add_query_arg( 'yatoon_group_token', rawurlencode( $token ), home_url( '/' ) );
            if ( get_option( 'yatoon_send_confirmation_email', '1' ) === '1' ) {
                /* translators: %s: group session title. */
                $subject = sprintf( __( 'Reservation confirmed: %s', 'yatoon-booking-system' ), $session->title );
                /* translators: 1: customer name, 2: session title, 3: session date/time, 4: seat count, 5: management URL. */
                $message = sprintf( __( "Hi %1\$s,\n\nYour reservation for %2\$s is confirmed for %3\$s. Places: %4\$d.\n\nManage or cancel: %5\$s", 'yatoon-booking-system' ), $name, $session->title, $session->start_time, $seats, $cancel_url );
                wp_mail( $email, $subject, $message );
            }
            return array( 'attendee_id' => $attendee_id, 'manage_token' => $token, 'cancel_url' => $cancel_url, 'remaining' => $remaining - $seats );
        } catch ( Throwable $e ) {
            $wpdb->query( 'ROLLBACK' );
            return new WP_Error( 'group_exception', __( 'Reservation could not be completed.', 'yatoon-booking-system' ) );
        } finally {
            $wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) );
        }
    }

    public static function ajax_book() {
        check_ajax_referer( 'yatoon_group_booking', 'nonce' );
        if ( function_exists( 'yatoon_public_rate_limit' ) ) { yatoon_public_rate_limit( 'group_booking', 8, 10 );
        }
        $customer = array(
            'name' => sanitize_text_field( wp_unslash( $_POST['name'] ?? '' ) ),
            'email' => sanitize_email( wp_unslash( $_POST['email'] ?? '' ) ),
            'phone' => sanitize_text_field( wp_unslash( $_POST['phone'] ?? '' ) ),
            'notes' => sanitize_textarea_field( wp_unslash( $_POST['notes'] ?? '' ) ),
        );
        $result = self::book( absint( $_POST['session_id'] ?? 0 ), $customer, absint( $_POST['seats'] ?? 1 ) );
        if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => $result->get_error_message() ) );
        }
        wp_send_json_success( $result );
    }

    public static function ajax_cancel() {
        check_ajax_referer( 'yatoon_group_cancel', 'nonce' );
global $wpdb;
        $token = sanitize_text_field( wp_unslash( $_POST['token'] ?? '' ) );
        // yatoon-release-checks: expiry-bounded-elsewhere
        // 4.6.2: this looked the attendee up by manage_token alone, with no time
        // bound at all -- yatoon_group_attendees has no manage_token_expires
        // column, so a cancel link stayed live forever. Rather than add a column
        // and a migration for it, bound the token by the thing that actually
        // gives it meaning: the session it belongs to. Once a class has finished
        // there is nothing legitimate left to cancel, so the link dies with it.
        // start_time/end_time are local-time columns, hence current_time() and
        // not gmdate().
        $attendee = $wpdb->get_row( $wpdb->prepare(
            "SELECT a.* FROM {$wpdb->prefix}yatoon_group_attendees a
             INNER JOIN {$wpdb->prefix}yatoon_group_sessions s ON s.id = a.session_id
             WHERE a.manage_token = %s AND s.end_time >= %s
             LIMIT 1",
            $token,
            current_time( 'mysql' )
        ) );
        if ( ! $attendee || ! in_array( $attendee->status, array( 'pending','confirmed' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Reservation was not found or is already cancelled.', 'yatoon-booking-system' ) ) );
        }
        $wpdb->update( $wpdb->prefix . 'yatoon_group_attendees', array( 'status' => 'cancelled', 'updated_at' => current_time( 'mysql' ) ), array( 'id' => $attendee->id ) );
        YATOON_Activity_Log::record( 'group_attendee_cancelled', __( 'Group session reservation cancelled.', 'yatoon-booking-system' ), array( 'session_id' => $attendee->session_id, 'attendee_id' => $attendee->id ) );
        wp_send_json_success( array( 'message' => __( 'Reservation cancelled.', 'yatoon-booking-system' ) ) );
    }

    public static function shortcode( $atts ) {
        if ( 'local' !== sanitize_key( get_option( 'yatoon_active_platform', 'local' ) ) ) { return current_user_can( 'manage_options' ) ? '<p>' . esc_html__( 'Group sessions are Local-mode only; Square/Vagaro must remain the scheduling authority in their modes.', 'yatoon-booking-system' ) . '</p>' : '';
        }
        global $wpdb;
$atts = shortcode_atts( array( 'location' => 0, 'limit' => 30 ), $atts );
$location = absint( $atts['location'] );
$limit = max( 1, min( 100, absint( $atts['limit'] ) ) );
        $where = "g.status='active' AND g.start_time>%s";
$args = array( current_time( 'mysql' ) );
        if ( $location ) { $where .= ' AND g.location_id=%d';
$args[] = $location; }
        $args[] = $limit;
        $sessions = $wpdb->get_results( $wpdb->prepare( "SELECT g.*,s.name service_name,st.name staff_name,l.name location_name,(SELECT COALESCE(SUM(a.seats),0) FROM {$wpdb->prefix}yatoon_group_attendees a WHERE a.session_id=g.id AND a.status IN ('pending','confirmed')) seats_used FROM {$wpdb->prefix}yatoon_group_sessions g LEFT JOIN {$wpdb->prefix}yatoon_services s ON s.id=g.service_id LEFT JOIN {$wpdb->prefix}yatoon_staff st ON st.id=g.staff_id LEFT JOIN {$wpdb->prefix}yatoon_locations l ON l.id=g.location_id WHERE {$where} ORDER BY g.start_time LIMIT %d", $args ) );
        $manage_token = sanitize_text_field( wp_unslash( $_GET['yatoon_group_token'] ?? '' ) );
        ob_start(); ?>
        <div class="yatoon-group-sessions">
        <?php if ( $manage_token ) : ?><section class="ygs-manage"><h3><?php esc_html_e( 'Manage reservation', 'yatoon-booking-system' ); ?></h3><button type="button" class="ygs-cancel" data-token="<?php echo esc_attr( $manage_token ); ?>"><?php esc_html_e( 'Cancel reservation', 'yatoon-booking-system' ); ?></button><p class="ygs-result" aria-live="polite"></p></section><?php endif; ?>
        <?php if ( ! $sessions ) : ?><p><?php esc_html_e( 'No upcoming group sessions are available.', 'yatoon-booking-system' ); ?></p><?php endif;
foreach ( $sessions as $session ) : $remaining = max( 0, $session->capacity - $session->seats_used );
$session_tz = new DateTimeZone( $session->booking_timezone ?: yatoon_timezone_string( $session->location_id ) );
$session_dt = new DateTime( $session->start_time, $session_tz ); ?>
        <?php /* translators: %d: number of seats still available. */ $remaining_label = sprintf( __( '%d place(s) left', 'yatoon-booking-system' ), $remaining ); ?>
        <article class="ygs-card"><div><h3><?php echo esc_html( $session->title ); ?></h3><p><?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $session_dt->getTimestamp(), $session_tz ) ); ?></p><p><?php echo esc_html( implode( ' · ', array_filter( array( $session->staff_name, $session->location_name, $remaining_label ) ) ) ); ?></p><strong><?php echo esc_html( YATOON_Tax::format_money( $session->price ) ); ?></strong></div>
        <?php if ( $remaining > 0 ) : ?><form class="ygs-form"><input type="hidden" name="session_id" value="<?php echo esc_attr( $session->id ); ?>"><label><?php esc_html_e( 'Name', 'yatoon-booking-system' ); ?><input name="name" required></label><label><?php esc_html_e( 'Email', 'yatoon-booking-system' ); ?><input type="email" name="email" required></label><label><?php esc_html_e( 'Phone', 'yatoon-booking-system' ); ?><input name="phone"></label><label><?php esc_html_e( 'Places', 'yatoon-booking-system' ); ?><input type="number" name="seats" min="1" max="<?php echo esc_attr( min( 20, $remaining ) ); ?>" value="1"></label><button type="submit"><?php esc_html_e( 'Reserve', 'yatoon-booking-system' ); ?></button><p class="ygs-result" aria-live="polite"></p></form><?php else : ?><span class="ygs-full"><?php esc_html_e( 'Full', 'yatoon-booking-system' ); ?></span><?php endif; ?></article><?php endforeach; ?></div>
        <script>(function(){var i18n={confirmed:<?php echo wp_json_encode( __( 'Reservation confirmed. Check your email.', 'yatoon-booking-system' ) ); ?>,unable:<?php echo wp_json_encode( __( 'Unable to reserve.', 'yatoon-booking-system' ) ); ?>,network:<?php echo wp_json_encode( __( 'Network error. Please try again.', 'yatoon-booking-system' ) ); ?>,networkShort:<?php echo wp_json_encode( __( 'Network error.', 'yatoon-booking-system' ) ); ?>};if(window.yatoonGroupBound)return;window.yatoonGroupBound=true;document.addEventListener('submit',function(e){if(!e.target.matches('.ygs-form'))return;e.preventDefault();var f=e.target,b=f.querySelector('button'),r=f.querySelector('.ygs-result'),d=new FormData(f);d.append('action','yatoon_book_group_session');d.append('nonce','<?php echo esc_js( wp_create_nonce( 'yatoon_group_booking' ) ); ?>');b.disabled=true;fetch('<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',{method:'POST',credentials:'same-origin',body:d}).then(x=>x.json()).then(x=>{r.textContent=x.success?i18n.confirmed:(x.data&&x.data.message||i18n.unable);if(x.success)f.reset();}).catch(()=>r.textContent=i18n.network).finally(()=>b.disabled=false);});document.addEventListener('click',function(e){if(!e.target.matches('.ygs-cancel'))return;var b=e.target,r=b.parentNode.querySelector('.ygs-result'),d=new FormData();d.append('action','yatoon_cancel_group_attendee');d.append('nonce','<?php echo esc_js( wp_create_nonce( 'yatoon_group_cancel' ) ); ?>');d.append('token',b.dataset.token);b.disabled=true;fetch('<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>',{method:'POST',credentials:'same-origin',body:d}).then(x=>x.json()).then(x=>{r.textContent=x.data&&x.data.message||'';if(x.success)b.remove();}).catch(()=>r.textContent=i18n.networkShort).finally(()=>b.disabled=false);});})();</script>
        <style>.yatoon-group-sessions{display:grid;gap:18px}.ygs-card,.ygs-manage{border:1px solid #e2e8f0;border-radius:12px;padding:20px;background:#fff}.ygs-card{display:grid;grid-template-columns:1fr minmax(260px,.8fr);gap:20px}.ygs-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.ygs-form label{font-weight:600}.ygs-form input{display:block;width:100%}.ygs-form button,.ygs-result{grid-column:1/-1}.ygs-full{font-weight:700;color:#b91c1c}@media(max-width:700px){.ygs-card{grid-template-columns:1fr}.ygs-form{grid-template-columns:1fr}}</style><?php
        return ob_get_clean();
    }

    public static function add_menu() {
 add_submenu_page( 'yatoon-booking-system', __( 'Group Sessions', 'yatoon-booking-system' ), __( 'Group Sessions', 'yatoon-booking-system' ), 'yatoon_manage_bookings', 'yatoon-group-sessions', array( __CLASS__, 'render_admin' ) ); }
    private static function require_admin() {
 if ( ! current_user_can( 'yatoon_manage_bookings' ) && ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to manage group sessions.', 'yatoon-booking-system' ) );
    } }

    public static function save_session() {
        self::require_admin();
check_admin_referer( 'yatoon_save_group_session' );
global $wpdb;
        if ( 'local' !== sanitize_key( get_option( 'yatoon_active_platform', 'local' ) ) ) { wp_die( esc_html__( 'Switch to Local booking mode before creating local group sessions.', 'yatoon-booking-system' ) );
        }
        $id = absint( $_POST['session_id'] ?? 0 );
$location_id = absint( $_POST['location_id'] ?? 0 );
$tz = yatoon_timezone( $location_id );
        try { $start = new DateTime( sanitize_text_field( wp_unslash( $_POST['start_time'] ?? '' ) ), $tz );
$end = new DateTime( sanitize_text_field( wp_unslash( $_POST['end_time'] ?? '' ) ), $tz );
} catch ( Exception $e ) { wp_die( esc_html__( 'Session date/time is invalid.', 'yatoon-booking-system' ) ); }
        if ( $end <= $start ) { wp_die( esc_html__( 'Session end must be after its start.', 'yatoon-booking-system' ) );
        }
        $availability_error = self::local_availability_error( absint( $_POST['staff_id'] ?? 0 ), $start, $end );
        if ( is_wp_error( $availability_error ) ) { wp_die( esc_html( $availability_error->get_error_message() ) );
        }
        if ( class_exists( 'YATOON_Staff_Locations' ) && ! YATOON_Staff_Locations::allows( absint( $_POST['staff_id'] ?? 0 ), absint( $_POST['service_id'] ?? 0 ), $location_id, $start, $end ) ) { wp_die( esc_html__( 'The selected staff/location rule does not allow this session.', 'yatoon-booking-system' ) );
        }
        if ( class_exists( 'YATOON_Resources' ) && ! YATOON_Resources::is_available( absint( $_POST['service_id'] ?? 0 ), $start, $end, 0, $location_id, $id ) ) { wp_die( esc_html__( 'A required room or resource is unavailable for this session.', 'yatoon-booking-system' ) );
        }
        $normal_conflict = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_bookings WHERE staff_id=%d AND status NOT IN ('cancelled','canceled','declined','no_show') AND start_time<%s AND end_time>%s LIMIT 1", absint( $_POST['staff_id'] ?? 0 ), $end->format('Y-m-d H:i:s'), $start->format('Y-m-d H:i:s') ) );
        if ( $normal_conflict || self::staff_has_session_conflict( absint( $_POST['staff_id'] ?? 0 ), $start, $end, $id ) ) { wp_die( esc_html__( 'The staff member already has an appointment or group session during this time.', 'yatoon-booking-system' ) );
        }
        $capacity = max( 1, absint( $_POST['capacity'] ?? 1 ) );
if ( $id && $capacity < self::seats_used( $id ) ) { wp_die( esc_html__( 'Capacity cannot be lower than existing confirmed places.', 'yatoon-booking-system' ) );
        }
        $data = array( 'service_id'=>absint($_POST['service_id']??0),'staff_id'=>absint($_POST['staff_id']??0),'location_id'=>$location_id,'title'=>sanitize_text_field(wp_unslash($_POST['title']??'')),'start_time'=>$start->format('Y-m-d H:i:s'),'end_time'=>$end->format('Y-m-d H:i:s'),'booking_timezone'=>$tz->getName(),'capacity'=>$capacity,'price'=>max(0,(float) sanitize_text_field(wp_unslash($_POST['price']??0))),'status'=>sanitize_key(wp_unslash($_POST['status']??'active')),'notes'=>sanitize_textarea_field(wp_unslash($_POST['notes']??'')),'updated_at'=>current_time('mysql') );
        if ( ! $data['title'] || ! $data['service_id'] || ! $data['staff_id'] ) { wp_die( esc_html__( 'Title, service and staff are required.', 'yatoon-booking-system' ) );
        }
        if ( $id ) { $wpdb->update( $wpdb->prefix . 'yatoon_group_sessions',$data,array('id'=>$id) );
} else { $data['created_at']=current_time('mysql');
$wpdb->insert($wpdb->prefix . 'yatoon_group_sessions',$data); }
        wp_safe_redirect( admin_url( 'admin.php?page=yatoon-group-sessions' ) );
exit;
    }
    public static function cancel_session() {
 self::require_admin();
check_admin_referer('yatoon_cancel_group_session');
global $wpdb;
$wpdb->update($wpdb->prefix . 'yatoon_group_sessions',array('status'=>'cancelled','updated_at'=>current_time('mysql')),array('id'=>absint($_GET['session_id']??0)));
wp_safe_redirect(admin_url('admin.php?page=yatoon-group-sessions'));
exit; }

    public static function render_admin() {
        self::require_admin();
global $wpdb;
$edit=absint($_GET['session_id']??0);
$row=$edit?$wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}yatoon_group_sessions WHERE id=%d",$edit)):null;
        $services=$wpdb->get_results("SELECT id,name,price,duration_minutes FROM {$wpdb->prefix}yatoon_services WHERE is_active=1 ORDER BY name");
$staff=$wpdb->get_results("SELECT id,name FROM {$wpdb->prefix}yatoon_staff WHERE is_active=1 ORDER BY name");
$locations=$wpdb->get_results("SELECT id,name FROM {$wpdb->prefix}yatoon_locations WHERE is_active=1 ORDER BY name");
$sessions=$wpdb->get_results("SELECT g.*,s.name service_name,st.name staff_name,l.name location_name,(SELECT COALESCE(SUM(a.seats),0) FROM {$wpdb->prefix}yatoon_group_attendees a WHERE a.session_id=g.id AND a.status IN ('pending','confirmed')) seats_used FROM {$wpdb->prefix}yatoon_group_sessions g LEFT JOIN {$wpdb->prefix}yatoon_services s ON s.id=g.service_id LEFT JOIN {$wpdb->prefix}yatoon_staff st ON st.id=g.staff_id LEFT JOIN {$wpdb->prefix}yatoon_locations l ON l.id=g.location_id ORDER BY g.start_time DESC LIMIT 100"); ?>
        <div class="wrap"><h1><?php esc_html_e('Group Sessions','yatoon-booking-system'); ?></h1><p><code>[yatoon_group_appointments]</code> · <?php esc_html_e('Capacity sessions are intentionally Local-mode only so Square/Vagaro remain authoritative in their modes.','yatoon-booking-system'); ?></p><div style="display:grid;grid-template-columns:minmax(360px,.8fr) minmax(600px,1.2fr);gap:18px"><section style="background:#fff;padding:20px"><h2><?php echo $row?esc_html__('Edit session','yatoon-booking-system'):esc_html__('New session','yatoon-booking-system'); ?></h2><form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>"><?php wp_nonce_field('yatoon_save_group_session'); ?><input type="hidden" name="action" value="yatoon_save_group_session"><input type="hidden" name="session_id" value="<?php echo esc_attr($edit); ?>"><?php foreach(array('title'=>__('Title','yatoon-booking-system'),'start_time'=>__('Start','yatoon-booking-system'),'end_time'=>__('End','yatoon-booking-system'),'capacity'=>__('Capacity','yatoon-booking-system'),'price'=>__('Price per place','yatoon-booking-system')) as $field=>$label): ?><label style="display:block;margin:10px 0;font-weight:600"><?php echo esc_html($label); ?><input style="display:block;width:100%" name="<?php echo esc_attr($field); ?>" <?php echo in_array($field,array('start_time','end_time'),true)?'type="datetime-local"':( in_array($field,array('capacity','price'),true)?'type="number" step="' . ( $field==='price'?'0.01':'1' ) . '"':'' ); ?> value="<?php echo esc_attr($row?( in_array($field,array('start_time','end_time'),true)?str_replace(' ','T',substr($row->$field,0,16)):$row->$field ):( $field==='capacity'?1:'' )); ?>" required></label><?php endforeach; ?><label>Service<select name="service_id" required><option></option><?php foreach($services as $x): ?><option value="<?php echo esc_attr($x->id); ?>" <?php selected($row->service_id??0,$x->id); ?>><?php echo esc_html($x->name); ?></option><?php endforeach; ?></select></label> <label>Staff<select name="staff_id" required><option></option><?php foreach($staff as $x): ?><option value="<?php echo esc_attr($x->id); ?>" <?php selected($row->staff_id??0,$x->id); ?>><?php echo esc_html($x->name); ?></option><?php endforeach; ?></select></label> <label>Location<select name="location_id"><option value="0">Default</option><?php foreach($locations as $x): ?><option value="<?php echo esc_attr($x->id); ?>" <?php selected($row->location_id??0,$x->id); ?>><?php echo esc_html($x->name); ?></option><?php endforeach; ?></select></label><input type="hidden" name="status" value="active"><p><button class="button button-primary"><?php esc_html_e('Save session','yatoon-booking-system'); ?></button></p></form></section><section style="background:#fff;padding:20px"><table class="widefat striped"><thead><tr><th>Session</th><th>Time</th><th>Staff/location</th><th>Capacity</th><th></th></tr></thead><tbody><?php foreach($sessions as $x): ?><tr><td><?php echo esc_html($x->title); ?><br><small><?php echo esc_html($x->status); ?></small></td><td><?php echo esc_html($x->start_time); ?></td><td><?php echo esc_html($x->staff_name . ' · ' . $x->location_name); ?></td><td><?php echo esc_html($x->seats_used . '/' . $x->capacity); ?></td><td><a class="button button-small" href="<?php echo esc_url(admin_url('admin.php?page=yatoon-group-sessions&session_id=' . $x->id)); ?>">Edit</a> <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=yatoon_cancel_group_session&session_id=' . $x->id),'yatoon_cancel_group_session')); ?>">Cancel</a></td></tr><?php endforeach; ?></tbody></table></section></div></div><?php
    }

    public static function register_rest_routes() {
        // 4.6.1: every permission_callback below is a string callable pointing
        // at YATOON_REST_API, which only loads when the "rest_api" module is
        // on. A permission_callback that cannot be resolved is treated by
        // WordPress as a failure, so the routes would 500 rather than 403.
        if ( ! class_exists( 'YATOON_REST_API' ) && class_exists( 'YATOON_Feature_Modules' ) ) {
            YATOON_Feature_Modules::ensure_loaded( 'rest_api' );
        }
        if ( ! class_exists( 'YATOON_REST_API' ) ) {
            return;
        }
        register_rest_route( 'yatoon/v1', '/group-sessions', array( array( 'methods'=>WP_REST_Server::READABLE,'callback'=>array(__CLASS__,'rest_sessions'),'permission_callback'=>array('YATOON_REST_API','can_read_directory') ), array( 'methods'=>WP_REST_Server::CREATABLE,'callback'=>array(__CLASS__,'rest_create_session'),'permission_callback'=>array('YATOON_REST_API','can_manage_bookings') ) ) );
        register_rest_route( 'yatoon/v1', '/group-sessions/(?P<id>\d+)', array( array( 'methods'=>WP_REST_Server::EDITABLE,'callback'=>array(__CLASS__,'rest_update_session'),'permission_callback'=>array('YATOON_REST_API','can_manage_bookings') ), array( 'methods'=>WP_REST_Server::DELETABLE,'callback'=>array(__CLASS__,'rest_delete_session'),'permission_callback'=>array('YATOON_REST_API','can_manage_bookings') ) ) );
        register_rest_route( 'yatoon/v1', '/group-sessions/(?P<id>\d+)/attendees', array( 'methods'=>WP_REST_Server::READABLE,'callback'=>array(__CLASS__,'rest_attendees'),'permission_callback'=>array('YATOON_REST_API','can_manage_bookings') ) );
    }
    public static function rest_sessions() {
 global $wpdb;
return $wpdb->get_results("SELECT * FROM {$wpdb->prefix}yatoon_group_sessions ORDER BY start_time DESC",ARRAY_A); }
    public static function rest_attendees( $request ) {
 global $wpdb;
return $wpdb->get_results($wpdb->prepare("SELECT id,session_id,customer_name,customer_email,customer_phone,seats,status,amount_due,payment_status,created_at FROM {$wpdb->prefix}yatoon_group_attendees WHERE session_id=%d ORDER BY id",absint($request['id'])),ARRAY_A); }
    public static function rest_create_session( $request ) {
 return self::rest_save_session( $request, 0 ); }
    public static function rest_update_session( $request ) {
 return self::rest_save_session( $request, absint( $request['id'] ) ); }
    private static function rest_save_session( WP_REST_Request $request, $id ) {
        global $wpdb;
        if ( 'local' !== sanitize_key( get_option( 'yatoon_active_platform', 'local' ) ) ) { return new WP_Error( 'group_local_only', __( 'Group sessions are Local-mode only so Square/Vagaro remain authoritative.', 'yatoon-booking-system' ), array( 'status'=>409 ) );
        }
        $existing = $id ? $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_group_sessions WHERE id=%d", $id ) ) : null;
        if ( $id && ! $existing ) { return new WP_Error( 'group_not_found', __( 'Group session not found.', 'yatoon-booking-system' ), array( 'status'=>404 ) );
        }
        $service_id = absint( $request->get_param('service_id') ?: ( $existing->service_id ?? 0 ) );
$staff_id = absint( $request->get_param('staff_id') ?: ( $existing->staff_id ?? 0 ) );
$location_id = absint( null !== $request->get_param('location_id') ? $request->get_param('location_id') : ( $existing->location_id ?? 0 ) );
        $tz = yatoon_timezone( $location_id );
        try { $start = new DateTime( $request->get_param('start_time') ?: ( $existing->start_time ?? '' ), $tz );
$end = new DateTime( $request->get_param('end_time') ?: ( $existing->end_time ?? '' ), $tz );
} catch ( Exception $e ) { return new WP_Error( 'group_datetime', __( 'Session date/time is invalid.', 'yatoon-booking-system' ), array( 'status'=>400 ) ); }
        if ( ! $service_id || ! $staff_id || $end <= $start ) { return new WP_Error( 'group_invalid', __( 'Service, staff and a valid time interval are required.', 'yatoon-booking-system' ), array( 'status'=>400 ) );
        }
        $availability_error = self::local_availability_error( $staff_id, $start, $end );
        if ( is_wp_error( $availability_error ) ) { $availability_error->add_data( array( 'status'=>409 ) );
return $availability_error; }
        $capacity = max( 1, absint( null !== $request->get_param('capacity') ? $request->get_param('capacity') : ( $existing->capacity ?? 1 ) ) );
        if ( $id && $capacity < self::seats_used( $id ) ) { return new WP_Error( 'group_capacity', __( 'Capacity cannot be lower than existing confirmed places.', 'yatoon-booking-system' ), array( 'status'=>409 ) );
        }
        if ( class_exists('YATOON_Staff_Locations') && ! YATOON_Staff_Locations::allows($staff_id,$service_id,$location_id,$start,$end) ) { return new WP_Error('group_location',__('Staff/location rules do not allow this session.','yatoon-booking-system'),array('status'=>409));
        }
        if ( self::staff_has_session_conflict($staff_id,$start,$end,$id) ) { return new WP_Error('group_conflict',__('Staff already has a group session during this interval.','yatoon-booking-system'),array('status'=>409));
        }
        $normal_conflict=$wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}yatoon_bookings WHERE staff_id=%d AND status NOT IN ('cancelled','canceled','declined','no_show') AND start_time<%s AND end_time>%s LIMIT 1",$staff_id,$end->format('Y-m-d H:i:s'),$start->format('Y-m-d H:i:s')));
        if($normal_conflict) { return new WP_Error('group_conflict',__('Staff already has an appointment during this interval.','yatoon-booking-system'),array('status'=>409));
        }
        if(class_exists('YATOON_Resources')&&!YATOON_Resources::is_available($service_id,$start,$end,0,$location_id,$id)) { return new WP_Error('group_resource',__('A required resource is unavailable.','yatoon-booking-system'),array('status'=>409));
        }
        $data=array('service_id'=>$service_id,'staff_id'=>$staff_id,'location_id'=>$location_id,'title'=>sanitize_text_field($request->get_param('title')?:( $existing->title??'' )),'start_time'=>$start->format('Y-m-d H:i:s'),'end_time'=>$end->format('Y-m-d H:i:s'),'booking_timezone'=>$tz->getName(),'capacity'=>$capacity,'price'=>max(0,(float) ( null!==$request->get_param('price')?$request->get_param('price'):( $existing->price??0 ) )),'status'=>sanitize_key($request->get_param('status')?:( $existing->status??'active' )),'notes'=>sanitize_textarea_field(null!==$request->get_param('notes')?$request->get_param('notes'):( $existing->notes??'' )),'updated_at'=>current_time('mysql'));
        if(!$data['title']) { return new WP_Error('group_title',__('Session title is required.','yatoon-booking-system'),array('status'=>400));
        }
        if($id) { $wpdb->update($wpdb->prefix . 'yatoon_group_sessions',$data,array('id'=>$id));
} else{$data['created_at']=current_time('mysql');
$wpdb->insert($wpdb->prefix . 'yatoon_group_sessions',$data);
$id=(int) $wpdb->insert_id;}
        $row=$wpdb->get_row($wpdb->prepare("SELECT * FROM {$wpdb->prefix}yatoon_group_sessions WHERE id=%d",$id),ARRAY_A);
        return new WP_REST_Response($row,$existing?200:201);
    }
    public static function rest_delete_session( $request ) {
 global $wpdb;
$id=absint($request['id']);
if(!$wpdb->get_var($wpdb->prepare("SELECT id FROM {$wpdb->prefix}yatoon_group_sessions WHERE id=%d",$id))) { return new WP_Error('group_not_found',__('Group session not found.','yatoon-booking-system'),array('status'=>404));
    } $wpdb->update($wpdb->prefix . 'yatoon_group_sessions',array('status'=>'cancelled','updated_at'=>current_time('mysql')),array('id'=>$id));
return new WP_REST_Response(null,204); }
}
