<?php

declare( strict_types=1 );

if ( ! defined( 'ABSPATH' ) ) exit;

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

/**
 * Registers all admin-authenticated REST API routes for Playard.
 * Hooked into the playard_register_routes action.
 */
class Playard_API_Admin {

	private Playard_Facility $facility;
	private Playard_Field    $field;
	private Playard_Payment  $payment;

	public function __construct(
		Playard_Facility $facility,
		Playard_Field    $field,
		Playard_Payment  $payment
	) {
		$this->facility = $facility;
		$this->field    = $field;
		$this->payment  = $payment;
	}

	public function register_routes(): void {
		$namespace = 'playard/v1';

		// ── Dashboard ─────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/dashboard', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'dashboard' ],
			'permission_callback' => [ $this, 'staff_permission' ],
		] );

		// ── Onboarding ────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/onboarding/facility', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'save_facility' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => $this->facility_args(),
		] );

		register_rest_route( $namespace, '/admin/onboarding/field', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'save_field' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => $this->field_args(),
		] );

		register_rest_route( $namespace, '/admin/onboarding/payment', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'save_payment' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => $this->payment_args(),
		] );

		register_rest_route( $namespace, '/admin/onboarding/complete', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'complete_onboarding' ],
			'permission_callback' => [ $this, 'admin_permission' ],
		] );

		register_rest_route( $namespace, '/admin/onboarding/skip', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'skip_onboarding' ],
			'permission_callback' => [ $this, 'admin_permission' ],
		] );

		// ── Facility ──────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/facility', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_facility' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
			[
				'methods'             => 'PUT',
				'callback'            => [ $this, 'update_facility' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => $this->facility_update_args(),
			],
		] );

		// ── Fields ───────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/fields', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'list_fields' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'create_field' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => $this->field_create_args(),
			],
		] );

		register_rest_route( $namespace, '/admin/fields/reorder', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'reorder_fields' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'order' => [
					'required' => true,
					'type'     => 'array',
					'items'    => [ 'type' => 'integer' ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/fields/(?P<id>\d+)', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_field' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
			[
				'methods'             => 'PUT',
				'callback'            => [ $this, 'update_field' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => $this->field_update_args(),
			],
			[
				'methods'             => 'DELETE',
				'callback'            => [ $this, 'deactivate_field' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
		] );

		// Permanent delete — only allowed for fields with zero bookings.
		register_rest_route( $namespace, '/admin/fields/(?P<id>\d+)/permanent', [
			'methods'             => 'DELETE',
			'callback'            => [ $this, 'delete_field_permanent' ],
			'permission_callback' => [ $this, 'admin_permission' ],
		] );

		// ── Field blocks ─────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/fields/(?P<id>\d+)/blocks', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'list_blocks' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => [
					'id'   => [ 'required' => true, 'type' => 'integer' ],
					'date' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
				],
			],
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'create_block' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => [
					'id'         => [ 'required' => true,  'type' => 'integer' ],
					'date'       => [ 'required' => true,  'type' => 'string'  ],
					'start_time' => [ 'required' => true,  'type' => 'string'  ],
					'end_time'   => [ 'required' => true,  'type' => 'string'  ],
					'reason'     => [ 'required' => false, 'type' => 'string', 'default' => '' ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/fields/(?P<id>\d+)/blocks/(?P<block_id>\d+)', [
			'methods'             => 'DELETE',
			'callback'            => [ $this, 'delete_block' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'id'       => [ 'required' => true, 'type' => 'integer' ],
				'block_id' => [ 'required' => true, 'type' => 'integer' ],
			],
		] );

		// ── Holidays ──────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/facility/holidays', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_holidays' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'add_holiday' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => [
					'date'   => [ 'required' => true,  'type' => 'string' ],
					'reason' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/facility/holidays/(?P<id>\d+)', [
			'methods'             => 'DELETE',
			'callback'            => [ $this, 'remove_holiday' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'id' => [ 'required' => true, 'type' => 'integer' ],
			],
		] );

		// ── Bookings list + create ───────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/bookings', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'list_bookings' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'field_id'       => [ 'required' => false, 'type' => 'integer', 'default' => 0 ],
					'date_from'      => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'date_to'        => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'status'         => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'payment_status' => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'sport'          => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'search'         => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'sort'           => [ 'required' => false, 'type' => 'string',  'default' => 'booking_date' ],
					'order'          => [ 'required' => false, 'type' => 'string',  'default' => 'desc' ],
					'page'           => [ 'required' => false, 'type' => 'integer', 'default' => 1 ],
					'per_page'       => [ 'required' => false, 'type' => 'integer', 'default' => 20 ],
				],
			],
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'create_walk_in' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'field_id'     => [ 'required' => true,  'type' => 'integer' ],
					'booking_date' => [ 'required' => true,  'type' => 'string' ],
					'slots'        => [ 'required' => true,  'type' => 'array', 'items' => [ 'type' => 'string' ] ],
					'sport'        => [ 'required' => true,  'type' => 'string' ],
					'customer'     => [
					'required'   => true,
					'type'       => 'object',
					'properties' => [
						'name'  => [ 'type' => 'string', 'required' => true  ],
						'phone' => [ 'type' => 'string', 'required' => true  ],
						'email' => [ 'type' => 'string', 'required' => false, 'default' => '' ],
					],
				],
					'payment_mode' => [ 'required' => false, 'type' => 'string', 'default' => 'cash', 'enum' => [ 'cash', 'paid', 'stripe' ] ],
					'admin_notes'  => [ 'required' => false, 'type' => 'string', 'default' => '' ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/customers/lookup', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'customer_lookup' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'phone' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_booking_admin' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'ref' => [ 'required' => true, 'type' => 'string' ],
				],
			],
			[
				'methods'             => 'PUT',
				'callback'            => [ $this, 'update_booking_admin' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'ref'          => [ 'required' => true,  'type' => 'string' ],
					'admin_notes'  => [ 'required' => false, 'type' => 'string', 'default' => '' ],
					'booking_date' => [ 'required' => false, 'type' => 'string' ],
					'slots'        => [ 'required' => false, 'type' => 'array'  ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)/cancel', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'cancel_booking_admin' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'ref'    => [ 'required' => true,  'type' => 'string' ],
				'reason' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			],
		] );

		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)/complete', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'complete_booking_admin' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'ref' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)/no-show', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'no_show_booking_admin' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'ref' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)/mark-paid', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'mark_booking_paid' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'ref'    => [ 'required' => true,  'type' => 'string' ],
				'amount' => [ 'required' => false, 'type' => 'number' ],
			],
		] );

		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)/payments', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'booking_payments' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'ref' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		// ── Calendar ─────────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/calendar/month', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'calendar_month' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'year'  => [ 'required' => true, 'type' => 'integer' ],
				'month' => [ 'required' => true, 'type' => 'integer' ],
			],
		] );

		register_rest_route( $namespace, '/admin/calendar/week', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'calendar_week' ],
			'permission_callback' => [ $this, 'staff_permission' ],
			'args'                => [
				'date' => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		// ── Customers ────────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/customers', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_customers_admin' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'search'   => [ 'required' => false, 'type' => 'string',  'default' => '' ],
					'sort'     => [ 'required' => false, 'type' => 'string',  'default' => 'created_at' ],
					'order'    => [ 'required' => false, 'type' => 'string',  'default' => 'desc' ],
					'page'     => [ 'required' => false, 'type' => 'integer', 'default' => 1 ],
					'per_page' => [ 'required' => false, 'type' => 'integer', 'default' => 20 ],
				],
			],
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'create_customer_admin' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'name'            => [ 'required' => true,  'type' => 'string',  'sanitize_callback' => 'sanitize_text_field' ],
					'phone'           => [ 'required' => true,  'type' => 'string',  'sanitize_callback' => 'sanitize_text_field' ],
					'email'           => [ 'required' => true,  'type' => 'string',  'sanitize_callback' => 'sanitize_email' ],
					'notes'           => [ 'required' => false, 'type' => 'string',  'sanitize_callback' => 'sanitize_textarea_field', 'default' => '' ],
					'create_wp_user'  => [ 'required' => false, 'type' => 'boolean', 'default' => false ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/customers/(?P<id>[\d]+)', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_customer_admin' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'id' => [ 'required' => true, 'type' => 'integer' ],
				],
			],
			[
				'methods'             => 'PUT',
				'callback'            => [ $this, 'update_customer_admin' ],
				'permission_callback' => [ $this, 'staff_permission' ],
				'args'                => [
					'id'    => [ 'required' => true,  'type' => 'integer' ],
					'notes' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
				],
			],
		] );

		// ── Reports ──────────────────────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/reports/revenue', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'report_revenue' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'from' => [ 'required' => true, 'type' => 'string' ],
				'to'   => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/reports/sports', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'report_sport' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'from' => [ 'required' => true, 'type' => 'string' ],
				'to'   => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/reports/trends', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'report_trends' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'from' => [ 'required' => true, 'type' => 'string' ],
				'to'   => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/reports/occupancy', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'report_occupancy' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'from' => [ 'required' => true, 'type' => 'string' ],
				'to'   => [ 'required' => true, 'type' => 'string' ],
			],
		] );

		register_rest_route( $namespace, '/admin/export/bookings', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'export_bookings_csv' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'from' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
				'to'   => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			],
		] );

		register_rest_route( $namespace, '/admin/export/customers', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'export_customers_csv' ],
			'permission_callback' => [ $this, 'admin_permission' ],
		] );

		// ── Settings (cancellation policy etc.) ──────────────────────────────────
		register_rest_route( $namespace, '/admin/settings', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_settings' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
			[
				'methods'             => 'PUT',
				'callback'            => [ $this, 'save_settings' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
		] );

		// ── Manual refund ─────────────────────────────────────────────────────────
		register_rest_route( $namespace, '/admin/bookings/(?P<ref>[A-Z0-9\-]+)/refund', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'refund_booking' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'ref' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field' ],
			],
		] );

		// ── Cancel duplicate bookings (data cleanup) ─────────────────────────────
		register_rest_route( $namespace, '/admin/bookings/cancel-duplicates', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'cancel_duplicate_bookings' ],
			'permission_callback' => [ $this, 'admin_permission' ],
		] );

		// ── Team (staff management) ──────────────────────────────────────────────

		register_rest_route( $namespace, '/admin/team', [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_team' ],
				'permission_callback' => [ $this, 'admin_permission' ],
			],
			[
				'methods'             => 'POST',
				'callback'            => [ $this, 'add_staff' ],
				'permission_callback' => [ $this, 'admin_permission' ],
				'args'                => [
					'user_id' => [ 'required' => true, 'type' => 'integer' ],
				],
			],
		] );

		register_rest_route( $namespace, '/admin/team/search', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'search_team_users' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'q' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			],
		] );

		register_rest_route( $namespace, '/admin/team/(?P<user_id>\d+)', [
			'methods'             => 'DELETE',
			'callback'            => [ $this, 'remove_staff' ],
			'permission_callback' => [ $this, 'admin_permission' ],
			'args'                => [
				'user_id' => [ 'required' => true, 'type' => 'integer' ],
			],
		] );

		// Pro plugin can register additional admin routes using the same permission callback.
		do_action( 'playard_register_admin_routes', $namespace, [ $this, 'admin_permission' ] );
	}

	// -------------------------------------------------------------------------
	// Permission
	// -------------------------------------------------------------------------

	public function admin_permission(): bool {
		return current_user_can( 'playard_admin' ) || current_user_can( 'manage_options' );
	}

	public function staff_permission(): bool {
		return current_user_can( 'playard_staff' ) || current_user_can( 'manage_options' );
	}

	// -------------------------------------------------------------------------
	// Dashboard
	// -------------------------------------------------------------------------

	public function dashboard(): WP_REST_Response {
		$report = new Playard_Report();

		return new WP_REST_Response(
			playard_rest_success(
				array_merge(
					$report->get_dashboard_stats(),
					[
						'upcoming_bookings' => $report->get_upcoming_bookings(),
						'recent_bookings'   => $report->get_recent_bookings(),
					]
				)
			),
			200
		);
	}

	// -------------------------------------------------------------------------
	// Onboarding — Facility
	// -------------------------------------------------------------------------

	public function save_facility( WP_REST_Request $request ): WP_REST_Response {
		$data = [
			'name'       => sanitize_text_field( $request->get_param( 'name' ) ),
			'address'    => sanitize_text_field( $request->get_param( 'address' ) ?? '' ),
			'phone'      => sanitize_text_field( $request->get_param( 'phone' ) ),
			'open_time'  => sanitize_text_field( $request->get_param( 'open_time' ) ),
			'close_time' => sanitize_text_field( $request->get_param( 'close_time' ) ),
			'open_days'  => array_map( 'sanitize_text_field', (array) $request->get_param( 'open_days' ) ),
			'timezone'   => sanitize_text_field( $request->get_param( 'timezone' ) ),
			'currency'   => sanitize_text_field( $request->get_param( 'currency' ) ),
		];

		do_action( 'playard_before_save_onboarding_facility', $data );

		$facility_id = $this->facility->save_onboarding( $data );

		if ( false === $facility_id ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to save facility. Please try again.', 'playard' ) ),
				500
			);
		}

		// Persist currency into global settings
		$settings             = (array) get_option( 'playard_settings', [] );
		$settings['currency'] = $data['currency'];
		$settings['timezone'] = $data['timezone'];
		update_option( 'playard_settings', $settings, 'no' );

		do_action( 'playard_onboarding_facility_saved', $facility_id, $data );

		return new WP_REST_Response(
			playard_rest_success(
				[ 'facility_id' => $facility_id ],
				__( 'Facility saved.', 'playard' )
			),
			200
		);
	}

	// -------------------------------------------------------------------------
	// Onboarding — Field
	// -------------------------------------------------------------------------

	public function save_field( WP_REST_Request $request ): WP_REST_Response {
		$facility_id = playard_get_facility_id();

		$data = [
			'facility_id'   => $facility_id > 0 ? $facility_id : absint( $request->get_param( 'facility_id' ) ),
			'name'          => sanitize_text_field( $request->get_param( 'name' ) ),
			'sports'        => array_map( 'sanitize_text_field', (array) $request->get_param( 'sports' ) ),
			'slot_duration' => absint( $request->get_param( 'slot_duration' ) ),
			'base_price'    => max( 0.0, (float) $request->get_param( 'base_price' ) ),
		];

		do_action( 'playard_before_save_onboarding_field', $data );

		// Upsert: if a field was already created earlier in onboarding (user went
		// back and forward), update it instead of creating a duplicate.
		$existing_id = absint( $request->get_param( 'field_id' ) );
		$existing    = $existing_id > 0 ? $this->field->get( $existing_id ) : null;

		if ( $existing && (int) $existing->facility_id === (int) $data['facility_id'] ) {
			$updated  = $this->field->update( $existing_id, [
				'name'          => $data['name'],
				'sports'        => $data['sports'],
				'slot_duration' => $data['slot_duration'],
				'base_price'    => $data['base_price'],
			] );
			$field_id = $updated ? $existing_id : false;
		} else {
			$field_id = $this->field->create( $data );
		}

		if ( false === $field_id ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to save field. Please try again.', 'playard' ) ),
				500
			);
		}

		do_action( 'playard_onboarding_field_saved', $field_id, $data );

		return new WP_REST_Response(
			playard_rest_success(
				[ 'field_id' => $field_id ],
				__( 'Field saved.', 'playard' )
			),
			200
		);
	}

	// -------------------------------------------------------------------------
	// Onboarding — Payment
	// -------------------------------------------------------------------------

	public function save_payment( WP_REST_Request $request ): WP_REST_Response {
		$method = sanitize_text_field( $request->get_param( 'method' ) );

		do_action( 'playard_before_save_onboarding_payment', $method );

		if ( $method === 'stripe' ) {
			$saved = $this->payment->save_stripe_keys( [
				'stripe_publishable_key' => $request->get_param( 'stripe_publishable_key' ) ?? '',
				'stripe_secret_key'      => $request->get_param( 'stripe_secret_key' ) ?? '',
				'test_mode'              => (bool) $request->get_param( 'test_mode' ),
			] );

			if ( ! $saved ) {
				return new WP_REST_Response(
					playard_rest_error(
						'invalid_stripe_keys',
						__( 'Invalid Stripe API keys. Please check the key format and try again.', 'playard' )
					),
					422
				);
			}
		} elseif ( $method === 'both' ) {
			$saved = $this->payment->save_stripe_keys( [
				'stripe_publishable_key' => $request->get_param( 'stripe_publishable_key' ) ?? '',
				'stripe_secret_key'      => $request->get_param( 'stripe_secret_key' ) ?? '',
				'test_mode'              => (bool) $request->get_param( 'test_mode' ),
			] );

			if ( ! $saved ) {
				return new WP_REST_Response(
					playard_rest_error(
						'invalid_stripe_keys',
						__( 'Invalid Stripe API keys. Please check the key format and try again.', 'playard' )
					),
					422
				);
			}

			$this->payment->save_cash_only();
		} elseif ( $method === 'cash' ) {
			$this->payment->save_cash_only();
		}

		do_action( 'playard_onboarding_payment_saved', $method );

		// Sync all fields' payment_mode to match the chosen payment method.
		$this->sync_fields_payment_mode( (array) get_option( 'playard_settings', [] ) );

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Payment settings saved.', 'playard' ) ),
			200
		);
	}

	// -------------------------------------------------------------------------
	// Onboarding — Complete
	// -------------------------------------------------------------------------

	public function complete_onboarding( WP_REST_Request $request ): WP_REST_Response {
		update_option( 'playard_onboarding_complete', true, 'no' );
		delete_transient( 'playard_onboarding_redirect' );

		do_action( 'playard_onboarding_complete' );

		// Return existing playard booking page if already created.
		$existing_id = (int) get_option( 'playard_booking_page_id', 0 );
		if ( $existing_id > 0 ) {
			$existing_page = get_post( $existing_id );
			if ( $existing_page && $existing_page->post_status === 'publish' ) {
				return new WP_REST_Response(
					playard_rest_success( [ 'booking_url' => (string) get_permalink( $existing_id ) ] ),
					200
				);
			}
		}

		$slug = sanitize_title( (string) ( $request->get_param( 'slug' ) ?? 'booking' ) );
		if ( empty( $slug ) ) {
			$slug = 'booking';
		}

		// Check if slug is taken by a page we didn't create.
		$taken = get_page_by_path( $slug, OBJECT, 'page' );
		if ( $taken ) {
			return new WP_REST_Response( [
				'success'     => false,
				'code'        => 'slug_taken',
				'message'     => sprintf( 'The URL "/%s" is already in use on this site.', $slug ),
				'suggestions' => $this->suggest_booking_slugs( $slug ),
			], 409 );
		}

		$page_id = wp_insert_post( [
			'post_title'   => 'Book Now',
			'post_name'    => $slug,
			'post_content' => '[playard]',
			'post_status'  => 'publish',
			'post_type'    => 'page',
		] );

		if ( is_wp_error( $page_id ) || ! $page_id ) {
			return new WP_REST_Response(
				playard_rest_error( 'page_create_failed', __( 'Failed to create booking page.', 'playard' ) ),
				500
			);
		}

		update_option( 'playard_booking_page_id', $page_id, 'no' );

		return new WP_REST_Response(
			playard_rest_success( [ 'booking_url' => (string) get_permalink( $page_id ) ] ),
			200
		);
	}

	private function suggest_booking_slugs( string $avoid ): array {
		$candidates  = [ 'book-now', 'bookings', 'playard-booking', 'book-a-field', 'reserve', 'book' ];
		$suggestions = [];
		foreach ( $candidates as $candidate ) {
			if ( $candidate !== $avoid && ! get_page_by_path( $candidate, OBJECT, 'page' ) ) {
				$suggestions[] = $candidate;
				if ( count( $suggestions ) >= 3 ) {
					break;
				}
			}
		}
		return $suggestions;
	}

	// -------------------------------------------------------------------------
	// Onboarding — Skip
	// -------------------------------------------------------------------------

	public function skip_onboarding(): WP_REST_Response {
		update_option( 'playard_onboarding_complete', true, 'no' );
		delete_transient( 'playard_onboarding_redirect' );

		do_action( 'playard_onboarding_skipped' );

		return new WP_REST_Response( playard_rest_success(), 200 );
	}

	// -------------------------------------------------------------------------
	// Facility — GET / PUT
	// -------------------------------------------------------------------------

	public function get_facility(): WP_REST_Response {
		$facility_id = playard_get_facility_id();

		if ( $facility_id === 0 ) {
			return new WP_REST_Response(
				playard_rest_success( null, __( 'No facility found.', 'playard' ) ),
				200
			);
		}

		$row = $this->facility->get( $facility_id );

		if ( ! $row ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Facility not found.', 'playard' ) ),
				404
			);
		}

		return new WP_REST_Response(
			playard_rest_success( $this->format_facility( $row ) ),
			200
		);
	}

	public function update_facility( WP_REST_Request $request ): WP_REST_Response {
		$facility_id = playard_get_facility_id();

		$allowed_days = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
		$open_days    = array_values( array_filter(
			array_map( 'sanitize_text_field', (array) ( $request->get_param( 'open_days' ) ?? [] ) ),
			fn( string $d ) => in_array( $d, $allowed_days, true )
		) );

		$open_time  = sanitize_text_field( (string) ( $request->get_param( 'open_time' )  ?? '06:00' ) );
		$close_time = sanitize_text_field( (string) ( $request->get_param( 'close_time' ) ?? '22:00' ) );

		// Server-side validation
		if ( empty( $open_days ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'open_days_required', __( 'Select at least one open day.', 'playard' ) ),
				422
			);
		}

		// '23:59' = close at end of day (midnight display) — always valid.
		// '00:00' as close_time = legacy midnight value — skip check for backward compat.
		$skip_range_check = ( '23:59' === $close_time || '00:00' === $close_time );
		if ( ! $skip_range_check && $close_time <= $open_time ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_hours', __( 'Close time must be after open time.', 'playard' ) ),
				422
			);
		}

		// Validate timezone
		$timezone        = sanitize_text_field( (string) ( $request->get_param( 'timezone' ) ?? 'UTC' ) );
		$valid_timezones = DateTimeZone::listIdentifiers();
		if ( ! in_array( $timezone, $valid_timezones, true ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_timezone', __( 'Invalid timezone.', 'playard' ) ),
				422
			);
		}

		// Validate currency — fall back to stored value when not sent by the form
		$stored_settings  = (array) get_option( 'playard_settings', [] );
		$fallback_currency = $stored_settings['currency'] ?? 'USD';
		$currency          = sanitize_text_field( (string) ( $request->get_param( 'currency' ) ?: $fallback_currency ) );
		$valid_currencies  = array_keys( playard_get_currency_list() );
		if ( ! in_array( $currency, $valid_currencies, true ) ) {
			$currency = $fallback_currency;
		}

		// Validate and sanitize date/time format options
		$allowed_date_formats = [ 'DD/MM/YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD' ];
		$date_format          = sanitize_text_field( (string) ( $request->get_param( 'date_format' ) ?? 'DD/MM/YYYY' ) );
		if ( ! in_array( $date_format, $allowed_date_formats, true ) ) {
			$date_format = 'DD/MM/YYYY';
		}

		$time_format = sanitize_text_field( (string) ( $request->get_param( 'time_format' ) ?? '12h' ) );
		if ( ! in_array( $time_format, [ '12h', '24h' ], true ) ) {
			$time_format = '12h';
		}

		// Validate email if provided
		$email_raw = sanitize_text_field( (string) ( $request->get_param( 'email' ) ?? '' ) );
		if ( $email_raw !== '' && ! is_email( $email_raw ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_email', __( 'Invalid email address.', 'playard' ) ),
				422
			);
		}

		$data = [
			'name'            => sanitize_text_field( $request->get_param( 'name' ) ),
			'logo_url'        => esc_url_raw( (string) ( $request->get_param( 'logo_url' ) ?? '' ) ),
			'cover_photo_url' => esc_url_raw( (string) ( $request->get_param( 'cover_photo_url' ) ?? '' ) ),
			'address'         => sanitize_text_field( (string) ( $request->get_param( 'address' ) ?? '' ) ),
			'lat'             => (float) $request->get_param( 'lat' ),
			'lng'             => (float) $request->get_param( 'lng' ),
			'open_days'       => $open_days,
			'open_time'       => $open_time,
			'close_time'      => $close_time,
			'timezone'        => $timezone,
			'currency'        => $currency,
			'phone'           => sanitize_text_field( (string) ( $request->get_param( 'phone' ) ?? '' ) ),
			'email'           => sanitize_email( $email_raw ),
		];

		if ( $facility_id > 0 ) {
			$ok = $this->facility->update_partial( $facility_id, $data );
		} else {
			// No facility yet — create one with minimal data
			$id = $this->facility->create( $data );
			$ok = $id !== false;
		}

		if ( ! $ok ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to save facility.', 'playard' ) ),
				500
			);
		}

		// Persist timezone, currency, and display format preferences in settings
		$settings                = (array) get_option( 'playard_settings', [] );
		$settings['timezone']    = $timezone;
		$settings['currency']    = $currency;
		$settings['date_format'] = $date_format;
		$settings['time_format'] = $time_format;
		update_option( 'playard_settings', $settings, 'no' );

		do_action( 'playard_facility_settings_saved', $facility_id, $settings );

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Facility saved.', 'playard' ) ),
			200
		);
	}

	// -------------------------------------------------------------------------
	// Fields — GET list / POST / GET single / PUT / DELETE (deactivate)
	// -------------------------------------------------------------------------

	public function list_fields(): WP_REST_Response {
		$facility_id = playard_get_facility_id();
		$rows        = $this->field->get_all( $facility_id );
		$formatted   = array_map( [ $this, 'format_field' ], $rows );

		return new WP_REST_Response(
			playard_rest_success( [
				'fields'  => $formatted,
				'can_add' => $this->field->can_add_field( $facility_id ),
			] ),
			200
		);
	}

	public function reorder_fields( WP_REST_Request $request ): WP_REST_Response {
		$facility_id = playard_get_facility_id();
		$order       = array_map( 'absint', (array) $request->get_param( 'order' ) );

		$this->field->batch_sort( $facility_id, $order );

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Fields reordered.', 'playard' ) ),
			200
		);
	}

	public function create_field( WP_REST_Request $request ): WP_REST_Response {
		$facility_id = playard_get_facility_id();

		$valid_sport_keys = array_column( playard_get_default_sports(), 'key' );
		$sports = array_values( array_filter(
			array_map( 'sanitize_text_field', (array) ( $request->get_param( 'sports' ) ?? [] ) ),
			fn( $s ) => in_array( $s, $valid_sport_keys, true )
		) );

		if ( empty( $sports ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'sports_required', __( 'Please select at least one sport.', 'playard' ) ),
				422
			);
		}

		$data = [
			'facility_id'   => $facility_id,
			'name'          => sanitize_text_field( $request->get_param( 'name' ) ),
			'description'   => sanitize_textarea_field( (string) ( $request->get_param( 'description' ) ?? '' ) ),
			'sports'        => $sports,
			'capacity'      => absint( $request->get_param( 'capacity' ) ?? 0 ),
			'slot_duration' => absint( $request->get_param( 'slot_duration' ) ?: 60 ),
			'buffer_time'   => absint( $request->get_param( 'buffer_time' ) ?? 0 ),
			'min_advance'   => absint( $request->get_param( 'min_advance' ) ?? 2 ),
			'max_advance'   => absint( $request->get_param( 'max_advance' ) ?? 30 ),
			'base_price'    => max( 0.0, (float) ( $request->get_param( 'base_price' ) ?? 0 ) ),
		];

		$field_id = $this->field->create( $data );

		if ( false === $field_id ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to create field.', 'playard' ) ),
				500
			);
		}

		$row = $this->field->get( $field_id );

		return new WP_REST_Response(
			playard_rest_success(
				$row ? $this->format_field( $row ) : [ 'id' => $field_id ],
				__( 'Field created.', 'playard' )
			),
			200
		);
	}

	public function get_field( WP_REST_Request $request ): WP_REST_Response {
		$id  = absint( $request->get_param( 'id' ) );
		$row = $this->field->get( $id );

		if ( ! $row ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Field not found.', 'playard' ) ),
				404
			);
		}

		return new WP_REST_Response(
			playard_rest_success( $this->format_field( $row ) ),
			200
		);
	}

	public function update_field( WP_REST_Request $request ): WP_REST_Response {
		$id  = absint( $request->get_param( 'id' ) );
		$row = $this->field->get( $id );

		if ( ! $row ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Field not found.', 'playard' ) ),
				404
			);
		}

		// Status change → set_status() (has last-field guard)
		if ( $request->has_param( 'status' ) ) {
			$status = sanitize_text_field( (string) $request->get_param( 'status' ) );
			$result = $this->field->set_status( $id, $status );

			if ( $result === 'invalid_status' ) {
				return new WP_REST_Response(
					playard_rest_error( 'invalid_status', __( 'Status must be active or inactive.', 'playard' ) ),
					422
				);
			}
			if ( $result === 'last_field' ) {
				return new WP_REST_Response(
					playard_rest_error( 'last_field', __( 'Cannot deactivate the last active field.', 'playard' ) ),
					422
				);
			}
			if ( $result === false ) {
				return new WP_REST_Response(
					playard_rest_error( 'save_failed', __( 'Failed to update field status.', 'playard' ) ),
					500
				);
			}
		}

		// Basic info (name, description, photos) → update_basic()
		$basic = [];
		if ( $request->has_param( 'name' ) )        $basic['name']        = sanitize_text_field( $request->get_param( 'name' ) );
		if ( $request->has_param( 'description' ) ) $basic['description'] = sanitize_textarea_field( (string) $request->get_param( 'description' ) );
		if ( $request->has_param( 'photos' ) )      $basic['photos']      = (array) $request->get_param( 'photos' );

		// Other fields → update()
		$other = [];
		if ( $request->has_param( 'sports' ) ) {
			$valid_sport_keys = array_column( playard_get_default_sports(), 'key' );
			$sports = array_values( array_filter(
				array_map( 'sanitize_text_field', (array) $request->get_param( 'sports' ) ),
				fn( $s ) => in_array( $s, $valid_sport_keys, true )
			) );

			if ( empty( $sports ) ) {
				return new WP_REST_Response(
					playard_rest_error( 'sports_required', __( 'Please select at least one sport.', 'playard' ) ),
					422
				);
			}

			$other['sports'] = $sports;
		}
		if ( $request->has_param( 'capacity' ) )      $other['capacity']      = absint( $request->get_param( 'capacity' ) );
		if ( $request->has_param( 'slot_duration' ) ) $other['slot_duration'] = absint( $request->get_param( 'slot_duration' ) );
		if ( $request->has_param( 'buffer_time' ) )   $other['buffer_time']   = absint( $request->get_param( 'buffer_time' ) );
		if ( $request->has_param( 'min_advance' ) )   $other['min_advance']   = absint( $request->get_param( 'min_advance' ) );
		if ( $request->has_param( 'max_advance' ) )   $other['max_advance']   = absint( $request->get_param( 'max_advance' ) );
		if ( $request->has_param( 'base_price' ) )    $other['base_price']    = max( 0.0, (float) $request->get_param( 'base_price' ) );
		if ( $request->has_param( 'payment_mode' ) ) $other['payment_mode']  = sanitize_text_field( (string) $request->get_param( 'payment_mode' ) );
		if ( $request->has_param( 'sort_order' ) )   $other['sort_order']    = absint( $request->get_param( 'sort_order' ) );

		if ( empty( $basic ) && empty( $other ) && ! $request->has_param( 'status' ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'no_data', __( 'No data provided.', 'playard' ) ),
				422
			);
		}

		if ( ! empty( $basic ) ) $this->field->update_basic( $id, $basic );
		if ( ! empty( $other ) ) $this->field->update( $id, $other );

		$updated = $this->field->get( $id );

		return new WP_REST_Response(
			playard_rest_success(
				$updated ? $this->format_field( $updated ) : [],
				__( 'Field updated.', 'playard' )
			),
			200
		);
	}

	public function deactivate_field( WP_REST_Request $request ): WP_REST_Response {
		$id     = absint( $request->get_param( 'id' ) );
		$result = $this->field->deactivate( $id );

		if ( $result === 'not_found' ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Field not found.', 'playard' ) ),
				404
			);
		}

		if ( $result === 'last_field' ) {
			return new WP_REST_Response(
				playard_rest_error( 'last_field', __( 'Cannot deactivate the last active field.', 'playard' ) ),
				422
			);
		}

		if ( $result === false ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to deactivate field.', 'playard' ) ),
				500
			);
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Field deactivated.', 'playard' ) ),
			200
		);
	}

	/**
	 * Permanently deletes a field — only if it has zero bookings. Protects
	 * historical data: any field that has ever been booked can only be deactivated.
	 */
	public function delete_field_permanent( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;
		$id = absint( $request->get_param( 'id' ) );

		$field = $this->field->get( $id );
		if ( ! $field ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Field not found.', 'playard' ) ),
				404
			);
		}

		// Must always keep at least one field for the facility.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$total_fields = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->prefix}playard_fields WHERE facility_id = %d",
			(int) $field->facility_id
		) );

		if ( $total_fields <= 1 ) {
			return new WP_REST_Response(
				playard_rest_error(
					'last_field',
					__( 'Cannot delete the only field. A facility must have at least one field.', 'playard' )
				),
				422
			);
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$bookings = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->prefix}playard_bookings WHERE field_id = %d",
			$id
		) );

		if ( $bookings > 0 ) {
			return new WP_REST_Response(
				playard_rest_error(
					'has_bookings',
					__( 'This field has bookings and cannot be deleted. Deactivate it instead to keep its history.', 'playard' )
				),
				422
			);
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		// Remove dependent rows that have no historical value, then the field itself.
		$wpdb->delete( $wpdb->prefix . 'playard_blocked_slots', [ 'field_id' => $id ], [ '%d' ] );
		$deleted = $wpdb->delete( $wpdb->prefix . 'playard_fields', [ 'id' => $id ], [ '%d' ] );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( false === $deleted ) {
			return new WP_REST_Response(
				playard_rest_error( 'delete_failed', __( 'Failed to delete field.', 'playard' ) ),
				500
			);
		}

		do_action( 'playard_field_deleted', $id );

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Field deleted permanently.', 'playard' ) ),
			200
		);
	}

	// ── Field blocks ─────────────────────────────────────────────────────────

	public function list_blocks( WP_REST_Request $request ): WP_REST_Response {
		$field_id = absint( $request->get_param( 'id' ) );
		$date     = sanitize_text_field( (string) $request->get_param( 'date' ) );

		if ( ! $this->field->get( $field_id ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Field not found.', 'playard' ) ),
				404
			);
		}

		$rows = $this->field->get_blocks( $field_id, $date );

		return new WP_REST_Response(
			playard_rest_success( array_map( [ $this, 'format_block' ], $rows ) ),
			200
		);
	}

	public function create_block( WP_REST_Request $request ): WP_REST_Response {
		$field_id   = absint( $request->get_param( 'id' ) );
		$date       = sanitize_text_field( (string) $request->get_param( 'date' ) );
		$start_time = sanitize_text_field( (string) $request->get_param( 'start_time' ) );
		$end_time   = sanitize_text_field( (string) $request->get_param( 'end_time' ) );
		$reason     = sanitize_text_field( (string) ( $request->get_param( 'reason' ) ?? '' ) );

		if ( ! $this->field->get( $field_id ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Field not found.', 'playard' ) ),
				404
			);
		}

		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_date', __( 'Invalid date format. Use YYYY-MM-DD.', 'playard' ) ),
				422
			);
		}

		if ( $start_time >= $end_time ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_time', __( 'Start time must be before end time.', 'playard' ) ),
				422
			);
		}

		$block_id = $this->field->add_block( $field_id, [
			'date'       => $date,
			'start_time' => $start_time,
			'end_time'   => $end_time,
			'reason'     => $reason,
		] );

		if ( false === $block_id ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to save block.', 'playard' ) ),
				500
			);
		}

		return new WP_REST_Response(
			playard_rest_success(
				[
					'id'         => $block_id,
					'field_id'   => $field_id,
					'date'       => $date,
					'start_time' => $start_time,
					'end_time'   => $end_time,
					'reason'     => $reason,
				],
				__( 'Slot blocked.', 'playard' )
			),
			200
		);
	}

	public function delete_block( WP_REST_Request $request ): WP_REST_Response {
		$field_id = absint( $request->get_param( 'id' ) );
		$block_id = absint( $request->get_param( 'block_id' ) );
		$result   = $this->field->remove_block( $block_id, $field_id );

		if ( $result === 'not_found' ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Block not found.', 'playard' ) ),
				404
			);
		}

		if ( $result === 'forbidden' ) {
			return new WP_REST_Response(
				playard_rest_error( 'forbidden', __( 'This block does not belong to the specified field.', 'playard' ) ),
				403
			);
		}

		if ( $result === false ) {
			return new WP_REST_Response(
				playard_rest_error( 'save_failed', __( 'Failed to remove block.', 'playard' ) ),
				500
			);
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Block removed.', 'playard' ) ),
			200
		);
	}

	/**
	 * @param object $row
	 * @return array<string,mixed>
	 */
	private function format_block( object $row ): array {
		return [
			'id'         => (int) $row->id,
			'field_id'   => (int) $row->field_id,
			'date'       => $row->blocked_date,
			'start_time' => $row->start_time,
			'end_time'   => $row->end_time,
			'reason'     => $row->reason ?? '',
		];
	}

	// ──────────────────────────────────────────────────────────────────────────

	/**
	 * @param object $row
	 * @return array<string,mixed>
	 */
	private function format_field( object $row ): array {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$bookings_count = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->prefix}playard_bookings WHERE field_id = %d",
			(int) $row->id
		) );

		return [
			'id'             => (int) $row->id,
			'facility_id'    => (int) $row->facility_id,
			'name'           => $row->name,
			'description'    => $row->description ?? '',
			'sports'         => json_decode( $row->sports ?? '[]', true ) ?: [],
			'photos'         => json_decode( $row->photos ?? '[]', true ) ?: [],
			'capacity'       => (int) $row->capacity,
			'slot_duration'  => (int) $row->slot_duration,
			'buffer_time'    => (int) $row->buffer_time,
			'min_advance'    => (int) $row->min_advance,
			'max_advance'    => (int) $row->max_advance,
			'base_price'     => (float) $row->base_price,
			'payment_mode'   => $row->payment_mode ?? 'online',
			'status'         => $row->status ?? 'active',
			'sort_order'     => (int) $row->sort_order,
			'bookings_count' => $bookings_count,
		];
	}

	private function field_create_args(): array {
		return [
			'name'          => [ 'required' => true,  'type' => 'string',  'minLength' => 2 ],
			'description'   => [ 'required' => false, 'type' => 'string',  'default' => '' ],
			'photos'        => [ 'required' => false, 'type' => 'array',   'items' => [ 'type' => 'string' ], 'default' => [] ],
			'sports'        => [ 'required' => true,  'type' => 'array',   'items' => [ 'type' => 'string' ] ],
			'capacity'      => [ 'required' => false, 'type' => 'integer', 'minimum' => 0, 'default' => 0 ],
			'slot_duration' => [ 'required' => false, 'type' => 'integer', 'enum' => [ 30, 60, 90, 120 ], 'default' => 60 ],
			'buffer_time'   => [ 'required' => false, 'type' => 'integer', 'enum' => [ 0, 5, 10, 15, 30 ], 'default' => 0 ],
			'min_advance'   => [ 'required' => false, 'type' => 'integer', 'minimum' => 0, 'default' => 2 ],
			'max_advance'   => [ 'required' => false, 'type' => 'integer', 'minimum' => 0, 'default' => 30 ],
			'base_price'    => [ 'required' => false, 'type' => 'number',  'minimum' => 0, 'default' => 0 ],
			'payment_mode'  => [ 'required' => false, 'type' => 'string',  'enum' => [ 'online', 'cash', 'both' ], 'default' => 'online' ],
		];
	}

	private function field_update_args(): array {
		return [
			'name'          => [ 'required' => false, 'type' => 'string',  'minLength' => 2 ],
			'description'   => [ 'required' => false, 'type' => 'string' ],
			'photos'        => [ 'required' => false, 'type' => 'array',   'items' => [ 'type' => 'string' ] ],
			'sports'        => [ 'required' => false, 'type' => 'array',   'items' => [ 'type' => 'string' ] ],
			'capacity'      => [ 'required' => false, 'type' => 'integer', 'minimum' => 0 ],
			'slot_duration' => [ 'required' => false, 'type' => 'integer', 'enum' => [ 30, 60, 90, 120 ] ],
			'buffer_time'   => [ 'required' => false, 'type' => 'integer', 'enum' => [ 0, 5, 10, 15, 30 ] ],
			'min_advance'   => [ 'required' => false, 'type' => 'integer', 'minimum' => 0 ],
			'max_advance'   => [ 'required' => false, 'type' => 'integer', 'minimum' => 0 ],
			'base_price'    => [ 'required' => false, 'type' => 'number',  'minimum' => 0 ],
			'sort_order'    => [ 'required' => false, 'type' => 'integer', 'minimum' => 0 ],
			'payment_mode'  => [ 'required' => false, 'type' => 'string',  'enum' => [ 'online', 'cash', 'both' ] ],
			'status'        => [ 'required' => false, 'type' => 'string',  'enum' => [ 'active', 'inactive' ] ],
		];
	}

	// -------------------------------------------------------------------------
	// Holidays — GET / POST / DELETE
	// -------------------------------------------------------------------------

	public function get_holidays(): WP_REST_Response {
		return new WP_REST_Response(
			playard_rest_success( $this->facility->get_holidays() ),
			200
		);
	}

	public function add_holiday( WP_REST_Request $request ): WP_REST_Response {
		$date   = sanitize_text_field( (string) $request->get_param( 'date' ) );
		$reason = sanitize_text_field( (string) ( $request->get_param( 'reason' ) ?? '' ) );

		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_date', __( 'Invalid date format. Use YYYY-MM-DD.', 'playard' ) ),
				422
			);
		}

		$id = $this->facility->add_holiday( $date, $reason );

		return new WP_REST_Response(
			playard_rest_success(
				[ 'id' => $id, 'date' => $date, 'reason' => $reason ],
				__( 'Holiday added.', 'playard' )
			),
			200
		);
	}

	public function remove_holiday( WP_REST_Request $request ): WP_REST_Response {
		$id = absint( $request->get_param( 'id' ) );
		$ok = $this->facility->remove_holiday( $id );

		if ( ! $ok ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Holiday not found.', 'playard' ) ),
				404
			);
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Holiday removed.', 'playard' ) ),
			200
		);
	}

	/**
	 * Formats a raw DB row into an API-safe array.
	 *
	 * @param object $row
	 * @return array<string,mixed>
	 */
	private function format_facility( object $row ): array {
		$settings = (array) get_option( 'playard_settings', [] );

		return [
			'id'              => (int) $row->id,
			'name'            => $row->name,
			'description'     => $row->description ?? '',
			'address'         => $row->address ?? '',
			'lat'             => (float) ( $row->lat ?? 0.0 ),
			'lng'             => (float) ( $row->lng ?? 0.0 ),
			'phone'           => $row->phone ?? '',
			'email'           => $row->email ?? '',
			'logo_url'        => $row->logo_url ?? '',
			'cover_photo_url' => $row->cover_photo_url ?? '',
			'timezone'        => $row->timezone ?? 'UTC',
			'currency'        => $row->currency ?? 'USD',
			'open_time'       => $row->open_time ? substr( $row->open_time, 0, 5 ) : '06:00',
			'close_time'      => ( $row->close_time && substr( $row->close_time, 0, 5 ) !== '00:00' ) ? substr( $row->close_time, 0, 5 ) : ( $row->close_time ? '23:59' : '22:00' ),
			'open_days'       => json_decode( $row->open_days ?? '[]', true ) ?: [],
			'status'          => $row->status ?? 'active',
			'date_format'     => $settings['date_format'] ?? 'DD/MM/YYYY',
			'time_format'     => $settings['time_format'] ?? '12h',
		];
	}

	// -------------------------------------------------------------------------
	// Argument Schemas
	// -------------------------------------------------------------------------

	private function facility_args(): array {
		return [
			'name'       => [ 'required' => true,  'type' => 'string', 'minLength' => 2 ],
			'phone'      => [ 'required' => true,  'type' => 'string' ],
			'address'    => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			'open_time'  => [ 'required' => true,  'type' => 'string' ],
			'close_time' => [ 'required' => true,  'type' => 'string' ],
			'open_days'  => [ 'required' => true,  'type' => 'array',  'items' => [ 'type' => 'string' ] ],
			'timezone'   => [ 'required' => true,  'type' => 'string' ],
			'currency'   => [ 'required' => false, 'type' => 'string', 'default' => '' ],
		];
	}

	private function field_args(): array {
		return [
			'name'          => [ 'required' => true,  'type' => 'string' ],
			'sports'        => [ 'required' => true,  'type' => 'array', 'items' => [ 'type' => 'string' ] ],
			'slot_duration' => [ 'required' => true,  'type' => 'integer', 'enum' => [ 30, 60, 90, 120 ] ],
			'base_price'    => [ 'required' => true,  'type' => 'number',  'minimum' => 0 ],
			'facility_id'   => [ 'required' => false, 'type' => 'integer', 'default' => 0 ],
		];
	}

	private function payment_args(): array {
		return [
			'method'                 => [ 'required' => true, 'type' => 'string', 'enum' => [ 'stripe', 'cash', 'both' ] ],
			'stripe_publishable_key' => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			'stripe_secret_key'      => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			'test_mode'              => [ 'required' => false, 'type' => 'boolean', 'default' => false ],
		];
	}

	private function facility_update_args(): array {
		return [
			'name'            => [ 'required' => true,  'type' => 'string',  'minLength' => 2 ],
			'logo_url'        => [ 'required' => false, 'type' => 'string',  'default' => '' ],
			'cover_photo_url' => [ 'required' => false, 'type' => 'string',  'default' => '' ],
			'address'         => [ 'required' => false, 'type' => 'string',  'default' => '' ],
			'lat'             => [ 'required' => false, 'type' => 'number',  'default' => 0.0 ],
			'lng'             => [ 'required' => false, 'type' => 'number',  'default' => 0.0 ],
			'open_days'       => [ 'required' => true,  'type' => 'array',   'items' => [ 'type' => 'string' ] ],
			'open_time'       => [ 'required' => true,  'type' => 'string' ],
			'close_time'      => [ 'required' => true,  'type' => 'string' ],
			'timezone'        => [ 'required' => true,  'type' => 'string' ],
			'currency'        => [ 'required' => false, 'type' => 'string', 'default' => '' ],
			'date_format'     => [ 'required' => false, 'type' => 'string',  'default' => 'DD/MM/YYYY' ],
			'time_format'     => [ 'required' => false, 'type' => 'string',  'default' => '12h', 'enum' => [ '12h', '24h' ] ],
			'phone'           => [ 'required' => false, 'type' => 'string',  'default' => '' ],
			'email'           => [ 'required' => false, 'type' => 'string',  'default' => '' ],
		];
	}

	// ── Bookings list + single ───────────────────────────────────────────────────

	public function list_bookings( WP_REST_Request $request ): WP_REST_Response {
		$booking = new Playard_Booking();

		$result = $booking->get_bookings( [
			'field_id'       => absint( $request->get_param( 'field_id' ) ),
			'date_from'      => sanitize_text_field( (string) $request->get_param( 'date_from' ) ),
			'date_to'        => sanitize_text_field( (string) $request->get_param( 'date_to' ) ),
			'status'         => sanitize_text_field( (string) $request->get_param( 'status' ) ),
			'payment_status' => sanitize_text_field( (string) $request->get_param( 'payment_status' ) ),
			'sport'          => sanitize_text_field( (string) $request->get_param( 'sport' ) ),
			'search'         => sanitize_text_field( (string) $request->get_param( 'search' ) ),
			'sort'           => sanitize_text_field( (string) $request->get_param( 'sort' ) ),
			'order'          => sanitize_text_field( (string) $request->get_param( 'order' ) ),
			'page'           => absint( $request->get_param( 'page' ) ),
			'per_page'       => absint( $request->get_param( 'per_page' ) ),
		] );

		return new WP_REST_Response(
			playard_rest_success( [
				'bookings' => array_map( [ $this, 'format_booking' ], $result['bookings'] ),
				'total'    => $result['total'],
				'page'     => $result['page'],
				'per_page' => $result['per_page'],
			] ),
			200
		);
	}

	public function get_booking_admin( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$ref = sanitize_text_field( (string) $request->get_param( 'ref' ) );

		$row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table read; booking data is request-specific and must be live, not cached.
			$wpdb->prepare(
				"SELECT b.*, f.name AS field_name, f.slot_duration,
				        c.name  AS customer_name,
				        c.phone AS customer_phone,
				        c.email AS customer_email
				 FROM {$wpdb->prefix}playard_bookings b
				 LEFT JOIN {$wpdb->prefix}playard_fields f    ON b.field_id    = f.id
				 LEFT JOIN {$wpdb->prefix}playard_customers c ON b.customer_id = c.id
				 WHERE b.booking_ref = %s
				 LIMIT 1",
				$ref
			)
		);

		if ( ! $row ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Booking not found.', 'playard' ) ),
				404
			);
		}

		return new WP_REST_Response( playard_rest_success( $this->format_booking( $row ) ), 200 );
	}

	/**
	 * Formats a raw booking DB row (with joined field/customer columns) into an API-safe array.
	 *
	 * @param object $row
	 * @return array<string,mixed>
	 */
	private function format_booking( object $row ): array {
		return [
			'id'                  => (int) $row->id,
			'booking_ref'         => $row->booking_ref,
			'field_id'            => (int) $row->field_id,
			'field_name'          => (string) ( $row->field_name    ?? '' ),
			'customer_id'         => (int) $row->customer_id,
			'customer_name'       => (string) ( $row->customer_name  ?? '' ),
			'customer_phone'      => (string) ( $row->customer_phone ?? '' ),
			'customer_email'      => (string) ( $row->customer_email ?? '' ),
			'booking_date'        => $row->booking_date,
			'start_time'          => $row->start_time,
			'end_time'            => $row->end_time,
			'slots_count'         => (int) $row->slots_count,
			'slot_duration'       => (int) ( $row->slot_duration ?? 60 ),
			'booked_slots'        => ! empty( $row->booked_slots ) ? json_decode( (string) $row->booked_slots, true ) : null,
			'sport'               => $row->sport,
			'amount_total'        => (float) $row->amount_total,
			'amount_paid'         => (float) $row->amount_paid,
			'amount_due'          => (float) $row->amount_due,
			'payment_mode'        => $row->payment_mode,
			'payment_status'      => $row->payment_status,
			'booking_status'      => $row->booking_status,
			'payment_gateway'     => $row->payment_gateway,
			'payment_intent_id'   => $row->payment_intent_id ?? '',
			'notes'               => $row->notes,
			'admin_notes'         => $row->admin_notes,
			'cancelled_at'        => $row->cancelled_at,
			'cancellation_reason' => $row->cancellation_reason,
			'created_at'          => $row->created_at,
			'updated_at'          => $row->updated_at,
		];
	}

	// ── Mark booking as paid (cash) ──────────────────────────────────────────────

	public function mark_booking_paid( WP_REST_Request $request ): WP_REST_Response {
		$ref    = sanitize_text_field( (string) $request->get_param( 'ref' ) );
		$amount = $request->get_param( 'amount' );
		$amount = $amount !== null ? (float) $amount : null;

		$result = ( new Playard_Booking() )->mark_as_paid( $ref, $amount );

		if ( is_wp_error( $result ) ) {
			$code = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$code
			);
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Booking marked as paid.', 'playard' ) ),
			200
		);
	}

	// ── Admin cancel booking ─────────────────────────────────────────────────

	public function cancel_booking_admin( WP_REST_Request $request ): WP_REST_Response {
		$ref    = sanitize_text_field( (string) $request->get_param( 'ref' ) );
		$reason = sanitize_textarea_field( (string) $request->get_param( 'reason' ) );

		$result = ( new Playard_Booking() )->admin_cancel_booking( $ref, $reason );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success( $result, __( 'Booking cancelled.', 'playard' ) ),
			200
		);
	}

	// ── Complete booking ─────────────────────────────────────────────────────

	public function complete_booking_admin( WP_REST_Request $request ): WP_REST_Response {
		$ref    = sanitize_text_field( (string) $request->get_param( 'ref' ) );
		$result = ( new Playard_Booking() )->mark_completed( $ref );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success(
				[ 'booking_ref' => $ref, 'booking_status' => 'completed' ],
				__( 'Booking marked as completed.', 'playard' )
			),
			200
		);
	}

	public function no_show_booking_admin( WP_REST_Request $request ): WP_REST_Response {
		$ref    = sanitize_text_field( (string) $request->get_param( 'ref' ) );
		$result = ( new Playard_Booking() )->mark_no_show( $ref );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success(
				[ 'booking_ref' => $ref, 'booking_status' => 'no_show' ],
				__( 'Booking marked as no-show.', 'playard' )
			),
			200
		);
	}

	// ── Update booking (admin notes + reschedule) ────────────────────────────

	public function update_booking_admin( WP_REST_Request $request ): WP_REST_Response {
		$ref          = sanitize_text_field( (string) $request->get_param( 'ref' ) );
		$booking_date = $request->get_param( 'booking_date' );
		$slots        = $request->get_param( 'slots' );

		// Reschedule path.
		if ( ! empty( $booking_date ) && ! empty( $slots ) ) {
			$data = [
				'booking_date' => sanitize_text_field( (string) $booking_date ),
				'slots'        => array_map( 'sanitize_text_field', (array) $slots ),
			];

			$result = ( new Playard_Booking() )->reschedule_booking( $ref, $data );

			if ( is_wp_error( $result ) ) {
				$http = $result->get_error_code() === 'not_found' ? 404 : 422;
				return new WP_REST_Response(
					playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
					$http
				);
			}

			return new WP_REST_Response(
				playard_rest_success( $result, __( 'Booking rescheduled.', 'playard' ) ),
				200
			);
		}

		// Admin notes path.
		$notes  = sanitize_textarea_field( (string) $request->get_param( 'admin_notes' ) );
		$notes  = mb_substr( $notes, 0, 1000 );
		$result = ( new Playard_Booking() )->update_admin_notes( $ref, $notes );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Notes saved.', 'playard' ) ),
			200
		);
	}

	// ── Walk-in booking ──────────────────────────────────────────────────────

	public function create_walk_in( WP_REST_Request $request ): WP_REST_Response {
		// get_param('customer') goes through WP schema sanitization for type:object,
		// which can silently strip nested properties. Read from the raw JSON body
		// directly to guarantee we receive the customer data as-sent.
		$body         = (array) ( json_decode( $request->get_body(), true ) ?: [] );
		$customer_raw = (array) ( $body['customer'] ?? $request->get_param( 'customer' ) ?? [] );

		$data = [
			'field_id'     => absint( $request->get_param( 'field_id' ) ),
			'booking_date' => sanitize_text_field( (string) $request->get_param( 'booking_date' ) ),
			'slots'        => array_map( 'sanitize_text_field', (array) $request->get_param( 'slots' ) ),
			'sport'        => sanitize_text_field( (string) $request->get_param( 'sport' ) ),
			'customer'     => [
				'name'  => sanitize_text_field( (string) ( $customer_raw['name']  ?? '' ) ),
				'phone' => sanitize_text_field( (string) ( $customer_raw['phone'] ?? '' ) ),
				'email' => sanitize_email( (string) ( $customer_raw['email'] ?? '' ) ),
			],
			'payment_mode' => sanitize_text_field( (string) $request->get_param( 'payment_mode' ) ),
			'admin_notes'  => sanitize_textarea_field( (string) $request->get_param( 'admin_notes' ) ),
		];

		$result = ( new Playard_Booking() )->admin_create_booking( $data );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'slot_unavailable' ? 409 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success( $result, __( 'Walk-in booking created.', 'playard' ) ),
			200
		);
	}

	public function cancel_duplicate_bookings( WP_REST_Request $_request ): WP_REST_Response {
		$cancelled = ( new Playard_Booking() )->cancel_duplicate_bookings();
		return new WP_REST_Response(
			playard_rest_success(
				[ 'cancelled' => $cancelled ],
				// translators: %d is the number of bookings cancelled
				sprintf( _n( '%d duplicate booking cancelled.', '%d duplicate bookings cancelled.', $cancelled, 'playard' ), $cancelled )
			),
			200
		);
	}

	public function customer_lookup( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$phone = sanitize_text_field( (string) $request->get_param( 'phone' ) );

		if ( empty( $phone ) ) {
			return new WP_REST_Response( playard_rest_success( null ), 200 );
		}

		$row = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table lookup by phone; must be live, not cached.
			$wpdb->prepare(
				"SELECT name, phone, email FROM {$wpdb->prefix}playard_customers
				 WHERE phone = %s LIMIT 1",
				$phone
			)
		);

		return new WP_REST_Response(
			playard_rest_success( $row ? [
				'name'  => (string) $row->name,
				'phone' => (string) $row->phone,
				'email' => (string) ( $row->email ?? '' ),
			] : null ),
			200
		);
	}

	// ── Calendar ─────────────────────────────────────────────────────────────

	public function calendar_month( WP_REST_Request $request ): WP_REST_Response {
		$year  = absint( $request->get_param( 'year' ) );
		$month = absint( $request->get_param( 'month' ) );

		if ( $year < 2000 || $year > 2100 || $month < 1 || $month > 12 ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_date', __( 'Invalid year or month.', 'playard' ) ),
				422
			);
		}

		$data = ( new Playard_Report() )->get_month_calendar( $year, $month );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	public function calendar_week( WP_REST_Request $request ): WP_REST_Response {
		$date = sanitize_text_field( (string) $request->get_param( 'date' ) );

		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'invalid_date', __( 'Invalid date format. Use YYYY-MM-DD.', 'playard' ) ),
				422
			);
		}

		$dt         = new \DateTime( $date );
		$dow        = (int) $dt->format( 'N' ); // 1=Mon … 7=Sun
		$dt->modify( '-' . ( $dow - 1 ) . ' days' );
		$week_start = $dt->format( 'Y-m-d' );

		$data = ( new Playard_Report() )->get_week_calendar( $week_start );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	public function booking_payments( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$ref = sanitize_text_field( (string) $request->get_param( 'ref' ) );

		$booking = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table lookup by ref; must be live, not cached.
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}playard_bookings WHERE booking_ref = %s LIMIT 1",
				$ref
			)
		);

		if ( ! $booking ) {
			return new WP_REST_Response(
				playard_rest_error( 'not_found', __( 'Booking not found.', 'playard' ) ),
				404
			);
		}

		$rows = $this->payment->get_booking_payments( (int) $booking->id );

		return new WP_REST_Response( playard_rest_success( $rows ), 200 );
	}

	// ── Customer list ────────────────────────────────────────────────────────

	public function get_customers_admin( WP_REST_Request $request ): WP_REST_Response {
		$args = [
			'search'   => sanitize_text_field( (string) $request->get_param( 'search' ) ),
			'sort'     => sanitize_text_field( (string) $request->get_param( 'sort' ) ),
			'order'    => sanitize_text_field( (string) $request->get_param( 'order' ) ),
			'page'     => absint( $request->get_param( 'page' ) ),
			'per_page' => absint( $request->get_param( 'per_page' ) ),
		];

		$data = ( new Playard_Customer() )->get_customers( $args );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	// ── Create customer (admin) ───────────────────────────────────────────────

	public function create_customer_admin( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$name           = sanitize_text_field( (string) $request->get_param( 'name' ) );
		$phone          = sanitize_text_field( (string) $request->get_param( 'phone' ) );
		$email          = sanitize_email( (string) $request->get_param( 'email' ) );
		$notes          = sanitize_textarea_field( (string) $request->get_param( 'notes' ) );
		$create_wp_user = (bool) $request->get_param( 'create_wp_user' );

		if ( empty( $name ) || empty( $phone ) || empty( $email ) ) {
			return new WP_REST_Response(
				playard_rest_error( 'missing_fields', __( 'Name, phone, and email are required.', 'playard' ) ),
				400
			);
		}

		$customer = new Playard_Customer();
		$phone    = $customer->normalize_phone( $phone );

		// Check if phone already registered
		$exists = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table uniqueness check; must be live, not cached.
			"SELECT id FROM {$wpdb->prefix}playard_customers WHERE phone = %s LIMIT 1",
			$phone
		) );

		if ( $exists ) {
			return new WP_REST_Response(
				playard_rest_error( 'phone_exists', __( 'A customer with this phone number already exists.', 'playard' ) ),
				409
			);
		}

		$result = $customer->get_or_create( [
			'name'  => $name,
			'phone' => $phone,
			'email' => $email,
		] );
		$id = (int) ( $result->id ?? 0 );

		if ( ! $id ) {
			return new WP_REST_Response(
				playard_rest_error( 'create_failed', __( 'Failed to create customer.', 'playard' ) ),
				500
			);
		}

		// Save notes if provided
		if ( $notes ) {
			$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table write; no caching layer applies.
				"{$wpdb->prefix}playard_customers",
				[ 'notes' => $notes ],
				[ 'id'    => $id ],
				[ '%s' ],
				[ '%d' ]
			);
		}

		// Optionally create a WordPress user account
		$wp_user_id = null;
		if ( $create_wp_user && $email ) {
			if ( email_exists( $email ) ) {
				return new WP_REST_Response(
					playard_rest_error( 'wp_email_exists', __( 'A WordPress user with this email already exists.', 'playard' ) ),
					409
				);
			}

			// Derive username from email prefix, ensure uniqueness
			$username_base = sanitize_user( strstr( $email, '@', true ), true );
			if ( empty( $username_base ) ) {
				$username_base = 'customer';
			}
			$username = $username_base;
			$suffix   = 1;
			while ( username_exists( $username ) ) {
				$username = $username_base . $suffix;
				$suffix++;
			}

			$wp_user_id = wp_insert_user( [
				'user_login' => $username,
				'user_email' => $email,
				'display_name' => $name,
				'first_name'   => $name,
				'role'         => 'subscriber',
				'user_pass'    => wp_generate_password( 16, true ),
			] );

			if ( is_wp_error( $wp_user_id ) ) {
				return new WP_REST_Response(
					playard_rest_error( 'wp_user_failed', $wp_user_id->get_error_message() ),
					500
				);
			}

			// Send the WP "new user" notification email with password reset link
			wp_send_new_user_notifications( $wp_user_id, 'user' );
		}

		return new WP_REST_Response(
			playard_rest_success(
				[ 'id' => $id, 'wp_user_id' => $wp_user_id ],
				__( 'Customer created.', 'playard' )
			),
			201
		);
	}

	// ── Customer detail ───────────────────────────────────────────────────────

	public function get_customer_admin( WP_REST_Request $request ): WP_REST_Response {
		$id     = absint( $request->get_param( 'id' ) );
		$result = ( new Playard_Customer() )->get_customer_detail( $id );

		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				404
			);
		}

		return new WP_REST_Response( playard_rest_success( $result ), 200 );
	}

	// ── Customer update (notes) ───────────────────────────────────────────────

	public function update_customer_admin( WP_REST_Request $request ): WP_REST_Response {
		$id    = absint( $request->get_param( 'id' ) );
		$notes = sanitize_textarea_field( (string) $request->get_param( 'notes' ) );
		$notes = mb_substr( $notes, 0, 1000 );

		$result = ( new Playard_Customer() )->update_customer_notes( $id, $notes );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Notes saved.', 'playard' ) ),
			200
		);
	}

	// ── Settings ─────────────────────────────────────────────────────────────

	public function get_settings( WP_REST_Request $_request ): WP_REST_Response {
		$settings = (array) get_option( 'playard_settings', [] );

		$defaults = [
			'cancellation' => [
				'allowed'        => true,
				'refund_type'    => 'time_based',
				'refund_window'  => 24,
				'refund_percent' => 100,
			],
			'payments' => [
				'stripe' => [
					'enabled'              => false,
					'mode'                 => 'test',
					'test_publishable_key' => '',
					'test_secret_key'      => '',
					'live_publishable_key' => '',
					'live_secret_key'      => '',
					'checkout_mode'        => 'embedded',
				],
				'cash'  => [ 'enabled' => true ],
				'order' => [ 'stripe', 'cash', 'paypal' ],
			],
			'currency' => [
				'currency'        => 'USD',
				'symbol'          => '$',
				'symbol_position' => 'before',
				'decimals'        => 0,
			],
			'auth' => [
				'require_pw_otp' => false,
			],
			'notifications' => [
				'from_name'                  => '',
				'from_email'                 => '',
				'account_created'            => true,
				'set_password_invitation'    => true,
				'booking_confirmed_customer' => true,
				'booking_confirmed_owner'    => true,
				'booking_cancelled_customer' => true,
				'booking_status_changed'     => true,
				'reminders_enabled'          => true,
			],
			'report_settings' => [
				'timezone'      => 'facility',
				'default_range' => '30d',
				'week_start'    => 'monday',
			],
		];

		$response = array_replace_recursive( $defaults, $settings );

		return new WP_REST_Response( playard_rest_success( $response ), 200 );
	}

	public function save_settings( WP_REST_Request $request ): WP_REST_Response {
		$body     = (array) $request->get_json_params();
		$settings = (array) get_option( 'playard_settings', [] );

		if ( isset( $body['cancellation'] ) ) {
			$c = (array) $body['cancellation'];

			$settings['cancellation'] = [
				'allowed'        => (bool) ( $c['allowed'] ?? true ),
				'refund_type'    => in_array( $c['refund_type'] ?? '', [ 'full', 'none', 'time_based' ], true )
				                    ? $c['refund_type'] : 'time_based',
				'refund_window'  => absint( $c['refund_window'] ?? 24 ),
				'refund_percent' => min( 100, max( 0, absint( $c['refund_percent'] ?? 100 ) ) ),
			];
		}

		if ( isset( $body['payments'] ) ) {
			$p = (array) $body['payments'];

			if ( isset( $p['stripe'] ) ) {
				$s = (array) $p['stripe'];
				$settings['payments']['stripe'] = [
					'enabled'              => (bool) ( $s['enabled'] ?? false ),
					'mode'                 => in_array( $s['mode'] ?? '', [ 'test', 'live' ], true ) ? $s['mode'] : 'test',
					'test_publishable_key' => sanitize_text_field( (string) ( $s['test_publishable_key'] ?? '' ) ),
					'test_secret_key'      => sanitize_text_field( (string) ( $s['test_secret_key']      ?? '' ) ),
					'live_publishable_key' => sanitize_text_field( (string) ( $s['live_publishable_key'] ?? '' ) ),
					'live_secret_key'      => sanitize_text_field( (string) ( $s['live_secret_key']      ?? '' ) ),
					'checkout_mode'        => in_array( $s['checkout_mode'] ?? '', [ 'embedded', 'hosted' ], true ) ? $s['checkout_mode'] : 'embedded',
				];
			}

			if ( isset( $p['cash'] ) ) {
				$c = (array) $p['cash'];
				$settings['payments']['cash'] = [
					'enabled' => (bool) ( $c['enabled'] ?? true ),
				];
			}

			if ( isset( $p['order'] ) && is_array( $p['order'] ) ) {
				$valid = [ 'stripe', 'cash', 'paypal' ];
				$settings['payments']['order'] = array_values(
					array_filter( (array) $p['order'], fn( $g ) => in_array( $g, $valid, true ) )
				);
			}
		}

		if ( isset( $body['currency'] ) ) {
			$c = (array) $body['currency'];
			$settings['currency'] = [
				'currency'        => sanitize_text_field( (string) ( $c['currency'] ?? 'USD' ) ),
				'symbol'          => sanitize_text_field( (string) ( $c['symbol']   ?? '$' ) ),
				'symbol_position' => in_array( $c['symbol_position'] ?? '', [ 'before', 'after' ], true ) ? $c['symbol_position'] : 'before',
				'decimals'        => min( 3, max( 0, absint( $c['decimals'] ?? 2 ) ) ),
			];
		}

		if ( isset( $body['auth'] ) ) {
			$a = (array) $body['auth'];
			$settings['auth'] = [
				'require_pw_otp' => (bool) ( $a['require_pw_otp'] ?? false ),
			];
		}

		if ( isset( $body['notifications'] ) ) {
			$n = (array) $body['notifications'];
			$settings['notifications'] = [
				'from_name'                  => sanitize_text_field( (string) ( $n['from_name'] ?? '' ) ),
				'from_email'                 => sanitize_email( (string) ( $n['from_email'] ?? '' ) ),
				'account_created'            => (bool) ( $n['account_created'] ?? true ),
				'set_password_invitation'    => (bool) ( $n['set_password_invitation'] ?? true ),
				'booking_confirmed_customer' => (bool) ( $n['booking_confirmed_customer'] ?? true ),
				'booking_confirmed_owner'    => (bool) ( $n['booking_confirmed_owner'] ?? true ),
				'booking_cancelled_customer' => (bool) ( $n['booking_cancelled_customer'] ?? true ),
				'booking_status_changed'     => (bool) ( $n['booking_status_changed'] ?? true ),
				'reminders_enabled'          => (bool) ( $n['reminders_enabled'] ?? true ),
			];
		}

		if ( isset( $body['report_settings'] ) ) {
			$r                           = (array) $body['report_settings'];
			$valid_tz                    = [ 'facility', 'UTC' ];
			$valid_range                 = [ '7d', '30d', '90d', 'month', 'year' ];
			$settings['report_settings'] = [
				'timezone'      => in_array( $r['timezone'] ?? 'facility', $valid_tz, true ) ? $r['timezone'] : 'facility',
				'default_range' => in_array( $r['default_range'] ?? '30d', $valid_range, true ) ? $r['default_range'] : '30d',
				'week_start'    => in_array( $r['week_start'] ?? 'monday', [ 'monday', 'sunday' ], true ) ? $r['week_start'] : 'monday',
			];
		}

		update_option( 'playard_settings', $settings );

		// When payment settings change, sync all fields' payment_mode automatically.
		if ( isset( $body['payments'] ) ) {
			$this->sync_fields_payment_mode( $settings );
		}

		return new WP_REST_Response(
			playard_rest_success( [], __( 'Settings saved.', 'playard' ) ),
			200
		);
	}

	/**
	 * Derives payment_mode for fields from global payment settings and updates all
	 * fields for the current facility. Called by both save_settings() and save_payment().
	 *
	 * @param array<string,mixed> $settings  The full playard_settings array.
	 */
	private function sync_fields_payment_mode( array $settings ): void {
		global $wpdb;

		$facility_id = playard_get_facility_id();
		if ( ! $facility_id ) {
			return;
		}

		$stripe_on = ! empty( $settings['payments']['stripe']['enabled'] );
		$cash_on   = ! empty( $settings['payments']['cash']['enabled'] );

		if ( $stripe_on && $cash_on ) {
			$mode = 'both';
		} elseif ( $stripe_on ) {
			$mode = 'online';
		} else {
			$mode = 'cash';
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update(
			$wpdb->prefix . 'playard_fields',
			[ 'payment_mode' => $mode ],
			[ 'facility_id'  => $facility_id ],
			[ '%s' ],
			[ '%d' ]
		);
	}

	// ── Manual refund ─────────────────────────────────────────────────────────

	public function refund_booking( WP_REST_Request $request ): WP_REST_Response {
		$ref  = sanitize_text_field( (string) $request->get_param( 'ref' ) );
		$body = (array) $request->get_json_params();

		$result = ( new Playard_Payment() )->record_manual_refund( $ref, $body );

		if ( is_wp_error( $result ) ) {
			$http = $result->get_error_code() === 'not_found' ? 404 : 422;
			return new WP_REST_Response(
				playard_rest_error( $result->get_error_code(), $result->get_error_message() ),
				$http
			);
		}

		return new WP_REST_Response(
			playard_rest_success( $result, __( 'Refund recorded.', 'playard' ) ),
			200
		);
	}

	// ── Revenue report ────────────────────────────────────────────────────────

	public function report_revenue( WP_REST_Request $request ): WP_REST_Response {
		$from = sanitize_text_field( (string) $request->get_param( 'from' ) );
		$to   = sanitize_text_field( (string) $request->get_param( 'to' ) );

		$data = ( new Playard_Report() )->get_revenue_report( $from, $to );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	// ── Sport report ──────────────────────────────────────────────────────────

	public function report_sport( WP_REST_Request $request ): WP_REST_Response {
		$from = sanitize_text_field( (string) $request->get_param( 'from' ) );
		$to   = sanitize_text_field( (string) $request->get_param( 'to' ) );

		$data = ( new Playard_Report() )->get_sport_report( $from, $to );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	// ── Booking trends report ─────────────────────────────────────────────────

	public function report_trends( WP_REST_Request $request ): WP_REST_Response {
		$from = sanitize_text_field( (string) $request->get_param( 'from' ) );
		$to   = sanitize_text_field( (string) $request->get_param( 'to' ) );

		$data = ( new Playard_Report() )->get_booking_trends( $from, $to );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	// ── Occupancy report ──────────────────────────────────────────────────────

	public function report_occupancy( WP_REST_Request $request ): WP_REST_Response {
		$from = sanitize_text_field( (string) $request->get_param( 'from' ) );
		$to   = sanitize_text_field( (string) $request->get_param( 'to' ) );

		$data = ( new Playard_Report() )->get_occupancy_report( $from, $to );

		return new WP_REST_Response( playard_rest_success( $data ), 200 );
	}

	// ── Export bookings CSV ───────────────────────────────────────────────────

	public function export_bookings_csv( WP_REST_Request $request ): void {
		global $wpdb;

		$from = sanitize_text_field( (string) $request->get_param( 'from' ) );
		$to   = sanitize_text_field( (string) $request->get_param( 'to' ) );

		$where  = [ '1=1' ];
		$params = [];

		if ( ! empty( $from ) ) {
			$where[]  = 'b.booking_date >= %s';
			$params[] = $from;
		}
		if ( ! empty( $to ) ) {
			$where[]  = 'b.booking_date <= %s';
			$params[] = $to;
		}

		$where_sql = implode( ' AND ', $where );

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$query = "SELECT b.booking_ref, f.name AS field_name, b.sport,
		                 c.name AS customer_name, c.email AS customer_email,
		                 b.booking_date, b.start_time, b.end_time, b.slots_count,
		                 b.amount_total, b.amount_paid,
		                 b.payment_status, b.booking_status, b.payment_gateway,
		                 b.created_at
		          FROM {$wpdb->prefix}playard_bookings b
		          LEFT JOIN {$wpdb->prefix}playard_fields    f ON b.field_id    = f.id
		          LEFT JOIN {$wpdb->prefix}playard_customers c ON b.customer_id = c.id
		          WHERE $where_sql
		          ORDER BY b.booking_date ASC, b.start_time ASC";

		$rows = empty( $params )
			? $wpdb->get_results( $query, ARRAY_A )
			: $wpdb->get_results( $wpdb->prepare( $query, ...$params ), ARRAY_A );
		// phpcs:enable

		$filename = 'bookings-export-' . gmdate( 'Y-m-d' ) . '.csv';

		status_header( 200 );
		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Cache-Control: no-cache, no-store, must-revalidate' );
		header( 'Pragma: no-cache' );

		// BOM for Excel UTF-8 compatibility — written directly to the output stream.
		echo "\xEF\xBB\xBF"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- UTF-8 byte order mark, not user data.

		$out = fopen( 'php://output', 'w' );

		fputcsv( $out, [
			'Booking Ref', 'Field', 'Sport', 'Customer Name', 'Customer Email',
			'Date', 'Start Time', 'End Time', 'Slots Count',
			'Amount Total', 'Amount Paid',
			'Payment Status', 'Booking Status', 'Payment Gateway', 'Created At',
		] );

		foreach ( $rows ?: [] as $row ) {
			fputcsv( $out, [
				$row['booking_ref']     ?? '',
				$row['field_name']      ?? '',
				$row['sport']           ?? '',
				$row['customer_name']   ?? '',
				$row['customer_email']  ?? '',
				$row['booking_date']    ?? '',
				$row['start_time']      ?? '',
				$row['end_time']        ?? '',
				$row['slots_count']     ?? '',
				$row['amount_total']    ?? '',
				$row['amount_paid']     ?? '',
				$row['payment_status']  ?? '',
				$row['booking_status']  ?? '',
				$row['payment_gateway'] ?? '',
				$row['created_at']      ?? '',
			] );
		}

		// php://output closes automatically when the script exits.
		exit;
	}

	// ── Export customers CSV ──────────────────────────────────────────────────

	public function export_customers_csv( WP_REST_Request $request ): void {
		global $wpdb;

		$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table export; one-off report read, not cacheable.
			"SELECT c.name, c.email, c.phone,
			        c.total_bookings, c.total_spent, c.created_at,
			        MAX( b.booking_date ) AS last_booking_date
			 FROM {$wpdb->prefix}playard_customers c
			 LEFT JOIN {$wpdb->prefix}playard_bookings b ON b.customer_id = c.id
			 GROUP BY c.id
			 ORDER BY c.created_at DESC",
			ARRAY_A
		);

		$filename = 'customers-export-' . gmdate( 'Y-m-d' ) . '.csv';

		status_header( 200 );
		header( 'Content-Type: text/csv; charset=UTF-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Cache-Control: no-cache, no-store, must-revalidate' );
		header( 'Pragma: no-cache' );

		echo "\xEF\xBB\xBF"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- UTF-8 byte order mark, not user data.

		$out = fopen( 'php://output', 'w' );

		fputcsv( $out, [ 'Name', 'Email', 'Phone', 'Total Bookings', 'Total Spent', 'Last Booking', 'Member Since' ] );

		foreach ( $rows ?: [] as $row ) {
			fputcsv( $out, [
				$row['name']              ?? '',
				$row['email']             ?? '',
				$row['phone']             ?? '',
				$row['total_bookings']    ?? '0',
				$row['total_spent']       ?? '0',
				$row['last_booking_date'] ?? '',
				$row['created_at']        ?? '',
			] );
		}

		// php://output closes automatically when the script exits.
		exit;
	}

	// ── Team (staff management) ───────────────────────────────────────────────

	private function format_wp_user( WP_User $u ): array {
		return [
			'id'         => $u->ID,
			'name'       => $u->display_name,
			'email'      => $u->user_email,
			'avatar_url' => get_avatar_url( $u->ID, [ 'size' => 40 ] ),
		];
	}

	public function get_team(): WP_REST_Response {
		$users = get_users( [ 'role' => 'playard_staff', 'number' => -1 ] );
		return new WP_REST_Response(
			playard_rest_success( array_map( [ $this, 'format_wp_user' ], $users ) ),
			200
		);
	}

	public function search_team_users( WP_REST_Request $request ): WP_REST_Response {
		$q = sanitize_text_field( (string) $request->get_param( 'q' ) );

		if ( strlen( $q ) < 2 ) {
			return new WP_REST_Response( playard_rest_success( [] ), 200 );
		}

		$staff_ids = get_users( [ 'role' => 'playard_staff', 'fields' => 'ID', 'number' => -1 ] );

		$users = get_users( [
			'search'         => "*{$q}*",
			'search_columns' => [ 'user_login', 'user_email', 'display_name' ],
			'number'         => 10,
			'exclude'        => array_merge( array_map( 'intval', $staff_ids ), [ get_current_user_id() ] ),
		] );

		return new WP_REST_Response(
			playard_rest_success( array_map( [ $this, 'format_wp_user' ], $users ) ),
			200
		);
	}

	public function add_staff( WP_REST_Request $request ): WP_REST_Response {
		$user_id = absint( $request->get_param( 'user_id' ) );
		$user    = get_user_by( 'ID', $user_id );

		if ( ! $user ) {
			return new WP_REST_Response(
				playard_rest_error( 'user_not_found', __( 'WordPress user not found.', 'playard' ) ),
				404
			);
		}

		$user->set_role( 'playard_staff' );

		return new WP_REST_Response( playard_rest_success( $this->format_wp_user( $user ) ), 201 );
	}

	public function remove_staff( WP_REST_Request $request ): WP_REST_Response {
		$user_id = absint( $request->get_param( 'user_id' ) );

		if ( $user_id === get_current_user_id() ) {
			return new WP_REST_Response(
				playard_rest_error( 'cannot_remove_self', __( 'You cannot remove yourself from the team.', 'playard' ) ),
				400
			);
		}

		$user = get_user_by( 'ID', $user_id );

		if ( ! $user ) {
			return new WP_REST_Response(
				playard_rest_error( 'user_not_found', __( 'WordPress user not found.', 'playard' ) ),
				404
			);
		}

		$user->set_role( 'subscriber' );

		return new WP_REST_Response( playard_rest_success( [ 'id' => $user_id ] ), 200 );
	}
}
