<?php
/**
 * PostGate Orders REST API Controller
 *
 * Provides REST API endpoints for managing customer orders, subscriptions, and payments.
 * Handles order listing with advanced filtering, pagination, and search capabilities,
 * as well as order-specific actions like refunds and subscription cancellations.
 *
 * @package PostGate
 * @subpackage API
 * @since 1.0.0
 * @author QodeSphere
 * @license GPL-2.0+
 */

defined( 'ABSPATH' ) || exit;

/**
 * PostGate Orders REST API Controller
 *
 * This controller manages all REST API operations related to orders within the PostGate plugin.
 * It provides comprehensive order management functionality including:
 *
 * - Order listing with advanced filtering (status, type, currency)
 * - Full-text search across order IDs and Stripe identifiers
 * - Pagination and sorting capabilities
 * - Order refunds for one-time purchases
 * - Subscription cancellations (immediate or at period end)
 * - User enrichment with customer details
 * - Object caching for improved performance
 *
 * REST API Endpoints:
 * - GET  /postgate/v1/orders - List and filter orders
 * - POST /postgate/v1/orders/{id}/refund - Refund a one-time order
 * - POST /postgate/v1/orders/{id}/cancel - Cancel a subscription
 *
 * All endpoints require 'manage_options' capability and interact directly with Stripe API
 * for payment processing operations. Order data is cached using WordPress object cache
 * with a 120-second TTL for optimal performance.
 *
 * @since 1.0.0
 */
class PostGate_Orders_Controller {

	/**
	 * Namespace for the REST API.
	 *
	 * @var string
	 */
	const NAMESPACE = 'postgate/v1';
	/**
	 * Route for orders.
	 *
	 * @var string
	 */
	const ROUTE = '/orders';

	/**
	 * Object cache group.
	 *
	 * @since 1.0.0
	 */
	private const CACHE_GROUP = 'postgate_orders';

	/**
	 * Default cache TTL (seconds).
	 *
	 * @since 1.0.0
	 */
	private const CACHE_TTL = 120;

	/**
	 * Initialize the orders controller.
	 *
	 * Registers the REST API initialization hook to set up all order-related endpoints.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function init() {
		add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] );
	}

	/**
	 * Register all REST API routes for order management.
	 *
	 * Registers three main endpoints:
	 * 1. GET /orders - List orders with filtering, pagination, and search
	 * 2. POST /orders/{id}/refund - Refund a one-time purchase
	 * 3. POST /orders/{id}/cancel - Cancel a subscription
	 *
	 * All endpoints require administrator privileges (manage_options capability).
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function register_routes() {
		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE,
			[
				'methods' => 'GET',
				'callback' => [ __CLASS__, 'get_orders' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'page' => [ 'type' => 'integer', 'default' => 1, 'minimum' => 1 ],
					'per_page' => [ 'type' => 'integer', 'default' => 10, 'minimum' => 1, 'maximum' => 100 ],
					'search' => [ 'type' => 'string', 'required' => false ],
					'orderby' => [ 'type' => 'string', 'required' => false ],
					'order' => [ 'type' => 'string', 'required' => false ],
					'status' => [ 'type' => 'string', 'required' => false ],
					'type' => [ 'type' => 'string', 'required' => false ],
					'currency' => [ 'type' => 'string', 'required' => false ],
				],
			]
		);

		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE . '/(?P<id>\d+)/refund',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'refund_order' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'reason' => [ 'type' => 'string', 'required' => false ],
				],
			]
		);

		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE . '/(?P<id>\d+)/cancel',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'cancel_subscription' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'reason' => [ 'type' => 'string', 'required' => false ],
					'cancel_at_period_end' => [ 'type' => 'boolean', 'required' => false, 'default' => true ],
				],
			]
		);

		// Manual order creation endpoint
		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE . '/manual',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'create_manual_order' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'user_id' => [
						'required' => true,
						'type' => 'integer',
						'validate_callback' => function ( $param ) {
							return is_numeric( $param ) && $param > 0;
						},
					],
					'product_id' => [
						'required' => true,
						'type' => 'string',
						'validate_callback' => function ( $param ) {
							return ! empty( $param );
						},
					],
					'price_id' => [
						'required' => true,
						'type' => 'string',
						'validate_callback' => function ( $param ) {
							return ! empty( $param );
						},
					],
					'amount' => [
						'required' => true,
						'type' => 'number',
						'validate_callback' => function ( $param ) {
							return is_numeric( $param ) && $param > 0;
						},
					],
					'stripe_payment_intent' => [
						'required' => false,
						'validate_callback' => function ( $param ) {
							// Empty string is allowed (for offline/Type 2 orders)
							// If provided, must start with 'pi_'
							return empty( $param ) || str_starts_with( (string) $param, 'pi_' );
						},
					],
					'notes' => [
						'required' => false,
						'type' => 'string',
					],
					'grant_access' => [
						'required' => false,
						'type' => 'boolean',
						'default' => true,
					],
				],
			]
		);

		// Update order endpoint
		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE . '/(?P<id>\d+)',
			[
				'methods' => 'PUT',
				'callback' => [ __CLASS__, 'update_order' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'product_id' => [
						'required' => false,
						'type' => 'string',
					],
					'price_id' => [
						'required' => false,
						'type' => 'string',
					],
					'amount' => [
						'required' => false,
						'validate_callback' => function ( $param ) {
							return is_numeric( $param ) && $param > 0;
						},
					],
					'grant_access' => [
						'required' => false,
						'type' => 'boolean',
					],
					'notes' => [
						'required' => false,
						'type' => 'string',
					],
				],
			]
		);

		// Verify Stripe Payment Intent endpoint
		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE . '/verify-payment-intent/(?P<intent_id>[a-zA-Z0-9_]+)',
			[
				'methods' => 'GET',
				'callback' => [ __CLASS__, 'verify_payment_intent' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
			]
		);

		// Verify Intent endpoint (POST for frontend verification)
		register_rest_route(
			self::NAMESPACE ,
			self::ROUTE . '/verify-intent',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'verify_intent_post' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'stripe_payment_intent' => [
						'required' => true,
						'type' => 'string',
						'validate_callback' => function ( $param ) {
							return str_starts_with( (string) $param, 'pi_' );
						},
					],
				],
			]
		);
	}

	/**
	 * Check if the current user has permission to access order endpoints.
	 *
	 * Verifies that the current user has administrator privileges required to
	 * manage orders, process refunds, and cancel subscriptions.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if user can manage options, false otherwise.
	 */
	public static function check_permission() {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Generate a cache key for order data.
	 *
	 * Creates a unique cache key by hashing the provided suffix to ensure
	 * consistent cache key generation for order-related data.
	 *
	 * @since 1.0.0
	 *
	 * @param string $suffix Unique identifier to append to the cache key.
	 *
	 * @return string The generated cache key.
	 */
	private static function cache_key( string $suffix ): string {
		return 'postgate_orders_' . md5( $suffix );
	}

	/**
	 * Get the current cache version.
	 *
	 * Retrieves the cache version number used to invalidate all order caches
	 * when data changes. Ensures a minimum version of 1.
	 *
	 * @since 1.0.0
	 *
	 * @return int The current cache version number.
	 */
	private static function get_cache_version(): int {
		return max( 1, (int) get_option( 'postgate_orders_cache_version', 1 ) );
	}

	/**
	 * Increment the cache version to invalidate all order caches.
	 *
	 * Called after data-modifying operations (refunds, cancellations) to ensure
	 * cached order data is refreshed on subsequent requests.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private static function bump_cache_version(): void {
		update_option( 'postgate_orders_cache_version', self::get_cache_version() + 1, false );
	}

	/**
	 * Get orders with filtering, pagination, and search.
	 *
	 * Retrieves a paginated list of orders from the database with support for:
	 * - Full-text search across order IDs and Stripe identifiers
	 * - Filtering by status, type, and currency
	 * - Sorting by any order field
	 * - User enrichment with customer names and emails
	 * - Object caching for improved performance
	 *
	 * The response includes both the order data and pagination metadata.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST API request object containing query parameters.
	 *
	 * @return WP_REST_Response The response object containing orders data and pagination info.
	 */
	public static function get_orders( WP_REST_Request $request ) {
		$page = max( 1, (int) ( $request->get_param( 'page' ) ?: 1 ) );
		$per_page = max( 1, min( 100, (int) ( $request->get_param( 'per_page' ) ?: 10 ) ) );
		$offset = ( $page - 1 ) * $per_page;

		$search = sanitize_text_field( (string) ( $request->get_param( 'search' ) ?: '' ) );
		$status = sanitize_text_field( (string) ( $request->get_param( 'status' ) ?: '' ) );
		$type = sanitize_text_field( (string) ( $request->get_param( 'type' ) ?: '' ) );
		$currency = sanitize_text_field( (string) ( $request->get_param( 'currency' ) ?: '' ) );

		$orderby = sanitize_key( (string) ( $request->get_param( 'orderby' ) ?: 'id' ) );
		$order = strtoupper( sanitize_text_field( (string) ( $request->get_param( 'order' ) ?: 'DESC' ) ) );
		$order = ( 'ASC' === $order ) ? 'ASC' : 'DESC';

		$allowed_orderby = [ 'id', 'user_id', 'amount_cents', 'fee_cents', 'status', 'type', 'currency', 'created_at', 'updated_at' ];
		if ( ! in_array( $orderby, $allowed_orderby, true ) ) {
			$orderby = 'id';
		}

		$cache_key = self::cache_key(
			'list_v1_' . self::get_cache_version() . '_' . md5(
				wp_json_encode(
					[
						'page' => $page,
						'per_page' => $per_page,
						'offset' => $offset,
						'search' => $search,
						'status' => $status,
						'type' => $type,
						'currency' => $currency,
						'orderby' => $orderby,
						'order' => $order,
					]
				)
			)
		);

		$cached = wp_cache_get( $cache_key, self::CACHE_GROUP );
		if ( false !== $cached && is_array( $cached ) ) {
			return rest_ensure_response( $cached );
		}

		global $wpdb;
		$table = $wpdb->prefix . 'postgate_orders';

		$has_search = ( '' !== $search );
		$has_status = ( '' !== $status );
		$has_type = ( '' !== $type );
		$has_currency = ( '' !== $currency );

		$like = $has_search ? '%' . $wpdb->esc_like( $search ) . '%' : '';

		// Total count.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$total_items = (int) $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(*)
				 FROM %i
				 WHERE ( %d = 0 OR ( id LIKE %s OR stripe_payment_intent_id LIKE %s OR stripe_checkout_session_id LIKE %s ) )
				   AND ( %d = 0 OR status = %s )
				   AND ( %d = 0 OR type = %s )
				   AND ( %d = 0 OR currency = %s )',
				$table,
				$has_search ? 1 : 0,
				$like,
				$like,
				$like,
				$has_status ? 1 : 0,
				$status,
				$has_type ? 1 : 0,
				$type,
				$has_currency ? 1 : 0,
				$currency
			)
		);

		$total_pages = (int) ceil( $total_items / $per_page );

		// Paginated results.
		if ( 'ASC' === $order ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$orders = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT *
					 FROM %i
					 WHERE ( %d = 0 OR ( id LIKE %s OR stripe_payment_intent_id LIKE %s OR stripe_checkout_session_id LIKE %s ) )
					   AND ( %d = 0 OR status = %s )
					   AND ( %d = 0 OR type = %s )
					   AND ( %d = 0 OR currency = %s )
					 ORDER BY %i ASC
					 LIMIT %d OFFSET %d',
					$table,
					$has_search ? 1 : 0,
					$like,
					$like,
					$like,
					$has_status ? 1 : 0,
					$status,
					$has_type ? 1 : 0,
					$type,
					$has_currency ? 1 : 0,
					$currency,
					$orderby,
					$per_page,
					$offset
				),
				ARRAY_A
			);
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$orders = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT *
					 FROM %i
					 WHERE ( %d = 0 OR ( id LIKE %s OR stripe_payment_intent_id LIKE %s OR stripe_checkout_session_id LIKE %s ) )
					   AND ( %d = 0 OR status = %s )
					   AND ( %d = 0 OR type = %s )
					   AND ( %d = 0 OR currency = %s )
					 ORDER BY %i DESC
					 LIMIT %d OFFSET %d',
					$table,
					$has_search ? 1 : 0,
					$like,
					$like,
					$like,
					$has_status ? 1 : 0,
					$status,
					$has_type ? 1 : 0,
					$type,
					$has_currency ? 1 : 0,
					$currency,
					$orderby,
					$per_page,
					$offset
				),
				ARRAY_A
			);
		}

		$orders = self::enrich_orders( $orders );

		$payload = [
			'data' => $orders,
			'totalItems' => $total_items,
			'totalPages' => $total_pages,
			'page' => $page,
			'perPage' => $per_page,
		];

		wp_cache_set( $cache_key, $payload, self::CACHE_GROUP, self::CACHE_TTL );

		return rest_ensure_response( $payload );
	}

	/**
	 * Enrich orders with user information and formatted dates.
	 *
	 * Processes raw order data from the database to add:
	 * - User display name, first name, last name, and email
	 * - Formatted creation and update timestamps
	 * - Type-cast numeric fields for consistent JSON responses
	 *
	 * @since 1.0.0
	 *
	 * @param array $orders Array of order records from the database.
	 *
	 * @return array The enriched orders array with additional user and formatting data.
	 */
	private static function enrich_orders( array $orders ): array {
		foreach ( $orders as &$order ) {
			$order['id'] = isset( $order['id'] ) ? (int) $order['id'] : 0;
			$order['user_id'] = isset( $order['user_id'] ) ? (int) $order['user_id'] : 0;
			$order['amount_cents'] = isset( $order['amount_cents'] ) ? (int) $order['amount_cents'] : 0;
			$order['fee_cents'] = isset( $order['fee_cents'] ) ? (int) $order['fee_cents'] : 0;

			// Convert amount_cents to decimal amount for easier use in frontend
			$order['amount'] = $order['amount_cents'] > 0 ? round( $order['amount_cents'] / 100, 2 ) : 0;

			$order['origin_post_id'] = ! empty( $order['origin_post_id'] ) ? (int) $order['origin_post_id'] : null;

			// Stripe Price IDs are strings (e.g., "price_123..."), not ints.
			$order['purchased_price_id'] = ! empty( $order['purchased_price_id'] ) ? (string) $order['purchased_price_id'] : null;

			// Admin notes are stored as strings
			$order['admin_notes'] = ! empty( $order['admin_notes'] ) ? (string) $order['admin_notes'] : '';

			$user = ( $order['user_id'] > 0 ) ? get_user_by( 'id', $order['user_id'] ) : false;

			if ( $user ) {
				$order['user_name'] = $user->display_name;
				$order['user_first_name'] = (string) get_user_meta( $user->ID, 'first_name', true );
				$order['user_last_name'] = (string) get_user_meta( $user->ID, 'last_name', true );
				$order['user_email'] = $user->user_email;
			} else {
				$order['user_name'] = __( 'Unknown', 'postgate' );
				$order['user_first_name'] = '';
				$order['user_last_name'] = '';
				$order['user_email'] = '';
			}

			if ( ! empty( $order['created_at'] ) ) {
				$order['created_at_formatted'] = wp_date(
					get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
					strtotime( (string) $order['created_at'] )
				);
			}

			if ( ! empty( $order['updated_at'] ) ) {
				$order['updated_at_formatted'] = wp_date(
					get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
					strtotime( (string) $order['updated_at'] )
				);
			}

			// Check if access has been granted for this order
			$order['has_access'] = PostGate_Access_Manager::order_has_granted_access(
				(int) $order['user_id'],
				(int) $order['id']
			) ? 1 : 0;
		}

		return $orders;
	}

	/**
	 * Refund a one-time purchase order via Stripe API.
	 *
	 * Processes a refund for a completed one-time purchase by:
	 * 1. Validating the order exists and is refundable
	 * 2. Calling Stripe Refunds API with payment intent or charge ID
	 * 3. Updating the order status to 'refunded'
	 * 4. Invalidating relevant caches
	 *
	 * Only one-time purchases can be refunded. Subscription orders must be canceled instead.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST API request containing order ID and optional reason.
	 *
	 * @return WP_REST_Response|WP_Error Response with refund confirmation or error details.
	 */
	public static function refund_order( WP_REST_Request $request ) {
		global $wpdb;

		$id = (int) $request['id'];
		$reason = sanitize_text_field( (string) ( $request->get_param( 'reason' ) ?: '' ) );

		$table = $wpdb->prefix . 'postgate_orders';

		$cache_key = self::cache_key( 'order_v1_' . self::get_cache_version() . '_' . $id );
		$order = wp_cache_get( $cache_key, self::CACHE_GROUP );

		if ( false === $order ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$order = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM %i WHERE id = %d", $table, $id ),
				ARRAY_A
			);
			wp_cache_set( $cache_key, $order, self::CACHE_GROUP, self::CACHE_TTL );
		}

		if ( ! $order ) {
			return new WP_Error( 'postgate_order_not_found', __( 'Order not found', 'postgate' ), [ 'status' => 404 ] );
		}

		if ( ( $order['type'] ?? '' ) !== 'one_time' ) {
			return new WP_Error( 'postgate_invalid_type', __( 'Only one-time orders can be refunded', 'postgate' ), [ 'status' => 400 ] );
		}

		$mode = ( get_option( 'postgate_stripe_connect_mode', 'sandbox' ) === 'live' ) ? 'live' : 'sandbox';
		$access_token = PostGate_Merchant_Auth::token( $mode );

		if ( '' === $access_token ) {
			return new WP_Error( 'postgate_no_token', __( 'Missing Stripe access token', 'postgate' ), [ 'status' => 400 ] );
		}

		$intent = (string) ( $order['stripe_payment_intent_id'] ?? '' );
		$charge = (string) ( $order['stripe_charge_id'] ?? '' );

		$payload = [];
		if ( '' !== $intent ) {
			$payload['payment_intent'] = $intent;
		}
		if ( '' !== $charge ) {
			$payload['charge'] = $charge;
		}
		if ( '' !== $reason ) {
			$payload['reason'] = 'requested_by_customer';
		}

		$response = wp_remote_post(
			'https://api.stripe.com/v1/refunds',
			[
				'headers' => [ 'Authorization' => 'Bearer ' . $access_token ],
				'body' => $payload,
				'timeout' => 20,
			]
		);

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'postgate_http_error', $response->get_error_message(), [ 'status' => 500 ] );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error( 'postgate_stripe_error', __( 'Stripe refund failed', 'postgate' ), [ 'status' => 500, 'body' => $body ] );
		}

		// Update order status.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->update(
			$table,
			[
				'status' => 'refunded',
				'updated_at' => current_time( 'mysql' ),
			],
			[ 'id' => $id ],
			[ '%s', '%s' ],
			[ '%d' ]
		);

		wp_cache_delete( $cache_key, self::CACHE_GROUP );
		self::bump_cache_version();

		return rest_ensure_response( [ 'success' => true, 'refund' => $body ] );
	}

	/**
	 * Cancel a subscription order via Stripe API.
	 *
	 * Processes subscription cancellation by:
	 * 1. Validating the order exists and is a subscription
	 * 2. Calling Stripe Subscriptions API to cancel or schedule cancellation
	 * 3. Updating order status if immediate cancellation
	 * 4. Invalidating relevant caches
	 *
	 * Supports two cancellation modes:
	 * - Immediate: Subscription ends immediately and status updates to 'canceled'
	 * - At period end: Subscription remains active until current period ends
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST API request containing order ID and cancellation options.
	 *
	 * @return WP_REST_Response|WP_Error Response with cancellation confirmation or error details.
	 */
	public static function cancel_subscription( WP_REST_Request $request ) {
		global $wpdb;

		$id = (int) $request['id'];
		$reason = sanitize_text_field( (string) ( $request->get_param( 'reason' ) ?: '' ) );

		// Proper boolean parsing.
		$cancel_at_period_end = rest_sanitize_boolean( $request->get_param( 'cancel_at_period_end' ) );
		if ( null === $request->get_param( 'cancel_at_period_end' ) ) {
			$cancel_at_period_end = true; // default.
		}

		$table = $wpdb->prefix . 'postgate_orders';

		$cache_key = self::cache_key( 'order_v1_' . self::get_cache_version() . '_' . $id );
		$order = wp_cache_get( $cache_key, self::CACHE_GROUP );

		if ( false === $order ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$order = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM %i WHERE id = %d", $table, $id ),
				ARRAY_A
			);
			wp_cache_set( $cache_key, $order, self::CACHE_GROUP, self::CACHE_TTL );
		}

		if ( ! $order ) {
			return new WP_Error( 'postgate_order_not_found', __( 'Order not found', 'postgate' ), [ 'status' => 404 ] );
		}

		if ( ( $order['type'] ?? '' ) !== 'subscription' ) {
			return new WP_Error( 'postgate_invalid_type', __( 'Only subscription orders can be canceled', 'postgate' ), [ 'status' => 400 ] );
		}

		$mode = ( get_option( 'postgate_stripe_connect_mode', 'sandbox' ) === 'live' ) ? 'live' : 'sandbox';
		$access_token = (string) PostGate_Merchant_Auth::token( $mode );

		if ( '' === $access_token ) {
			return new WP_Error( 'postgate_no_token', __( 'Missing Stripe access token', 'postgate' ), [ 'status' => 400 ] );
		}

		$subscription_id = (string) ( $order['stripe_subscription_id'] ?? '' );
		if ( '' === $subscription_id ) {
			return new WP_Error( 'postgate_no_subscription', __( 'Missing Stripe subscription id', 'postgate' ), [ 'status' => 400 ] );
		}

		$endpoint = 'https://api.stripe.com/v1/subscriptions/' . rawurlencode( $subscription_id );

		if ( $cancel_at_period_end ) {
			$response = wp_remote_post(
				$endpoint,
				[
					'headers' => [ 'Authorization' => 'Bearer ' . $access_token ],
					'body' => [ 'cancel_at_period_end' => 'true' ],
					'timeout' => 20,
				]
			);
		} else {
			$response = wp_remote_request(
				$endpoint,
				[
					'method' => 'DELETE',
					'headers' => [ 'Authorization' => 'Bearer ' . $access_token ],
					'timeout' => 20,
				]
			);
		}

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'postgate_http_error', $response->get_error_message(), [ 'status' => 500 ] );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( $code < 200 || $code >= 300 ) {
			return new WP_Error( 'postgate_stripe_error', __( 'Stripe cancellation failed', 'postgate' ), [ 'status' => 500, 'body' => $body ] );
		}

		$update_data = [ 'updated_at' => current_time( 'mysql' ) ];
		$update_fmt = [ '%s' ];

		if ( ! $cancel_at_period_end ) {
			$update_data['status'] = 'canceled';
			$update_fmt[] = '%s';
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$wpdb->update(
			$table,
			$update_data,
			[ 'id' => $id ],
			$update_fmt,
			[ '%d' ]
		);

		wp_cache_delete( $cache_key, self::CACHE_GROUP );
		self::bump_cache_version();

		return rest_ensure_response( [ 'success' => true, 'canceled' => $body ] );
	}

	/**
	 * Create a manual order.
	 *
	 * Allows administrators to manually create orders for customers, typically used
	 * when webhooks fail or for offline payments. Optionally verifies payment intent
	 * against Stripe to prevent fraud and ensure payment was actually successful.
	 *
	 * Flow:
	 * 1. Validate user exists and is active
	 * 2. Validate product and price exist
	 * 3. If payment intent provided, verify it against Stripe
	 * 4. Check for duplicate orders (same payment intent)
	 * 5. Create order record in database
	 * 6. Optionally grant access to content
	 * 7. Log admin action for audit trail
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object.
	 *
	 * @return WP_REST_Response Response with order creation status.
	 */
	public static function create_manual_order( WP_REST_Request $request ): WP_REST_Response {
		$user_id = $request->get_param( 'user_id' );
		$product_id = $request->get_param( 'product_id' );
		$price_id = $request->get_param( 'price_id' );
		$amount = $request->get_param( 'amount' );
		$stripe_payment_intent = $request->get_param( 'stripe_payment_intent' );
		$notes = $request->get_param( 'notes' );
		$grant_access = $request->get_param( 'grant_access' );

		// Validate user exists
		$user = get_user_by( 'id', $user_id );
		if ( ! $user ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => __( 'Invalid user ID', 'postgate' ),
				],
				400
			);
		}

		// Get Stripe products to validate product/price
		$mode = PostGate_Merchant_Auth::mode();
		$stripe_products = get_option( 'postgate_stripe_products', [] );
		$products_data = $mode === 'live' ? ( $stripe_products['live'] ?? [] ) : ( $stripe_products['sandbox'] ?? [] );

		// Extract products array (handle structure with 'products' key and metadata)
		$products_for_mode = [];
		if ( is_array( $products_data ) ) {
			if ( isset( $products_data['products'] ) && is_array( $products_data['products'] ) ) {
				// Structure: { updated_at: ..., products: [...] }
				$products_for_mode = $products_data['products'];
			} elseif ( ! isset( $products_data['updated_at'] ) ) {
				// Structure: directly array of products
				$products_for_mode = $products_data;
			}
		}

		// Find the product and price
		$found_product = null;
		$found_price = null;

		foreach ( $products_for_mode as $product ) {
			if ( $product['id'] === $product_id ) {
				$found_product = $product;
				foreach ( $product['prices'] as $price ) {
					if ( $price['id'] === $price_id ) {
						$found_price = $price;
						break 2;
					}
				}
			}
		}

		if ( ! $found_product || ! $found_price ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => __( 'Invalid product or price ID', 'postgate' ),
				],
				400
			);
		}

		// Check for duplicate order (only if stripe_payment_intent provided)
		if ( $stripe_payment_intent ) {
			$existing_order = PostGate_Orders_Repo::get_by_intent_id( $stripe_payment_intent );
			if ( $existing_order ) {
				return new WP_REST_Response(
					[
						'success' => false,
						'message' => __( 'An order already exists for this payment intent', 'postgate' ),
						'order_id' => $existing_order['id'],
					],
					400
				);
			}
		}

		// Verify payment intent with Stripe (OPTIONAL - only if provided and not empty string)
		if ( ! empty( $stripe_payment_intent ) ) {
			$verification = self::verify_stripe_payment_internal( $stripe_payment_intent );
			if ( is_wp_error( $verification ) ) {
				return new WP_REST_Response(
					[
						'success' => false,
						'message' => $verification->get_error_message(),
					],
					400
				);
			}
		}

		// Create order
		$order_id = self::create_manual_order_record(
			$user_id,
			$product_id,
			$price_id,
			$found_product['name'],
			$amount,
			$found_price['currency'],
			$found_price['type'],
			$found_price['recurring_interval'] ?? null,
			$stripe_payment_intent,
			$notes
		);

		if ( ! $order_id ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => __( 'Failed to create order', 'postgate' ),
				],
				500
			);
		}

		// Grant access if requested
		if ( $grant_access ) {
			$order = PostGate_Orders_Repo::get_by_id( $order_id );
			if ( $order ) {
				PostGate_Access_Manager::grant_access_from_order( $order );
			}
		}

		// Log admin action
		self::log_manual_order_creation( $order_id, get_current_user_id(), $notes );

		// Send email notification to customer
		$order_data = array(
			'order_id' => $order_id,
			'user_id' => $user_id,
			'admin_id' => get_current_user_id(),
			'product_id' => $product_id,
			'product_name' => $found_product['name'],
			'amount' => (int) round( $amount * 100 ),
			'currency' => $found_price['currency'],
			'access_granted' => $grant_access,
			'notes' => $notes,
		);
		PostGate_Email_Notification::send_manual_order_notification( $order_data );

		// Invalidate cache
		self::bump_cache_version();

		return new WP_REST_Response(
			[
				'success' => true,
				'order_id' => $order_id,
				'message' => __( 'Order created successfully', 'postgate' ),
			],
			201
		);
	}

	/**
	 * Update an existing order.
	 *
	 * Allows administrators to update order details after creation, including:
	 * - Product and price information
	 * - Order amount
	 * - Access grant status
	 * - Notes and metadata
	 *
	 * @since 1.0.2
	 *
	 * @param WP_REST_Request $request The REST request.
	 * @return WP_REST_Response REST response.
	 */
	public static function update_order( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$order_id = (int) $request->get_param( 'id' );
		$product_id = $request->get_param( 'product_id' );
		$price_id = $request->get_param( 'price_id' );
		$amount = $request->get_param( 'amount' );
		$grant_access = $request->get_param( 'grant_access' );
		$notes = $request->get_param( 'notes' );

		// Check if order exists
		$table = $wpdb->prefix . 'postgate_orders';
		$cache_key = self::cache_key( 'order_v1_' . self::get_cache_version() . '_' . $order_id );
		$order = wp_cache_get( $cache_key, self::CACHE_GROUP );

		if ( false === $order ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$order = $wpdb->get_row(
				$wpdb->prepare( "SELECT * FROM %i WHERE id = %d", $table, $order_id )
			);
			wp_cache_set( $cache_key, $order, self::CACHE_GROUP, self::CACHE_TTL );
		}

		if ( ! $order ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => __( 'Order not found', 'postgate' ),
				],
				404
			);
		}

		// If product/price are being updated, validate them
		$found_product = null;
		$found_price = null;

		if ( ! empty( $product_id ) && ! empty( $price_id ) ) {
			// Get Stripe products to validate product/price
			$mode = PostGate_Merchant_Auth::mode();
			$stripe_products = get_option( 'postgate_stripe_products', [] );
			$products_data = $mode === 'live' ? ( $stripe_products['live'] ?? [] ) : ( $stripe_products['sandbox'] ?? [] );

			// Extract products array (handle structure with 'products' key and metadata)
			$products_for_mode = [];
			if ( is_array( $products_data ) ) {
				if ( isset( $products_data['products'] ) && is_array( $products_data['products'] ) ) {
					$products_for_mode = $products_data['products'];
				} elseif ( ! isset( $products_data['updated_at'] ) ) {
					$products_for_mode = $products_data;
				}
			}

			// Find the product and price
			foreach ( $products_for_mode as $product ) {
				if ( $product['id'] === $product_id ) {
					$found_product = $product;
					foreach ( $product['prices'] as $price ) {
						if ( $price['id'] === $price_id ) {
							$found_price = $price;
							break 2;
						}
					}
				}
			}

			if ( ! $found_product || ! $found_price ) {
				return new WP_REST_Response(
					[
						'success' => false,
						'message' => __( 'Invalid product or price ID', 'postgate' ),
					],
					400
				);
			}
		}

		$updated = false;
		$update_data = [];

		// Update product/price if provided
		if ( ! empty( $product_id ) && $product_id !== $order->purchased_product_id ) {
			$update_data['purchased_product_id'] = $product_id;
			$update_data['product_name'] = $found_product['name'] ?? '';
			$updated = true;
		}

		if ( ! empty( $price_id ) && $price_id !== $order->purchased_price_id ) {
			$update_data['purchased_price_id'] = $price_id;
			$updated = true;
		}

		// Update amount if provided (for Type 2 Offline Manual Orders)
		if ( ! empty( $amount ) ) {
			$amount_cents = (int) round( (float) $amount * 100 );
			if ( $amount_cents !== (int) $order->amount_cents ) {
				$update_data['amount_cents'] = $amount_cents;
				$updated = true;
			}
		}

		// Always update notes if provided (even if empty string) - allow clearing notes
		if ( null !== $notes ) {
			$notes_str = (string) $notes;
			$existing_notes = (string) ( $order->admin_notes ?? '' );

			if ( $notes_str !== $existing_notes ) {
				$update_data['admin_notes'] = $notes_str;
				$updated = true;
			}
		}

		// Handle access grants and revocations
		// Calculate current has_access state from user's grants
		$user_id = (int) $order->user_id;
		$order_id = (int) $order->id;
		$current_has_access = PostGate_Access_Manager::order_has_granted_access( $user_id, $order_id ) ? 1 : 0;

		// If grant_access is explicitly set, handle it
		if ( null !== $grant_access ) {
			if ( $grant_access === true && $current_has_access !== 1 ) {
				// Grant access if not already granted
				PostGate_Access_Manager::grant_access_from_order( (array) $order );
				$updated = true;
			} elseif ( $grant_access === false && $current_has_access === 1 ) {
				// Revoke access if currently granted
				PostGate_Access_Manager::revoke_access_from_order( (array) $order );
				$updated = true;
			}
		}

		if ( ! $updated ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => __( 'No changes were made', 'postgate' ),
				],
				200
			);
		}

		// Apply updates
		if ( ! empty( $update_data ) ) {
			// Build format array based on field types
			$formats = [];
			foreach ( $update_data as $key => $value ) {
				if ( in_array( $key, [ 'amount_cents', 'fee_cents' ], true ) ) {
					$formats[] = '%d'; // Integer fields
				} else {
					$formats[] = '%s'; // String fields
				}
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
			$wpdb->update(
				$table,
				$update_data,
				[ 'id' => $order_id ],
				$formats,
				[ '%d' ]
			);
		}

		// Log the update
		$current_user = wp_get_current_user();
		PostGate_Logger::info(
			'Order updated',
			[
				'order_id' => $order_id,
				'admin_name' => $current_user->user_login ?? 'Unknown',
				'admin_id' => get_current_user_id(),
				'product_id' => $product_id ?? 'unchanged',
				'price_id' => $price_id ?? 'unchanged',
				'access_granted' => $grant_access ? 'yes' : 'no',
				'notes_updated' => ! empty( $notes ) ? 'yes' : 'no',
			]
		);

		// Invalidate cache
		wp_cache_delete( $cache_key, self::CACHE_GROUP );
		self::bump_cache_version();

		return new WP_REST_Response(
			[
				'success' => true,
				'order_id' => $order_id,
				'message' => __( 'Order updated successfully', 'postgate' ),
			],
			200
		);
	}

	/**
	 * Create a manual order record in the database.
	 *
	 * Inserts a new order with 'paid' status and 'manual' creation source.
	 * The order is marked as created by an administrator for audit purposes.
	 *
	 * @since 1.0.0
	 *
	 * @param int         $user_id             User ID.
	 * @param string      $product_id          Stripe Product ID.
	 * @param string      $price_id            Stripe Price ID.
	 * @param string      $product_name        Product name.
	 * @param float       $amount              Amount in major currency units.
	 * @param string      $currency            Currency code.
	 * @param string      $price_type          Price type (one_time or recurring).
	 * @param string|null $recurring_interval  Recurring interval if subscription.
	 * @param string|null $payment_intent      Optional Stripe Payment Intent ID.
	 * @param string|null $notes               Optional admin notes.
	 *
	 * @return int|false Order ID on success, false on failure.
	 */
	private static function create_manual_order_record(
		int $user_id,
		string $product_id,
		string $price_id,
		string $product_name,
		float $amount,
		string $currency,
		string $price_type,
		?string $recurring_interval,
		?string $payment_intent,
		?string $notes
	) {
		global $wpdb;
		$table = $wpdb->prefix . 'postgate_orders';

		$order_type = $price_type === 'recurring' ? 'subscription' : 'one_time';
		$amount_cents = (int) round( $amount * 100 );

		// Calculate unlocked content for this price
		$unlocked_content = PostGate_Product_Content_Resolver::resolve_content_for_product( $product_id, 0 );

		$insert_data = [
			'user_id' => $user_id,
			'stripe_account_id' => PostGate_Merchant_Auth::account(),
			'type' => $order_type,
			'status' => 'paid',
			'created_via' => 'manual',
			'currency' => strtolower( $currency ),
			'amount_cents' => $amount_cents,
			'fee_cents' => 0,
			'fee_percent' => null,
			'recurring_interval' => $recurring_interval,
			'stripe_payment_intent_id' => $payment_intent,
			'purchased_product_id' => $product_id,
			'purchased_price_id' => $price_id,
			'product_name' => $product_name,
			'unlocked_content' => wp_json_encode( $unlocked_content ),
			'admin_notes' => $notes,
			'created_by_admin_id' => get_current_user_id(),
		];

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$inserted = $wpdb->insert( $table, $insert_data );

		return $inserted ? $wpdb->insert_id : false;
	}

	/**
	 * Log manual order creation for audit trail.
	 *
	 * Creates a log entry documenting who created the order and why.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $order_id  Order ID.
	 * @param int    $admin_id  Admin user ID who created the order.
	 * @param string $notes     Admin notes explaining the reason.
	 *
	 * @return void
	 */
	private static function log_manual_order_creation( int $order_id, int $admin_id, ?string $notes ): void {
		$admin = get_user_by( 'id', $admin_id );
		$admin_name = $admin ? $admin->display_name : 'Unknown';

		PostGate_Logger::info(
			'Manual order created',
			[
				'order_id' => $order_id,
				'admin_name' => $admin_name,
				'admin_id' => $admin_id,
				'notes' => $notes ?? 'None',
			]
		);
	}

	/**
	 * Verify a Stripe Payment Intent.
	 *
	 * Retrieves payment intent from Stripe API and returns details including
	 * payment status, amount, currency, and customer information.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The REST request object.
	 *
	 * @return WP_REST_Response Response with payment intent details.
	 */
	/**
	 * Verify Stripe Payment Intent via POST request.
	 *
	 * Called from frontend when user enters a Payment Intent ID to verify it exists in Stripe.
	 * Provides real-time feedback about the payment.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_REST_Response
	 */
	public static function verify_intent_post( WP_REST_Request $request ): WP_REST_Response {
		$intent_id = $request->get_param( 'stripe_payment_intent' );

		// Verify with Stripe
		$result = self::verify_stripe_payment_internal( $intent_id );

		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => $result->get_error_message(),
				],
				400
			);
		}

		return new WP_REST_Response( $result );
	}

	public static function verify_payment_intent( WP_REST_Request $request ): WP_REST_Response {
		$intent_id = $request->get_param( 'intent_id' );

		// Validate intent ID format
		if ( ! str_starts_with( $intent_id, 'pi_' ) ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => __( 'Invalid payment intent ID format', 'postgate' ),
				],
				400
			);
		}

		// Verify with Stripe
		$result = self::verify_stripe_payment_internal( $intent_id );

		if ( is_wp_error( $result ) ) {
			return new WP_REST_Response(
				[
					'success' => false,
					'message' => $result->get_error_message(),
				],
				400
			);
		}

		return new WP_REST_Response( $result, 200 );
	}

	/**
	 * Internal method to verify payment intent with Stripe.
	 *
	 * Retrieves payment intent from Stripe API and validates it.
	 *
	 * @since 1.0.0
	 *
	 * @param string $intent_id Stripe Payment Intent ID.
	 *
	 * @return array|WP_Error Payment intent data or error.
	 */
	private static function verify_stripe_payment_internal( string $intent_id ) {
		try {
			$stripe_token = PostGate_Merchant_Auth::token();
			if ( empty( $stripe_token ) ) {
				return new WP_Error(
					'no_stripe_token',
					__( 'Stripe is not connected', 'postgate' )
				);
			}

			\Stripe\Stripe::setApiKey( $stripe_token );
			$intent = \Stripe\PaymentIntent::retrieve( $intent_id );

			// Format amount for display
			$amount_display = number_format( $intent->amount / 100, 2 );
			$currency_upper = strtoupper( $intent->currency );

			// Get customer email - first try receipt_email, then fetch from customer
			$customer_email = $intent->receipt_email;
			if ( empty( $customer_email ) && ! empty( $intent->customer ) ) {
				try {
					$customer = \Stripe\Customer::retrieve( $intent->customer );
					$customer_email = $customer->email ?? null;
				} catch (\Exception $e) {
					$customer_email = null;
				}
			}

			// Extract product name from metadata if available
			$product_name = null;
			if ( ! empty( $intent->metadata ) && isset( $intent->metadata['product_name'] ) ) {
				$product_name = sanitize_text_field( $intent->metadata['product_name'] );
			}

			return [
				'success' => true,
				'intent_id' => $intent->id,
				'status' => $intent->status,
				'amount' => $intent->amount,
				'amount_display' => "{$amount_display} {$currency_upper}",
				'currency' => $intent->currency,
				'customer' => $intent->customer,
				'customer_email' => $customer_email,
				'product_name' => $product_name,
				'description' => $intent->description ?? null,
			];

		} catch (\Stripe\Exception\InvalidRequestException $e) {
			return new WP_Error(
				'invalid_payment_intent',
				sprintf(
					/* translators: %s: error message from Stripe */
					__( 'Payment intent not found: %s', 'postgate' ),
					$e->getMessage()
				)
			);
		} catch (\Stripe\Exception\ApiErrorException $e) {
			return new WP_Error(
				'stripe_api_error',
				sprintf(
					/* translators: %s: error message from Stripe */
					__( 'Stripe API error: %s', 'postgate' ),
					$e->getMessage()
				)
			);
		}
	}
}