<?php
/**
 * Authenticated business REST API for bookings and directory data.
 *
 * Authentication is provided by WordPress (cookie + REST nonce or an
 * Application Password). Every route additionally checks a Yatoon capability.
 */
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_REST_API {
    const NAMESPACE = 'yatoon/v1';

    public static function init() {
        add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
    }

    public static function register_routes() {
        register_rest_route( self::NAMESPACE, '/bookings', array(
            array(
                'methods' => WP_REST_Server::READABLE,
                'callback' => array( __CLASS__, 'list_bookings' ),
                'permission_callback' => array( __CLASS__, 'can_manage_bookings' ),
            ),
            array(
                'methods' => WP_REST_Server::CREATABLE,
                'callback' => array( __CLASS__, 'create_booking' ),
                'permission_callback' => array( __CLASS__, 'can_manage_bookings' ),
            ),
        ) );
        register_rest_route( self::NAMESPACE, '/bookings/(?P<id>\d+)', array(
            array(
                'methods' => WP_REST_Server::READABLE,
                'callback' => array( __CLASS__, 'get_booking' ),
                'permission_callback' => array( __CLASS__, 'can_manage_bookings' ),
            ),
            array(
                'methods' => WP_REST_Server::EDITABLE,
                'callback' => array( __CLASS__, 'update_booking' ),
                'permission_callback' => array( __CLASS__, 'can_manage_bookings' ),
            ),
            array(
                'methods' => WP_REST_Server::DELETABLE,
                'callback' => array( __CLASS__, 'cancel_booking' ),
                'permission_callback' => array( __CLASS__, 'can_manage_bookings' ),
            ),
        ) );
        register_rest_route( self::NAMESPACE, '/orders/(?P<id>\d+)/refunds', array(
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => array( __CLASS__, 'create_refund' ),
            'permission_callback' => array( __CLASS__, 'can_manage_bookings' ),
        ) );

        foreach ( array( 'services', 'staff', 'locations', 'customers' ) as $entity ) {
            register_rest_route( self::NAMESPACE, '/' . $entity, array(
                array(
                    'methods' => WP_REST_Server::READABLE,
                    'callback' => function ( $request ) use ( $entity ) {
					return YATOON_REST_API::list_entities( $entity, $request ); },
                    'permission_callback' => function () use ( $entity ) {
					return YATOON_REST_API::can_read_entity( $entity ); },
                ),
                array(
                    'methods' => WP_REST_Server::CREATABLE,
                    'callback' => function ( $request ) use ( $entity ) {
					return YATOON_REST_API::create_entity( $entity, $request ); },
                    'permission_callback' => function () use ( $entity ) {
					return YATOON_REST_API::can_write_entity( $entity ); },
                ),
            ) );
            register_rest_route( self::NAMESPACE, '/' . $entity . '/(?P<id>\d+)', array(
                array(
                    'methods' => WP_REST_Server::READABLE,
                    'callback' => function ( $request ) use ( $entity ) {
					return YATOON_REST_API::get_entity( $entity, $request ); },
                    'permission_callback' => function () use ( $entity ) {
					return YATOON_REST_API::can_read_entity( $entity ); },
                ),
                array(
                    'methods' => WP_REST_Server::EDITABLE,
                    'callback' => function ( $request ) use ( $entity ) {
					return YATOON_REST_API::update_entity( $entity, $request ); },
                    'permission_callback' => function () use ( $entity ) {
					return YATOON_REST_API::can_write_entity( $entity ); },
                ),
                array(
                    'methods' => WP_REST_Server::DELETABLE,
                    'callback' => function ( $request ) use ( $entity ) {
					return YATOON_REST_API::delete_entity( $entity, $request ); },
                    'permission_callback' => function () use ( $entity ) {
					return YATOON_REST_API::can_write_entity( $entity ); },
                ),
            ) );
        }
    }

    public static function can_manage_bookings() {
        return current_user_can( 'yatoon_manage_bookings' ) || current_user_can( 'manage_options' );
    }

    public static function can_read_directory() {
        return self::can_manage_bookings()
            || current_user_can( 'yatoon_manage_services' )
            || current_user_can( 'yatoon_manage_staff' );
    }

    public static function can_read_entity( $entity ) {
        if ( 'customers' === $entity ) { return self::can_manage_bookings();
        }
        if ( 'staff' === $entity ) { return self::can_manage_bookings() || current_user_can( 'yatoon_manage_staff' );
        }
        return self::can_manage_bookings() || current_user_can( 'yatoon_manage_services' );
    }

    public static function can_write_entity( $entity ) {
        $cap = in_array( $entity, array( 'services', 'locations' ), true ) ? 'yatoon_manage_services'
            : ( 'staff' === $entity ? 'yatoon_manage_staff' : 'yatoon_manage_bookings' );
        return current_user_can( $cap ) || current_user_can( 'manage_options' );
    }

    private static function page_args( WP_REST_Request $request ) {
        return array(
            max( 1, min( 100, absint( $request->get_param( 'per_page' ) ?: 25 ) ) ),
            max( 1, absint( $request->get_param( 'page' ) ?: 1 ) ),
        );
    }

    private static function response_collection( $rows, $total, $page, $per_page ) {
        $response = new WP_REST_Response( array_values( (array) $rows ), 200 );
        $response->header( 'X-WP-Total', (int) $total );
        $response->header( 'X-WP-TotalPages', max( 1, (int) ceil( $total / $per_page ) ) );
        $response->header( 'X-Yatoon-Page', $page );
        return $response;
    }

    private static function public_booking_row( array $row ) {
        unset( $row['manage_token'], $row['design_upload_token'] );
        return $row;
    }

    private static function public_entity_row( $entity, array $row ) {
        if ( 'staff' === $entity ) { unset( $row['portal_pin'], $row['portal_pin_lookup'] );
        }
        return $row;
    }

    public static function list_bookings( WP_REST_Request $request ) {
        global $wpdb;
        list( $per_page, $page ) = self::page_args( $request );
        $where = array( '1=1' );
        $args  = array();
        foreach ( array( 'staff_id', 'service_id', 'location_id' ) as $key ) {
            if ( absint( $request->get_param( $key ) ) ) {
                $where[] = 'b.' . $key . '=%d';
                $args[]  = absint( $request->get_param( $key ) );
            }
        }
        if ( $request->get_param( 'status' ) ) {
            $where[] = 'b.status=%s';
            $args[]  = sanitize_key( $request->get_param( 'status' ) );
        }
        $date_from = $request->get_param( 'date_from' ) ?: $request->get_param( 'from' );
        $date_to = $request->get_param( 'date_to' ) ?: $request->get_param( 'to' );
        if ( $date_from ) {
            $where[] = 'b.start_time>=%s';
            $args[]  = sanitize_text_field( $date_from );
        }
        if ( $date_to ) {
            $where[] = 'b.start_time<%s';
            $args[]  = sanitize_text_field( $date_to );
        }
        if ( $request->get_param( 'search' ) ) {
            $like = '%' . $wpdb->esc_like( sanitize_text_field( $request->get_param( 'search' ) ) ) . '%';
            $where[] = '(b.customer_name LIKE %s OR b.customer_email LIKE %s OR b.customer_phone LIKE %s)';
            array_push( $args, $like, $like, $like );
        }
        $where_sql = implode( ' AND ', $where );
        $count_sql = "SELECT COUNT(*) FROM {$wpdb->prefix}yatoon_bookings b WHERE {$where_sql}";
        // The WHERE fragments above are fixed literals; only their values are placeholders.
        $count_query = $args ? $wpdb->prepare( $count_sql, $args ) : $count_sql; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $total = (int) yatoon_db_get_var( $count_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $query_args = array_merge( $args, array( $per_page, ( $page - 1 ) * $per_page ) );
        $sql = "SELECT b.*,s.name service_name,st.name staff_name,l.name location_name
                FROM {$wpdb->prefix}yatoon_bookings b
                LEFT JOIN {$wpdb->prefix}yatoon_services s ON s.id=b.service_id
                LEFT JOIN {$wpdb->prefix}yatoon_staff st ON st.id=b.staff_id
                LEFT JOIN {$wpdb->prefix}yatoon_locations l ON l.id=b.location_id
                WHERE {$where_sql} ORDER BY b.start_time DESC LIMIT %d OFFSET %d";
        $prepared_sql = $wpdb->prepare( $sql, $query_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $rows = array_map( array( __CLASS__, 'public_booking_row' ), yatoon_db_get_results( $prepared_sql, ARRAY_A ) );
        return self::response_collection( $rows, $total, $page, $per_page );
    }

    public static function get_booking( WP_REST_Request $request ) {
        global $wpdb;
        $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_bookings WHERE id=%d", absint( $request['id'] ) ), ARRAY_A );
        return $row ? self::public_booking_row( $row ) : new WP_Error( 'yatoon_not_found', __( 'Booking not found.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
    }

    public static function create_booking( WP_REST_Request $request ) {
        global $wpdb;
        if ( 'local' !== sanitize_key( get_option( 'yatoon_active_platform', 'local' ) ) ) {
            return new WP_Error( 'yatoon_external_platform', __( 'REST booking creation is disabled in Square/Vagaro mode; use the synchronized booking workflow so the external platform remains authoritative.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
        }
        $service_id = absint( $request->get_param( 'service_id' ) );
        $staff_id   = absint( $request->get_param( 'staff_id' ) );
        $service = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_services WHERE id=%d AND is_active=1", $service_id ) );
        $staff   = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_staff WHERE id=%d AND is_active=1 AND is_bookable=1", $staff_id ) );
        if ( ! $service || ! $staff ) {
            return new WP_Error( 'yatoon_invalid_assignment', __( 'Active service and staff are required.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
        }
        $start = self::parse_datetime( $request->get_param( 'start_time' ), absint( $request->get_param( 'location_id' ) ) );
        if ( is_wp_error( $start ) ) {
            return $start;
        }
        $duration = max( 1, absint( $request->get_param( 'duration_minutes' ) ?: $service->duration_minutes ) );
        $end = clone $start;
        $end->modify( '+' . $duration . ' minutes' );
        $booking_locks = YATOON_Booking_Lock::acquire( $start, $end );
        if ( false === $booking_locks ) {
            return new WP_Error( 'yatoon_booking_busy', __( 'That calendar is being updated. Please try again.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
        }
        register_shutdown_function( array( 'YATOON_Booking_Lock', 'release' ), $booking_locks );
        if ( self::booking_conflict( $staff_id, $start, $end ) ) {
            return new WP_Error( 'yatoon_booking_conflict', __( 'The staff member is already booked during this interval.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
        }
        if ( class_exists( 'YATOON_Staff_Locations' ) && ! YATOON_Staff_Locations::allows( $staff_id, $service_id, absint( $request->get_param( 'location_id' ) ), $start, $end ) ) {
            return new WP_Error( 'yatoon_location_rule', __( 'The staff/location rule does not allow this booking.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
        }
        if ( class_exists( 'YATOON_Business_Hours' ) ) {
            $day_hours = YATOON_Business_Hours::get_day_hours( $start->format( 'Y-m-d' ) );
            if ( ! $day_hours ) { return new WP_Error( 'yatoon_closed', __( 'The business is closed at this time.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            $open = new DateTime( $start->format( 'Y-m-d' ) . ' ' . ( $day_hours['start'] ?? '09:00' ), $start->getTimezone() );
            $close = new DateTime( $start->format( 'Y-m-d' ) . ' ' . ( $day_hours['end'] ?? '17:00' ), $start->getTimezone() );
            $break_conflict = false;
            if ( ! empty( $day_hours['break_start'] ) && ! empty( $day_hours['break_end'] ) ) {
                $break_start = new DateTime( $start->format( 'Y-m-d' ) . ' ' . $day_hours['break_start'], $start->getTimezone() );
                $break_end = new DateTime( $start->format( 'Y-m-d' ) . ' ' . $day_hours['break_end'], $start->getTimezone() );
                $break_conflict = $start < $break_end && $end > $break_start;
            }
            if ( $start->format( 'Y-m-d' ) !== $end->format( 'Y-m-d' ) || $start < $open || $end > $close || $break_conflict || YATOON_Business_Hours::has_staff_time_off_conflict( $staff_id, $start, $end, $start->getTimezone() ) || YATOON_Business_Hours::has_external_busy_conflict( $staff_id, $start, $end ) ) { return new WP_Error( 'yatoon_unavailable', __( 'The staff member is not available during this interval.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
        }
        if ( class_exists( 'YATOON_Resources' ) && ! YATOON_Resources::is_available( $service_id, $start, $end, 0, absint( $request->get_param( 'location_id' ) ) ) ) { return new WP_Error( 'yatoon_resource_unavailable', __( 'A required resource is unavailable.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
        }
        $customer_name = sanitize_text_field( $request->get_param( 'customer_name' ) );
        $customer_email = sanitize_email( $request->get_param( 'customer_email' ) );
        if ( ! $customer_name || ! is_email( $customer_email ) ) { return new WP_Error( 'yatoon_customer_required', __( 'Customer name and a valid email address are required.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
        }
        $base_price = class_exists( 'YATOON_Staff_Locations' ) ? YATOON_Staff_Locations::price_for( $staff_id, $service_id, absint( $request->get_param( 'location_id' ) ), $service->price ) : (float) $service->price;
        $payable_total = class_exists( 'YATOON_Tax' ) ? YATOON_Tax::payable_total( $base_price, $service_id ) : $base_price;
        $data = array(
            'square_booking_id' => null,
            'customer_name' => $customer_name,
            'customer_email' => $customer_email,
            'customer_phone' => sanitize_text_field( $request->get_param( 'customer_phone' ) ),
            'client_timezone' => sanitize_text_field( $request->get_param( 'client_timezone' ) ),
            'booking_timezone' => $start->getTimezone()->getName(),
            'location_id' => absint( $request->get_param( 'location_id' ) ) ?: null,
            'service_id' => $service_id,
            'staff_id' => $staff_id,
            'start_time' => $start->format( 'Y-m-d H:i:s' ),
            'end_time' => $end->format( 'Y-m-d H:i:s' ),
            'status' => 'confirmed',
            'notes' => sanitize_textarea_field( $request->get_param( 'notes' ) ),
            'total_price' => $payable_total,
            'manage_token' => wp_generate_password( 40, false, false ),
            'manage_token_expires' => $end->modify( '+30 days' )->format( 'Y-m-d H:i:s' ),
            'created_at' => current_time( 'mysql' ),
            'created_at_gmt' => current_time( 'mysql', true ),
        );
        if ( class_exists( 'YATOON_Tax' ) && YATOON_Tax::columns_exist() ) {
            $data = array_merge( $data, YATOON_Tax::booking_fields( $base_price, $service_id ) );
        }
        if ( false === $wpdb->insert( $wpdb->prefix . 'yatoon_bookings', $data ) ) {
            $status = function_exists( 'yatoon_is_slot_guard_conflict' ) && yatoon_is_slot_guard_conflict( $wpdb->last_error ) ? 409 : 500;
            return new WP_Error( 'yatoon_insert_failed', $status === 409 ? __( 'That slot was just booked.', 'yatoon-booking-system' ) : __( 'Booking could not be created.', 'yatoon-booking-system' ), array( 'status' => $status ) );
        }
        $id = (int) $wpdb->insert_id;
        do_action( 'yatoon_booking_created', array( 'booking_id' => $id, 'source' => 'rest_api' ) );
        YATOON_Activity_Log::record( 'booking_api_created', __( 'Booking created through REST API.', 'yatoon-booking-system' ), array( 'booking_id' => $id ) );
        $request->set_param( 'id', $id );
        YATOON_Booking_Lock::release( $booking_locks );
        return new WP_REST_Response( self::get_booking( $request ), 201 );
    }

    public static function update_booking( WP_REST_Request $request ) {
        global $wpdb;
        $id = absint( $request['id'] );
        $external_mode = 'local' !== sanitize_key( get_option( 'yatoon_active_platform', 'local' ) );
        $existing = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_bookings WHERE id=%d", $id ) );
        if ( ! $existing ) {
            return new WP_Error( 'yatoon_not_found', __( 'Booking not found.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
        }
        $changes = array();
        foreach ( array( 'customer_name', 'customer_phone' ) as $field ) {
            if ( null !== $request->get_param( $field ) ) { $changes[ $field ] = sanitize_text_field( $request->get_param( $field ) );
            }
        }
        if ( null !== $request->get_param( 'customer_email' ) ) { $changes['customer_email'] = sanitize_email( $request->get_param( 'customer_email' ) );
        }
        if ( null !== $request->get_param( 'notes' ) ) { $changes['notes'] = sanitize_textarea_field( $request->get_param( 'notes' ) );
        }
        if ( null !== $request->get_param( 'status' ) ) {
            $status = sanitize_key( $request->get_param( 'status' ) );
            if ( ! in_array( $status, array( 'pending', 'confirmed', 'completed', 'no_show', 'cancelled' ), true ) ) {
                return new WP_Error( 'yatoon_invalid_status', __( 'Booking status is invalid.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
            }
            if ( ( $external_mode || ! empty( $existing->square_booking_id ) ) && $status !== $existing->status ) {
                return new WP_Error( 'yatoon_external_booking', __( 'Use the Square/Vagaro synchronized workflow to change an external booking.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            $changes['status'] = $status;
        }
        if ( null !== $request->get_param( 'start_time' ) || null !== $request->get_param( 'staff_id' ) || null !== $request->get_param( 'service_id' ) || null !== $request->get_param( 'location_id' ) ) {
            if ( $external_mode || ! empty( $existing->square_booking_id ) ) {
                return new WP_Error( 'yatoon_external_booking', __( 'Use the synchronized reschedule workflow for an external booking.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            $staff_id = absint( $request->get_param( 'staff_id' ) ?: $existing->staff_id );
            $service_id = absint( $request->get_param( 'service_id' ) ?: $existing->service_id );
            $location_id = absint( null !== $request->get_param( 'location_id' ) ? $request->get_param( 'location_id' ) : $existing->location_id );
            $service = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_services WHERE id=%d", $service_id ) );
            $staff = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_staff WHERE id=%d AND is_active=1 AND is_bookable=1", $staff_id ) );
            $start = self::parse_datetime( $request->get_param( 'start_time' ) ?: $existing->start_time, $location_id );
            if ( is_wp_error( $start ) || ! $service || ! $staff ) { return is_wp_error( $start ) ? $start : new WP_Error( 'yatoon_assignment_missing', __( 'Active service and staff are required.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
            }
            $end = clone $start;
            $end->modify( '+' . max( 1, absint( $request->get_param( 'duration_minutes' ) ?: $service->duration_minutes ) ) . ' minutes' );
            $booking_locks = YATOON_Booking_Lock::acquire( $start, $end );
            if ( false === $booking_locks ) {
                return new WP_Error( 'yatoon_booking_busy', __( 'That calendar is being updated. Please try again.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            register_shutdown_function( array( 'YATOON_Booking_Lock', 'release' ), $booking_locks );
            if ( self::booking_conflict( $staff_id, $start, $end, $id ) ) { return new WP_Error( 'yatoon_booking_conflict', __( 'The staff member is already booked during this interval.', '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( 'yatoon_location_rule', __( 'The staff/location rule does not allow this booking.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            if ( class_exists( 'YATOON_Business_Hours' ) ) {
                $day = YATOON_Business_Hours::get_day_hours( $start->format( 'Y-m-d' ) );
                if ( ! $day || $start->format( 'Y-m-d' ) !== $end->format( 'Y-m-d' ) ) { return new WP_Error( 'yatoon_closed', __( 'The business is closed at this time.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
                }
                $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() );
                $break_conflict = false;
                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() );
                    $break_conflict = $start < $break_end && $end > $break_start;
                }
                if ( $start < $open || $end > $close || $break_conflict || YATOON_Business_Hours::has_staff_time_off_conflict( $staff_id, $start, $end, $start->getTimezone() ) || YATOON_Business_Hours::has_external_busy_conflict( $staff_id, $start, $end ) ) { return new WP_Error( 'yatoon_unavailable', __( 'The staff member is not available during this interval.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
                }
            }
            if ( class_exists( 'YATOON_Resources' ) && ! YATOON_Resources::is_available( $service_id, $start, $end, $id, $location_id ) ) { return new WP_Error( 'yatoon_resource_unavailable', __( 'A required resource is unavailable.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            $base_price = class_exists( 'YATOON_Staff_Locations' ) ? YATOON_Staff_Locations::price_for( $staff_id, $service_id, $location_id, $service->price ) : (float) $service->price;
            $new_total = class_exists( 'YATOON_Tax' ) ? YATOON_Tax::payable_total( $base_price, $service_id ) : $base_price;
            if ( abs( $new_total - (float) $existing->total_price ) > 0.00001 && in_array( (string) $existing->payment_status, array( 'paid', 'partially_refunded', 'refunded' ), true ) ) { return new WP_Error( 'yatoon_paid_price_change', __( 'A paid booking cannot be moved to a service/staff/location with a different price through the REST API.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
            $changes = array_merge( $changes, array( 'staff_id' => $staff_id, 'service_id' => $service_id, 'location_id' => $location_id ?: null, 'start_time' => $start->format( 'Y-m-d H:i:s' ), 'end_time' => $end->format( 'Y-m-d H:i:s' ), 'booking_timezone' => $start->getTimezone()->getName(), 'total_price' => $new_total ) );
            if ( class_exists( 'YATOON_Tax' ) && YATOON_Tax::columns_exist() ) { $changes = array_merge( $changes, YATOON_Tax::booking_fields( $base_price, $service_id ) );
            }
        }
        if ( $changes ) { $wpdb->update( $wpdb->prefix . 'yatoon_bookings', $changes, array( 'id' => $id ) );
        }
        if ( isset( $booking_locks ) ) { YATOON_Booking_Lock::release( $booking_locks );
        }
        if ( isset( $changes['status'] ) && $changes['status'] !== $existing->status ) { do_action( 'yatoon_booking_status_changed', $id, $changes['status'], $existing->status );
        }
        YATOON_Activity_Log::record( 'booking_api_updated', __( 'Booking updated through REST API.', 'yatoon-booking-system' ), array( 'booking_id' => $id ) );
        return self::get_booking( $request );
    }

    public static function cancel_booking( WP_REST_Request $request ) {
        $request->set_param( 'status', 'cancelled' );
        return self::update_booking( $request );
    }

    public static function create_refund( WP_REST_Request $request ) {
        if ( ! class_exists( 'YATOON_Commerce' ) || ( class_exists( 'YATOON_Feature_Modules' ) && ! YATOON_Feature_Modules::enabled( 'commerce' ) ) ) { return new WP_Error( 'yatoon_commerce_missing', __( 'Commerce module is unavailable.', 'yatoon-booking-system' ), array( 'status' => 503 ) );
        }
        $result = YATOON_Commerce::process_provider_refund( absint( $request['id'] ), (float) $request->get_param( 'amount' ), sanitize_key( $request->get_param( 'provider' ) ), sanitize_text_field( $request->get_param( 'reason' ) ) );
        return is_wp_error( $result ) ? $result : new WP_REST_Response( $result, 201 );
    }

    private static function parse_datetime( $value, $location_id = 0 ) {
        try {
            $timezone = function_exists( 'yatoon_timezone' ) ? yatoon_timezone( $location_id ) : wp_timezone();
            $date = new DateTime( sanitize_text_field( (string) $value ), $timezone );
            return $date;
        } catch ( Exception $e ) {
            return new WP_Error( 'yatoon_invalid_datetime', __( 'Date and time are invalid.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
        }
    }

    private static function booking_conflict( $staff_id, DateTimeInterface $start, DateTimeInterface $end, $exclude = 0 ) {
        global $wpdb;
        $booking_conflict = (bool) $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM {$wpdb->prefix}yatoon_bookings WHERE staff_id=%d AND id<>%d AND status NOT IN ('cancelled','canceled','declined','no_show') AND start_time<%s AND end_time>%s LIMIT 1",
            $staff_id, $exclude, $end->format( 'Y-m-d H:i:s' ), $start->format( 'Y-m-d H:i:s' )
        ) );
        return $booking_conflict || ( class_exists( 'YATOON_Group_Appointments' ) && YATOON_Group_Appointments::staff_has_session_conflict( $staff_id, $start, $end ) );
    }

    private static function entity_config( $entity ) {
        $map = array(
            'services' => array( 'table' => 'yatoon_services', 'search' => array( 'name', 'description', 'category' ), 'fields' => array( 'square_id','name','description','duration_minutes','price','estimated_cost','category','image_url','is_active','require_policy','require_consent','allow_staff_select','allow_design_upload','buffer_before_min','buffer_after_min','min_lead_time_min','sort_order','is_featured' ) ),
            'staff' => array( 'table' => 'yatoon_staff', 'search' => array( 'name', 'email', 'phone' ), 'fields' => array( 'square_id','name','email','phone','bio','image_url','calendar_color','is_bookable','is_active','sort_order','portal_role','portal_permissions' ) ),
            'locations' => array( 'table' => 'yatoon_locations', 'search' => array( 'name', 'address', 'phone' ), 'fields' => array( 'square_id','name','address','phone','timezone','is_active' ) ),
            'customers' => array( 'table' => 'yatoon_clients', 'search' => array( 'customer_name', 'customer_email', 'customer_phone' ), 'fields' => array( 'customer_name','customer_email','customer_phone','notes','allergies','preferences','preferred_staff','tags' ) ),
        );
        return $map[ $entity ] ?? null;
    }

    public static function list_entities( $entity, WP_REST_Request $request ) {
        global $wpdb;
        $config = self::entity_config( $entity );
        if ( ! $config ) { return new WP_Error( 'yatoon_invalid_entity', __( 'Resource type is invalid.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
        }
        list( $per_page, $page ) = self::page_args( $request );
        $table = $wpdb->prefix . $config['table'];
        $where = '1=1';
$args = array();
        if ( $request->get_param( 'search' ) ) {
            $like = '%' . $wpdb->esc_like( sanitize_text_field( $request->get_param( 'search' ) ) ) . '%';
            $where = '(' . implode( ' OR ', array_map( function ( $field ) {
 return $field . ' LIKE %s';
}, $config['search'] ) ) . ')';
            $args = array_fill( 0, count( $config['search'] ), $like );
        }
        $count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where}"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table and search columns come from the fixed entity allowlist.
        $count_query = $args ? $wpdb->prepare( $count_sql, $args ) : $count_sql; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $total = (int) yatoon_db_get_var( $count_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        $list_sql = "SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- fixed allowlist.
        $list_query = $wpdb->prepare( $list_sql, array_merge( $args, array( $per_page, ( $page - 1 ) * $per_page ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $rows = array_map( function ( $row ) use ( $entity ) {
 return YATOON_REST_API::public_entity_row( $entity, $row );
}, yatoon_db_get_results( $list_query, ARRAY_A ) );
        return self::response_collection( $rows, $total, $page, $per_page );
    }

    public static function get_entity( $entity, WP_REST_Request $request ) {
        global $wpdb;
        $config = self::entity_config( $entity );
        if ( ! $config ) { return new WP_Error( 'yatoon_invalid_entity', __( 'Resource type is invalid.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
        }
        $table = $wpdb->prefix . $config['table'];
        $query = $wpdb->prepare( "SELECT * FROM {$table} WHERE id=%d", absint( $request['id'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- fixed entity allowlist.
        $row = yatoon_db_get_row( $query, ARRAY_A );
        return $row ? self::public_entity_row( $entity, $row ) : new WP_Error( 'yatoon_not_found', __( 'Resource not found.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
    }

    private static function entity_data( $entity, WP_REST_Request $request, $creating = false ) {
        $config = self::entity_config( $entity );
$data = array();
        foreach ( $config['fields'] as $field ) {
            if ( null === $request->get_param( $field ) ) { continue;
            }
            $value = $request->get_param( $field );
            if ( false !== strpos( $field, 'email' ) ) { $value = sanitize_email( $value );
            } elseif ( in_array( $field, array( 'description','bio','address','notes','allergies','preferences' ), true ) ) { $value = sanitize_textarea_field( $value );
            } elseif ( in_array( $field, array( 'duration_minutes','sort_order','buffer_before_min','buffer_after_min','min_lead_time_min','is_active','is_bookable','require_policy','require_consent','allow_staff_select','allow_design_upload','is_featured' ), true ) ) { $value = absint( $value );
            } elseif ( in_array( $field, array( 'price','estimated_cost' ), true ) ) { $value = max( 0, (float) $value );
            } else { $value = sanitize_text_field( $value );
            }
            $data[ $field ] = $value;
        }
        if ( $creating && in_array( $entity, array( 'services','staff','locations' ), true ) && empty( $data['square_id'] ) ) { $data['square_id'] = 'local-' . wp_generate_uuid4();
        }
        return $data;
    }

    public static function create_entity( $entity, WP_REST_Request $request ) {
        global $wpdb;
        $config = self::entity_config( $entity );
        if ( ! $config ) { return new WP_Error( 'yatoon_invalid_entity', __( 'Resource type is invalid.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
        }
        $data = self::entity_data( $entity, $request, true );
        if ( in_array( $entity, array( 'services','staff','locations' ), true ) && empty( $data['name'] ) ) { return new WP_Error( 'yatoon_name_required', __( 'Name is required.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
        }
        if ( 'services' === $entity ) { $data['duration_minutes'] = max( 1, absint( $data['duration_minutes'] ?? 30 ) );
$data['price'] = max( 0, (float) ( $data['price'] ?? 0 ) ); }
        if ( 'customers' === $entity && empty( $data['customer_name'] ) ) { return new WP_Error( 'yatoon_name_required', __( 'Customer name is required.', 'yatoon-booking-system' ), array( 'status' => 400 ) );
        }
        if ( false === $wpdb->insert( $wpdb->prefix . $config['table'], $data ) ) { return new WP_Error( 'yatoon_insert_failed', __( 'Resource could not be created.', 'yatoon-booking-system' ), array( 'status' => 500 ) );
        }
        $request->set_param( 'id', (int) $wpdb->insert_id );
        return new WP_REST_Response( self::get_entity( $entity, $request ), 201 );
    }

    public static function update_entity( $entity, WP_REST_Request $request ) {
        global $wpdb;
        $existing = self::get_entity( $entity, $request );
        if ( is_wp_error( $existing ) ) { return $existing;
        }
        $config = self::entity_config( $entity );
        $data = self::entity_data( $entity, $request );
        if ( $data ) { $wpdb->update( $wpdb->prefix . $config['table'], $data, array( 'id' => absint( $request['id'] ) ) );
        }
        return self::get_entity( $entity, $request );
    }

    public static function delete_entity( $entity, WP_REST_Request $request ) {
        global $wpdb;
        $id = absint( $request['id'] );
$config = self::entity_config( $entity );
        if ( ! $config ) { return new WP_Error( 'yatoon_invalid_entity', __( 'Resource type is invalid.', 'yatoon-booking-system' ), array( 'status' => 404 ) );
        }
        if ( 'customers' !== $entity ) {
            $column = 'services' === $entity ? 'service_id' : ( 'staff' === $entity ? 'staff_id' : 'location_id' );
            $reference_query = $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_bookings WHERE {$column}=%d LIMIT 1", $id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column comes from a fixed allowlist.
            if ( yatoon_db_get_var( $reference_query ) ) { return new WP_Error( 'yatoon_resource_in_use', __( 'Resource is referenced by bookings and cannot be deleted; deactivate it instead.', 'yatoon-booking-system' ), array( 'status' => 409 ) );
            }
        }
        if ( false === $wpdb->delete( $wpdb->prefix . $config['table'], array( 'id' => $id ) ) ) { return new WP_Error( 'yatoon_delete_failed', __( 'Resource could not be deleted.', 'yatoon-booking-system' ), array( 'status' => 500 ) );
        }
        return new WP_REST_Response( null, 204 );
    }
}
