<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class BingoCR_REST_API {

	const NS = 'bingocr/v1';

	public function register(): void {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	public function register_routes(): void {
		// ── Public routes ────────────────────────────────────────────────

		register_rest_route( self::NS, '/cars', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_cars' ),
			'permission_callback' => '__return_true',
		) );

		register_rest_route( self::NS, '/cars/available', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_available_cars' ),
			'permission_callback' => '__return_true',
			'args'                => array(
				'pickup_date' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
				'return_date' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
				'category'    => array( 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ),
			),
		) );

		register_rest_route( self::NS, '/cars/categories', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_categories' ),
			'permission_callback' => '__return_true',
		) );

		register_rest_route( self::NS, '/cars/(?P<id>\d+)', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_car' ),
			'permission_callback' => '__return_true',
		) );

		register_rest_route( self::NS, '/locations', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_locations' ),
			'permission_callback' => '__return_true',
		) );

		register_rest_route( self::NS, '/settings/public', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'get_public_settings' ),
			'permission_callback' => '__return_true',
		) );

		register_rest_route( self::NS, '/reservations', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'create_reservation' ),
			'permission_callback' => array( $this, 'verify_nonce' ),
		) );

		// ── Admin routes (manage_options required) ────────────────────────

		register_rest_route( self::NS, '/admin/cars', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'admin_get_cars' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'admin_create_car' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		register_rest_route( self::NS, '/admin/cars/(?P<id>\d+)', array(
			array(
				'methods'             => 'PUT',
				'callback'            => array( $this, 'admin_update_car' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'admin_delete_car' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		register_rest_route( self::NS, '/admin/reservations', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'admin_get_reservations' ),
			'permission_callback' => array( $this, 'is_admin_user' ),
		) );

		register_rest_route( self::NS, '/admin/reservations/(?P<id>\d+)', array(
			'methods'             => 'PUT',
			'callback'            => array( $this, 'admin_update_reservation' ),
			'permission_callback' => array( $this, 'is_admin_user' ),
		) );

		register_rest_route( self::NS, '/admin/reservations/(?P<id>\d+)/payment', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'admin_get_reservation_payment' ),
			'permission_callback' => array( $this, 'is_admin_user' ),
		) );

		register_rest_route( self::NS, '/admin/locations', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'admin_get_locations' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'admin_create_location' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		register_rest_route( self::NS, '/admin/locations/(?P<id>\d+)', array(
			array(
				'methods'             => 'PUT',
				'callback'            => array( $this, 'admin_update_location' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'admin_delete_location' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		register_rest_route( self::NS, '/admin/settings', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'admin_update_settings' ),
			'permission_callback' => array( $this, 'is_admin_user' ),
		) );

		register_rest_route( self::NS, '/admin/dashboard', array(
			'methods'             => 'GET',
			'callback'            => array( $this, 'admin_dashboard_stats' ),
			'permission_callback' => array( $this, 'is_admin_user' ),
		) );

		// ── Pricing calculate (public — used by booking widget) ───────────

		register_rest_route( self::NS, '/pricing/calculate', array(
			'methods'             => 'POST',
			'callback'            => array( $this, 'calculate_price' ),
			'permission_callback' => array( $this, 'verify_nonce' ),
		) );

		// ── Seasonal prices ───────────────────────────────────────────────

		register_rest_route( self::NS, '/admin/seasonal-prices', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'admin_get_seasonal_prices' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'admin_create_seasonal_price' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		register_rest_route( self::NS, '/admin/seasonal-prices/(?P<id>\d+)', array(
			array(
				'methods'             => 'PUT',
				'callback'            => array( $this, 'admin_update_seasonal_price' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'admin_delete_seasonal_price' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		// ── Period pricing ────────────────────────────────────────────────

		register_rest_route( self::NS, '/admin/period-pricing', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'admin_get_period_pricing' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'admin_create_period_price' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );

		register_rest_route( self::NS, '/admin/period-pricing/(?P<id>\d+)', array(
			array(
				'methods'             => 'PUT',
				'callback'            => array( $this, 'admin_update_period_price' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
			array(
				'methods'             => 'DELETE',
				'callback'            => array( $this, 'admin_delete_period_price' ),
				'permission_callback' => array( $this, 'is_admin_user' ),
			),
		) );
	}

	// ── Permission callbacks ─────────────────────────────────────────────

	public function is_admin_user(): bool {
		return current_user_can( 'manage_options' );
	}

	public function verify_nonce( WP_REST_Request $request ): bool {
		$nonce = $request->get_header( 'X-WP-Nonce' );
		return (bool) wp_verify_nonce( $nonce, 'wp_rest' );
	}

	// ── Public handlers ──────────────────────────────────────────────────

	public function get_cars( WP_REST_Request $request ): WP_REST_Response {
		$args = array();
		if ( $request->get_param( 'category' ) ) {
			$args['category'] = sanitize_text_field( $request->get_param( 'category' ) );
		}
		$cars = BingoCR_DB::get_cars( array_merge( array( 'status' => 'available' ), $args ) );
		return new WP_REST_Response( array_map( array( $this, 'prepare_car' ), $cars ) );
	}

	public function get_available_cars( WP_REST_Request $request ): WP_REST_Response {
		$pickup   = $request->get_param( 'pickup_date' );
		$return   = $request->get_param( 'return_date' );
		$category = $request->get_param( 'category' ) ?? '';

		if ( ! self::valid_date( $pickup ) || ! self::valid_date( $return ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Invalid dates.', 'bingocar' ) ), 400 );
		}
		if ( strtotime( $pickup ) >= strtotime( $return ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Return date must be after pickup date.', 'bingocar' ) ), 400 );
		}
		if ( strtotime( $pickup ) < strtotime( gmdate( 'Y-m-d' ) ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Pickup date cannot be in the past.', 'bingocar' ) ), 400 );
		}

		$settings = BingoCR_DB::get_settings();
		$days     = (int) ( ( strtotime( $return ) - strtotime( $pickup ) ) / DAY_IN_SECONDS );

		if ( $days < (int) $settings['min_rental_days'] ) {
			return new WP_REST_Response( array(
				'message' => sprintf(
					/* translators: %d: minimum days */
					__( 'Minimum rental is %d day(s).', 'bingocar' ),
					$settings['min_rental_days']
				),
			), 400 );
		}
		if ( $days > (int) $settings['max_rental_days'] ) {
			return new WP_REST_Response( array(
				'message' => sprintf(
					/* translators: %d: maximum days */
					__( 'Maximum rental is %d day(s).', 'bingocar' ),
					$settings['max_rental_days']
				),
			), 400 );
		}

		$cars = BingoCR_DB::get_available_cars( $pickup, $return, $category );
		return new WP_REST_Response( array_map( array( $this, 'prepare_car' ), $cars ) );
	}

	public function get_categories(): WP_REST_Response {
		return new WP_REST_Response( BingoCR_DB::get_car_categories() );
	}

	public function get_car( WP_REST_Request $request ): WP_REST_Response {
		$car = BingoCR_DB::get_car( (int) $request['id'] );
		if ( ! $car ) {
			return new WP_REST_Response( array( 'message' => __( 'Car not found.', 'bingocar' ) ), 404 );
		}
		return new WP_REST_Response( $this->prepare_car( $car ) );
	}

	public function get_locations(): WP_REST_Response {
		return new WP_REST_Response( BingoCR_DB::get_locations( true ) );
	}

	public function get_public_settings(): WP_REST_Response {
		$s = BingoCR_DB::get_settings();
		return new WP_REST_Response( array(
			'company_name'         => $s['company_name'],
			'currency_symbol'      => $s['currency_symbol'],
			'currency_position'    => $s['currency_position'],
			'date_format'          => $s['date_format'],
			'min_rental_days'      => $s['min_rental_days'],
			'max_rental_days'      => $s['max_rental_days'],
			'advance_booking_days' => $s['advance_booking_days'],
			'default_deposit'      => (float) ( $s['default_deposit'] ?? 0 ),
			'language'             => $s['language'],
			'auto_detect_language' => $s['auto_detect_language'],
			'show_attribution'     => $s['show_attribution'],
			'theme'                => $s['theme'],
			'primary_color'        => $s['primary_color'],
		) );
	}

	public function create_reservation( WP_REST_Request $request ): WP_REST_Response {
		$body = $request->get_json_params();
		if ( empty( $body ) ) {
			$body = $request->get_params();
		}

		$required = array( 'car_id', 'customer_name', 'customer_email', 'pickup_date', 'return_date' );
		foreach ( $required as $field ) {
			if ( empty( $body[ $field ] ) ) {
				return new WP_REST_Response(
					array(
						'message' => sprintf(
							/* translators: %s: field name */
							__( 'Field %s is required.', 'bingocar' ),
							$field
						),
					),
					400
				);
			}
		}

		$pickup  = sanitize_text_field( $body['pickup_date'] );
		$return  = sanitize_text_field( $body['return_date'] );
		$car_id  = absint( $body['car_id'] );

		if ( ! self::valid_date( $pickup ) || ! self::valid_date( $return ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Invalid dates.', 'bingocar' ) ), 400 );
		}
		// Full date-range validation (mirrors the availability search) — the create
		// endpoint must not trust that the client already validated.
		if ( strtotime( $pickup ) >= strtotime( $return ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Return date must be after pickup date.', 'bingocar' ) ), 400 );
		}
		if ( strtotime( $pickup ) < strtotime( gmdate( 'Y-m-d' ) ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Pickup date cannot be in the past.', 'bingocar' ) ), 400 );
		}
		$settings     = BingoCR_DB::get_settings();
		$bingocr_days = (int) ( ( strtotime( $return ) - strtotime( $pickup ) ) / DAY_IN_SECONDS );
		if ( $bingocr_days < (int) $settings['min_rental_days'] ) {
			/* translators: %d: minimum days */
			return new WP_REST_Response( array( 'message' => sprintf( __( 'Minimum rental is %d day(s).', 'bingocar' ), (int) $settings['min_rental_days'] ) ), 400 );
		}
		if ( $bingocr_days > (int) $settings['max_rental_days'] ) {
			/* translators: %d: maximum days */
			return new WP_REST_Response( array( 'message' => sprintf( __( 'Maximum rental is %d day(s).', 'bingocar' ), (int) $settings['max_rental_days'] ) ), 400 );
		}

		$car = BingoCR_DB::get_car( $car_id );
		if ( ! $car ) {
			return new WP_REST_Response( array( 'message' => __( 'Car not found.', 'bingocar' ) ), 404 );
		}

		// Serialize the availability check + insert per car (named DB lock) so two
		// simultaneous bookings can't both pass the capacity check and overbook.
		// Real contention (timeout) fails closed; an unavailable lock degrades to no lock.
		$bingocr_lock = BingoCR_DB::acquire_booking_lock( $car_id );
		if ( false === $bingocr_lock ) {
			return new WP_REST_Response( array( 'message' => __( 'This vehicle is being booked right now. Please try again in a moment.', 'bingocar' ) ), 503 );
		}
		$bingocr_locked = ( true === $bingocr_lock ); // null => proceed without the lock
		$booked         = BingoCR_DB::get_booked_count( $car_id, $pickup, $return );
		if ( $booked >= (int) $car['quantity'] ) {
			if ( $bingocr_locked ) { BingoCR_DB::release_booking_lock( $car_id ); }
			return new WP_REST_Response( array( 'message' => __( 'Car is no longer available for those dates.', 'bingocar' ) ), 409 );
		}

		// ── Authoritative pricing (server-side, never trust the client) ──────────
		// The browser's total_price is for display only. We recompute the real total
		// here with BingoCR_Pricing::calculate(), so a crafted request cannot set its
		// own price. Extras and discounts are premium-only and gated — calculate()
		// ignores them on the free build (premium code stripped), so they cannot be
		// injected here even if the gate were bypassed.
		$pickup_time = sanitize_text_field( $body['pickup_time'] ?? '10:00' );
		$return_time = sanitize_text_field( $body['return_time'] ?? '10:00' );
		// Gate on the stripped premium class, not just can() — so a free build (where
		// BingoCR_Premium is physically absent) never stores extras/discount on the
		// reservation even if the can() gate is edited. Keeps the stored record — and
		// therefore the confirmation emails — consistent with the server-priced total.
		$bingocr_premium_on = class_exists( 'BingoCR_Premium' );
		$extra_ids   = ( $bingocr_premium_on && BingoCR_License::can( 'extras' ) && isset( $body['extras'] ) && is_array( $body['extras'] ) )
			? array_values( array_filter( array_map( 'absint', $body['extras'] ) ) )
			: array();
		$discount_in = ( $bingocr_premium_on && BingoCR_License::can( 'discounts' ) ) ? sanitize_text_field( $body['discount_code'] ?? '' ) : '';

		$quote        = BingoCR_Pricing::calculate( $car, $pickup, $return, $extra_ids, $discount_in, $pickup_time, $return_time );
		$total_price  = (float) $quote['total'];
		$discount_amt = (float) $quote['discount_amount'];
		$discount_id  = (int) ( $quote['discount_id'] ?? 0 );
		$discount_out = (string) ( $quote['discount_code'] ?? '' ); // only a code that was actually applied

		// Payment method — accept only methods the admin has enabled.
		// ($settings was already loaded during date validation above.)
		$enabled_methods = array();
		if ( ! empty( $settings['payment_cash'] ) ) { $enabled_methods[] = 'cash'; }
		if ( ! empty( $settings['payment_card'] ) ) { $enabled_methods[] = 'card'; }
		if ( ! empty( $settings['payment_bank'] ) ) { $enabled_methods[] = 'bank'; }
		if ( class_exists( 'BingoCR_PayPal' ) && ( new BingoCR_PayPal() )->is_ready() ) {
			$enabled_methods[] = 'paypal';
		}
		$payment_method = sanitize_text_field( $body['payment_method'] ?? '' );
		if ( ! in_array( $payment_method, $enabled_methods, true ) ) {
			$payment_method = $enabled_methods ? $enabled_methods[0] : 'cash';
		}
		// PayPal is captured separately; defer the confirmation email until paid.
		$is_online = ( 'paypal' === $payment_method );

		$data = array(
			'car_id'              => $car_id,
			'customer_name'       => sanitize_text_field( $body['customer_name'] ),
			'customer_email'      => sanitize_email( $body['customer_email'] ),
			'customer_phone'      => sanitize_text_field( $body['customer_phone'] ?? '' ),
			'pickup_date'         => $pickup,
			'return_date'         => $return,
			'pickup_time'         => $pickup_time,
			'return_time'         => $return_time,
			'pickup_location_id'  => absint( $body['pickup_location_id'] ?? 0 ) ?: null,
			'dropoff_location_id' => absint( $body['dropoff_location_id'] ?? 0 ) ?: null,
			'total_price'         => $total_price,
			'deposit_amount'      => self::compute_deposit( $car, $total_price, $settings ),
			'discount_code'       => $discount_out,
			'discount_amount'     => $discount_amt,
			'extras'              => $extra_ids,
			'notes'               => sanitize_textarea_field( $body['notes'] ?? '' ),
			'payment_method'      => $payment_method,
			'payment_status'      => 'unpaid',
			'language'            => sanitize_text_field( $body['language'] ?? 'en' ),
		);

		$id = BingoCR_DB::insert_reservation( $data );
		if ( $bingocr_locked ) { BingoCR_DB::release_booking_lock( $car_id ); }
		if ( ! $id ) {
			return new WP_REST_Response( array( 'message' => __( 'Could not create reservation.', 'bingocar' ) ), 500 );
		}

		if ( $discount_id ) {
			BingoCR_DB::increment_discount_usage( $discount_id );
		}

		$reservation = BingoCR_DB::get_reservation( $id );
		// For PayPal the emails fire after a successful capture, not here.
		if ( ! $is_online ) {
			BingoCR_Email::send_customer_confirmation( $reservation );
			BingoCR_Email::send_admin_notification( $reservation );
		}

		return new WP_REST_Response( array(
			'id'                 => $id,
			'reservation_number' => $reservation['reservation_number'],
			'total_price'        => $reservation['total_price'],
			'status'             => $reservation['status'],
		), 201 );
	}

	// ── Admin handlers ───────────────────────────────────────────────────

	public function admin_get_cars(): WP_REST_Response {
		$cars = BingoCR_DB::get_cars();
		return new WP_REST_Response( array_map( array( $this, 'prepare_car' ), $cars ) );
	}

	public function admin_create_car( WP_REST_Request $request ): WP_REST_Response {
		$data = $this->sanitize_car_data( $request->get_json_params() );
		$id   = BingoCR_DB::insert_car( $data );
		if ( ! $id ) {
			return new WP_REST_Response( array( 'message' => __( 'Could not create car.', 'bingocar' ) ), 500 );
		}
		return new WP_REST_Response( BingoCR_DB::get_car( $id ), 201 );
	}

	public function admin_update_car( WP_REST_Request $request ): WP_REST_Response {
		$id   = (int) $request['id'];
		$data = $this->sanitize_car_data( $request->get_json_params() );
		BingoCR_DB::update_car( $id, $data );
		return new WP_REST_Response( BingoCR_DB::get_car( $id ) );
	}

	public function admin_delete_car( WP_REST_Request $request ): WP_REST_Response {
		BingoCR_DB::delete_car( (int) $request['id'] );
		return new WP_REST_Response( null, 204 );
	}

	public function admin_get_reservations( WP_REST_Request $request ): WP_REST_Response {
		$args = array(
			'status' => sanitize_text_field( $request->get_param( 'status' ) ?? '' ),
			'search' => sanitize_text_field( $request->get_param( 'search' ) ?? '' ),
			'limit'  => absint( $request->get_param( 'limit' ) ?? 50 ),
			'offset' => absint( $request->get_param( 'offset' ) ?? 0 ),
		);
		return new WP_REST_Response( BingoCR_DB::get_reservations( $args ) );
	}

	public function admin_get_reservation_payment( WP_REST_Request $request ): WP_REST_Response {
		$id      = (int) $request['id'];
		$payment = BingoCR_DB::get_reservation_payment( $id );
		if ( ! $payment ) {
			return new WP_REST_Response( null );
		}

		// Pull the PayPal fee/net out of the stored capture response, if present.
		$fee = null;
		$net = null;
		if ( ! empty( $payment['gateway_response'] ) ) {
			$resp = json_decode( $payment['gateway_response'], true );
			$bd   = $resp['purchase_units'][0]['payments']['captures'][0]['seller_receivable_breakdown'] ?? null;
			if ( is_array( $bd ) ) {
				$fee = $bd['paypal_fee']['value'] ?? null;
				$net = $bd['net_amount']['value'] ?? null;
			}
		}

		// Return only safe, display-ready fields — never the raw gateway blob.
		return new WP_REST_Response( array(
			'method'       => $payment['method'],
			'status'       => $payment['status'],
			'amount'       => $payment['amount'],
			'currency'     => $payment['currency'],
			'order_id'     => $payment['paypal_order_id'],
			'capture_id'   => $payment['paypal_capture_id'],
			'payer_email'  => $payment['payer_email'],
			'completed_at' => $payment['completed_at'],
			'fee'          => $fee,
			'net'          => $net,
		) );
	}

	public function admin_update_reservation( WP_REST_Request $request ): WP_REST_Response {
		$id   = (int) $request['id'];
		$body = $request->get_json_params();
		$data = array();

		if ( isset( $body['status'] ) ) {
			$data['status'] = sanitize_text_field( $body['status'] );
		}
		if ( isset( $body['notes'] ) ) {
			$data['notes'] = sanitize_textarea_field( $body['notes'] );
		}

		$old = BingoCR_DB::get_reservation( $id );
		BingoCR_DB::update_reservation( $id, $data );
		$updated = BingoCR_DB::get_reservation( $id );

		// Notify the customer when the status actually changed to confirmed/cancelled.
		if (
			$old && $updated
			&& isset( $data['status'] )
			&& $old['status'] !== $updated['status']
			&& in_array( $updated['status'], array( 'confirmed', 'cancelled' ), true )
		) {
			BingoCR_Email::send_status_update( $updated );
		}

		return new WP_REST_Response( $updated );
	}

	public function admin_get_locations(): WP_REST_Response {
		return new WP_REST_Response( BingoCR_DB::get_locations( false ) );
	}

	public function admin_create_location( WP_REST_Request $request ): WP_REST_Response {
		$body = $request->get_json_params();
		$data = array(
			'name'          => sanitize_text_field( $body['name'] ?? '' ),
			'address'       => sanitize_textarea_field( $body['address'] ?? '' ),
			'city'          => sanitize_text_field( $body['city'] ?? '' ),
			'phone'         => sanitize_text_field( $body['phone'] ?? '' ),
			'email'         => sanitize_email( $body['email'] ?? '' ),
			'opening_hours' => sanitize_text_field( $body['opening_hours'] ?? '' ),
			'status'        => sanitize_text_field( $body['status'] ?? 'active' ),
			'sort_order'    => absint( $body['sort_order'] ?? 0 ),
		);
		$id = BingoCR_DB::insert_location( $data );
		return new WP_REST_Response( BingoCR_DB::get_location( $id ), 201 );
	}

	public function admin_update_location( WP_REST_Request $request ): WP_REST_Response {
		$id   = (int) $request['id'];
		$body = $request->get_json_params();
		$data = array(
			'name'          => sanitize_text_field( $body['name'] ?? '' ),
			'address'       => sanitize_textarea_field( $body['address'] ?? '' ),
			'city'          => sanitize_text_field( $body['city'] ?? '' ),
			'phone'         => sanitize_text_field( $body['phone'] ?? '' ),
			'email'         => sanitize_email( $body['email'] ?? '' ),
			'opening_hours' => sanitize_text_field( $body['opening_hours'] ?? '' ),
			'status'        => sanitize_text_field( $body['status'] ?? 'active' ),
			'sort_order'    => absint( $body['sort_order'] ?? 0 ),
		);
		BingoCR_DB::update_location( $id, $data );
		return new WP_REST_Response( BingoCR_DB::get_location( $id ) );
	}

	public function admin_delete_location( WP_REST_Request $request ): WP_REST_Response {
		BingoCR_DB::delete_location( (int) $request['id'] );
		return new WP_REST_Response( null, 204 );
	}

	public function admin_update_settings( WP_REST_Request $request ): WP_REST_Response {
		$body     = $request->get_json_params();
		$allowed  = array(
			'company_name', 'currency', 'currency_symbol', 'currency_position',
			'date_format', 'min_rental_days', 'max_rental_days', 'advance_booking_days',
			'default_deposit', 'notify_email', 'from_name', 'from_email',
			'language', 'auto_detect_language', 'show_attribution',
			'theme', 'primary_color',
			'business_hours_start', 'business_hours_end',
			'out_of_hours_enabled', 'out_of_hours_fee',
			'terms_conditions',
			'payment_cash', 'payment_card', 'payment_bank', 'payment_instructions',
			'paypal_enabled', 'paypal_mode', 'paypal_client_id', 'paypal_client_secret', 'paypal_webhook_id',
		);
		$multiline = array( 'terms_conditions', 'payment_instructions' );
		$data = array();
		foreach ( $allowed as $key ) {
			if ( isset( $body[ $key ] ) ) {
				if ( in_array( $key, $multiline, true ) ) {
					// Multi-line rich text — keep safe HTML and line breaks.
					$data[ $key ] = wp_kses_post( $body[ $key ] );
				} else {
					$data[ $key ] = is_bool( $body[ $key ] ) ? (bool) $body[ $key ] : sanitize_text_field( $body[ $key ] );
				}
			}
		}
		// PayPal client secret: write-only + encrypted at rest. An empty submission
		// keeps the stored secret (so re-saving other settings never wipes it); a new
		// value is encrypted before storage so it is never plain text in wp_options.
		if ( array_key_exists( 'paypal_client_secret', $data ) ) {
			$bingocr_new_secret = trim( (string) $data['paypal_client_secret'] );
			if ( '' === $bingocr_new_secret ) {
				unset( $data['paypal_client_secret'] );
			} else {
				$data['paypal_client_secret'] = BingoCR_Crypto::encrypt( $bingocr_new_secret );
			}
		}

		BingoCR_DB::update_settings( $data );
		// Never echo the secret back to the browser — redact before responding.
		return new WP_REST_Response( BingoCR_Crypto::redact_settings( BingoCR_DB::get_settings() ) );
	}

	public function admin_dashboard_stats(): WP_REST_Response {
		global $wpdb;
		$cars_table = $wpdb->prefix . 'bingocr_cars';
		return new WP_REST_Response( array(
			'total_cars'         => (int) $wpdb->get_var( "SELECT COUNT(*) FROM `$cars_table`" ), // phpcs:ignore
			'total_reservations' => BingoCR_DB::count_reservations(),
			'pending'            => BingoCR_DB::count_reservations( 'pending' ),
			'confirmed'          => BingoCR_DB::count_reservations( 'confirmed' ),
			'revenue'            => BingoCR_DB::get_revenue_total(),
		) );
	}

	// ── Helpers ──────────────────────────────────────────────────────────

	private function prepare_car( array $car ): array {
		if ( is_string( $car['features'] ) && $car['features'] ) {
			$car['features'] = json_decode( $car['features'], true ) ?: array();
		}
		$car['price_per_day'] = (float) $car['price_per_day'];
		$car['seats']         = (int) $car['seats'];
		$car['quantity']      = (int) $car['quantity'];
		if ( isset( $car['available_units'] ) ) {
			$car['available_units'] = (int) $car['available_units'];
		}
		if ( $car['image_id'] ) {
			$src = wp_get_attachment_image_url( (int) $car['image_id'], 'large' );
			if ( $src ) {
				$car['image_url'] = $src;
			}
		}
		return $car;
	}

	private function sanitize_car_data( array $body ): array {
		return array(
			'brand'         => sanitize_text_field( $body['brand'] ?? '' ),
			'model'         => sanitize_text_field( $body['model'] ?? '' ),
			'year'          => absint( $body['year'] ?? 0 ) ?: null,
			'category'      => sanitize_text_field( $body['category'] ?? '' ),
			'transmission'  => sanitize_text_field( $body['transmission'] ?? 'manual' ),
			'fuel_type'     => sanitize_text_field( $body['fuel_type'] ?? 'petrol' ),
			'seats'         => absint( $body['seats'] ?? 5 ),
			'price_per_day' => (float) ( $body['price_per_day'] ?? 0 ),
			'deposit_type'  => sanitize_text_field( $body['deposit_type'] ?? 'default' ),
			'deposit_value' => (float) ( $body['deposit_value'] ?? 0 ),
			'image_id'      => absint( $body['image_id'] ?? 0 ) ?: null,
			'image_url'     => esc_url_raw( $body['image_url'] ?? '' ),
			'description'   => wp_kses_post( $body['description'] ?? '' ),
			'features'      => isset( $body['features'] ) && is_array( $body['features'] )
				? array_map( 'sanitize_text_field', $body['features'] ) : array(),
			'quantity'      => max( 1, absint( $body['quantity'] ?? 1 ) ),
			'status'        => sanitize_text_field( $body['status'] ?? 'available' ),
			'sort_order'    => absint( $body['sort_order'] ?? 0 ),
		);
	}

	private static function valid_date( string $date ): bool {
		$d = DateTime::createFromFormat( 'Y-m-d', $date );
		return $d && $d->format( 'Y-m-d' ) === $date;
	}

	/**
	 * Security deposit, computed server-side from the car + global settings — never
	 * trusted from the request. Mirrors the frontend's calcDeposit():
	 * percentage of total, flat amount, or the global default.
	 */
	private static function compute_deposit( array $car, float $total, array $settings ): float {
		$type  = $car['deposit_type'] ?? 'default';
		$value = (float) ( $car['deposit_value'] ?? 0 );
		if ( 'percentage' === $type ) {
			return round( $total * ( $value / 100 ), 2 );
		}
		if ( 'flat' === $type ) {
			return round( $value, 2 );
		}
		return round( (float) ( $settings['default_deposit'] ?? 0 ), 2 );
	}

	// ── Pricing calculate ────────────────────────────────────────────────

	public function calculate_price( WP_REST_Request $request ): WP_REST_Response {
		$body    = $request->get_json_params() ?: $request->get_params();
		$car_id  = absint( $body['car_id'] ?? 0 );
		$pickup  = sanitize_text_field( $body['pickup_date'] ?? '' );
		$return  = sanitize_text_field( $body['return_date'] ?? '' );
		$extras  = isset( $body['extras'] ) && is_array( $body['extras'] )
			? array_map( 'absint', $body['extras'] ) : array();
		$code    = sanitize_text_field( $body['discount_code'] ?? '' );
		$ptime   = sanitize_text_field( $body['pickup_time'] ?? '' );
		$rtime   = sanitize_text_field( $body['return_time'] ?? '' );

		if ( ! $car_id || ! self::valid_date( $pickup ) || ! self::valid_date( $return ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Invalid request parameters.', 'bingocar' ) ), 400 );
		}
		if ( strtotime( $pickup ) >= strtotime( $return ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Return date must be after pickup date.', 'bingocar' ) ), 400 );
		}
		if ( strtotime( $pickup ) < strtotime( gmdate( 'Y-m-d' ) ) ) {
			return new WP_REST_Response( array( 'message' => __( 'Pickup date cannot be in the past.', 'bingocar' ) ), 400 );
		}
		$bingocr_settings = BingoCR_DB::get_settings();
		$bingocr_days     = (int) ( ( strtotime( $return ) - strtotime( $pickup ) ) / DAY_IN_SECONDS );
		if ( $bingocr_days < (int) $bingocr_settings['min_rental_days'] ) {
			/* translators: %d: minimum days */
			return new WP_REST_Response( array( 'message' => sprintf( __( 'Minimum rental is %d day(s).', 'bingocar' ), (int) $bingocr_settings['min_rental_days'] ) ), 400 );
		}
		if ( $bingocr_days > (int) $bingocr_settings['max_rental_days'] ) {
			/* translators: %d: maximum days */
			return new WP_REST_Response( array( 'message' => sprintf( __( 'Maximum rental is %d day(s).', 'bingocar' ), (int) $bingocr_settings['max_rental_days'] ) ), 400 );
		}

		$car = BingoCR_DB::get_car( $car_id );
		if ( ! $car ) {
			return new WP_REST_Response( array( 'message' => __( 'Car not found.', 'bingocar' ) ), 404 );
		}

		return new WP_REST_Response( BingoCR_Pricing::calculate( $car, $pickup, $return, $extras, $code, $ptime, $rtime ) );
	}

	// ── Seasonal prices ──────────────────────────────────────────────────

	public function admin_get_seasonal_prices(): WP_REST_Response {
		return new WP_REST_Response( BingoCR_DB::get_seasonal_prices() );
	}

	public function admin_create_seasonal_price( WP_REST_Request $request ): WP_REST_Response {
		$data = $this->sanitize_seasonal_data( $request->get_json_params() );
		$id   = BingoCR_DB::insert_seasonal_price( $data );
		if ( ! $id ) {
			return new WP_REST_Response( array( 'message' => __( 'Could not create seasonal price.', 'bingocar' ) ), 500 );
		}
		return new WP_REST_Response( BingoCR_DB::get_seasonal_price( $id ), 201 );
	}

	public function admin_update_seasonal_price( WP_REST_Request $request ): WP_REST_Response {
		$id   = (int) $request['id'];
		$data = $this->sanitize_seasonal_data( $request->get_json_params() );
		BingoCR_DB::update_seasonal_price( $id, $data );
		return new WP_REST_Response( BingoCR_DB::get_seasonal_price( $id ) );
	}

	public function admin_delete_seasonal_price( WP_REST_Request $request ): WP_REST_Response {
		BingoCR_DB::delete_seasonal_price( (int) $request['id'] );
		return new WP_REST_Response( null, 204 );
	}

	private function sanitize_seasonal_data( array $body ): array {
		return array(
			'name'       => sanitize_text_field( $body['name'] ?? '' ),
			'start_date' => sanitize_text_field( $body['start_date'] ?? '' ),
			'end_date'   => sanitize_text_field( $body['end_date'] ?? '' ),
			'modifier'   => (float) ( $body['modifier'] ?? 0 ),
			'applies_to' => sanitize_text_field( $body['applies_to'] ?? 'all' ),
			'category'   => sanitize_text_field( $body['category'] ?? '' ),
			'priority'   => absint( $body['priority'] ?? 0 ),
			'status'     => sanitize_text_field( $body['status'] ?? 'active' ),
		);
	}

	// ── Period pricing ───────────────────────────────────────────────────

	public function admin_get_period_pricing(): WP_REST_Response {
		return new WP_REST_Response( BingoCR_DB::get_period_pricing() );
	}

	public function admin_create_period_price( WP_REST_Request $request ): WP_REST_Response {
		$data = $this->sanitize_period_data( $request->get_json_params() );
		$id   = BingoCR_DB::insert_period_price( $data );
		if ( ! $id ) {
			return new WP_REST_Response( array( 'message' => __( 'Could not create pricing rule.', 'bingocar' ) ), 500 );
		}
		return new WP_REST_Response( BingoCR_DB::get_period_price( $id ), 201 );
	}

	public function admin_update_period_price( WP_REST_Request $request ): WP_REST_Response {
		$id   = (int) $request['id'];
		$data = $this->sanitize_period_data( $request->get_json_params() );
		BingoCR_DB::update_period_price( $id, $data );
		return new WP_REST_Response( BingoCR_DB::get_period_price( $id ) );
	}

	public function admin_delete_period_price( WP_REST_Request $request ): WP_REST_Response {
		BingoCR_DB::delete_period_price( (int) $request['id'] );
		return new WP_REST_Response( null, 204 );
	}

	private function sanitize_period_data( array $body ): array {
		return array(
			'name'         => sanitize_text_field( $body['name'] ?? '' ),
			'pricing_type' => sanitize_text_field( $body['pricing_type'] ?? 'rate_per_day' ),
			'min_days'     => max( 1, absint( $body['min_days'] ?? 1 ) ),
			'max_days'     => max( 1, absint( $body['max_days'] ?? 7 ) ),
			'fixed_price'  => (float) ( $body['fixed_price'] ?? 0 ),
			'price_per_day' => (float) ( $body['price_per_day'] ?? 0 ),
			'applies_to'   => sanitize_text_field( $body['applies_to'] ?? 'all' ),
			'category'     => sanitize_text_field( $body['category'] ?? '' ),
			'car_id'       => absint( $body['car_id'] ?? 0 ) ?: null,
			'priority'     => absint( $body['priority'] ?? 0 ),
			'status'       => sanitize_text_field( $body['status'] ?? 'active' ),
		);
	}
}
