<?php
/**
 * Warranty Controller for Warranty Cart Admin REST API
 *
 * Handles warranty product and record management REST API operations.
 *
 * @package WarrantyCart
 */

namespace WarrantyCart\REST\Controllers;

/*
 * The warranty lifecycle, aliased because this controller is also called
 * Warranties. That class turns completed orders into records and renews
 * them; this one is the HTTP surface over the same data.
 */
use WarrantyCart\Core\Warranties as WarrantyLifecycle;
use WarrantyCart\REST\Controller;
use WarrantyCart\REST\Csv;
use WC_Product;
use WC_Product_Simple;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

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

/**
 * Warranties class
 */
class Warranties extends Controller {

	/**
	 * Rows an export will fetch at most.
	 *
	 * A ceiling rather than "everything", so a shop with a hundred thousand
	 * records cannot build the whole file in memory and exhaust the request.
	 * The limit is reported to the admin when it bites — see below — rather
	 * than the file quietly stopping short.
	 */
	private const EXPORT_LIMIT = 10000;

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->rest_base = 'admin';
	}

	/**
	 * Register routes
	 */
	public function register_routes(): void {
		// Warranty Products endpoints (WooCommerce products with warranty meta)
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranty-products',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_warranty_products' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => $this->get_warranties_args(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_warranty_product' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => $this->get_create_warranty_args(),
				),
			)
		);

		// Individual Warranty Product endpoints
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranty-products/(?P<id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_warranty_product' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'id' => array(
							'required'          => true,
							'type'              => 'integer',
							'description'       => __( 'Warranty product ID.', 'warranty-cart' ),
							'sanitize_callback' => 'absint',
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_warranty_product' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array_merge(
						array(
							'id' => array(
								'required'          => true,
								'type'              => 'integer',
								'description'       => __( 'Warranty product ID.', 'warranty-cart' ),
								'sanitize_callback' => 'absint',
							),
						),
						$this->get_update_warranty_args()
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_warranty_product' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'id' => array(
							'required'          => true,
							'type'              => 'integer',
							'description'       => __( 'Warranty product ID.', 'warranty-cart' ),
							'sanitize_callback' => 'absint',
						),
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranties/export',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'export_warranties_csv' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranty-records/export',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'export_warranty_records_csv' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
				),
			)
		);

		// Warranty Records endpoints (purchased warranty instances from database)
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranty-records',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_warranty_records' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => $this->get_warranty_records_args(),
				),

				/*
				 * Issuing cover by hand.
				 *
				 * Every other warranty in the plugin comes from a completed
				 * order. That leaves no way to cover a phone order, grant a
				 * policy as an apology, correct a mistake, or bring across the
				 * records a shop kept before this plugin — all of which meant
				 * editing the table directly.
				 */
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'issue_warranty_record' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => $this->get_issue_warranty_record_args(),
				),
			)
		);

		// Individual Warranty Record endpoints
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranty-records/(?P<id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_warranty_record' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'id' => array(
							'required'          => true,
							'type'              => 'integer',
							'description'       => __( 'Warranty record ID.', 'warranty-cart' ),
							'sanitize_callback' => 'absint',
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_warranty_record' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array_merge(
						array(
							'id' => array(
								'required'          => true,
								'type'              => 'integer',
								'description'       => __( 'Warranty record ID.', 'warranty-cart' ),
								'sanitize_callback' => 'absint',
							),
						),
						$this->get_update_warranty_record_args()
					),
				),
			)
		);

		// Warranty Record renewal endpoint
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/warranty-records/(?P<id>\d+)/renew',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'renew_warranty_record' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'id' => array(
							'required'          => true,
							'type'              => 'integer',
							'description'       => __( 'Warranty record ID to renew.', 'warranty-cart' ),
							'sanitize_callback' => 'absint',
						),
					),
				),
			)
		);
	}

	/**
	 * Get warranties arguments
	 */
	private function get_warranties_args(): array {
		return array(
			'page'     => array(
				'required'    => false,
				'type'        => 'integer',
				'default'     => 1,
				'description' => __( 'Page number.', 'warranty-cart' ),
				'minimum'     => 1,
			),
			'per_page' => array(
				'required'    => false,
				'type'        => 'integer',
				'default'     => 10,
				'description' => __( 'Items per page.', 'warranty-cart' ),
				'minimum'     => 1,
				'maximum'     => 100,
			),
			'search'   => array(
				'required'          => false,
				'type'              => 'string',
				'description'       => __( 'Search term.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'type'     => array(
				'required'          => false,
				'type'              => 'string',
				'enum'              => array( '', 'main_warranty', 'additional_service' ),
				'description'       => __( 'Warranty type filter.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
		);
	}

	/**
	 * Get warranty records arguments
	 */
	private function get_warranty_records_args(): array {
		return array(
			'page'        => array(
				'required'    => false,
				'type'        => 'integer',
				'default'     => 1,
				'description' => __( 'Page number.', 'warranty-cart' ),
				'minimum'     => 1,
			),
			'per_page'    => array(
				'required'    => false,
				'type'        => 'integer',
				'default'     => 10,
				'description' => __( 'Items per page.', 'warranty-cart' ),
				'minimum'     => 1,
				'maximum'     => 100,
			),
			'search'      => array(
				'required'          => false,
				'type'              => 'string',
				'description'       => __( 'Search term.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'status'      => array(
				'required'          => false,
				'type'              => 'string',

				/*
				 * Every status the plugin actually writes, plus 'all'.
				 *
				 * The list was active/expired/cancelled. Cron and the record
				 * editor also write 'pending' and 'refunded', so those could never be
				 * filtered for — and the admin screen's own default value, 'all', was
				 * rejected outright with a 400. The page caught that, emptied its
				 * list, and reported "Warranty Records (0)" on first load for every
				 * installation.
				 *
				 * 'all' is accepted and then ignored by the query, so validation and
				 * the filter agree on what it means.
				 */
				'enum'              => array( 'all', 'active', 'expired', 'cancelled', 'pending', 'refunded' ),
				'description'       => __( 'Warranty status filter.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'customer_id' => array(
				'required'          => false,
				'type'              => 'integer',
				'description'       => __( 'Filter by customer ID.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
			'order_id'    => array(
				'required'          => false,
				'type'              => 'integer',
				'description'       => __( 'Filter by order ID.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
		);
	}

	/**
	 * Get update warranty record arguments
	 */
	private function get_update_warranty_record_args(): array {
		return array(
			'status'           => array(
				'required'          => false,
				'type'              => 'string',
				// Same vocabulary as the filter above, minus 'all': this one sets a
				// record's status, and 'all' is not a status.
				'enum'              => array( 'active', 'expired', 'cancelled', 'pending', 'refunded' ),
				'description'       => __( 'Warranty status.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'expiration_date'  => array(
				'required'          => false,
				'type'              => 'string',
				'format'            => 'date',
				'description'       => __( 'New expiration date (YYYY-MM-DD).', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'reminder_sent'    => array(
				'required'          => false,
				'type'              => 'boolean',
				'description'       => __( 'Whether reminder has been sent.', 'warranty-cart' ),
				'sanitize_callback' => 'wp_validate_boolean',
			),
			'expired_notified' => array(
				'required'          => false,
				'type'              => 'boolean',
				'description'       => __( 'Whether expiration notification has been sent.', 'warranty-cart' ),
				'sanitize_callback' => 'wp_validate_boolean',
			),
		);
	}

	/**
	 * What issuing a warranty by hand accepts.
	 *
	 * Only the plan and the customer are required. Everything else falls back to
	 * the plan's own terms, so the ordinary case is two fields.
	 *
	 * @return array<string, mixed>
	 */
	private function get_issue_warranty_record_args(): array {
		return array(
			'product_id'         => array(
				'required'          => true,
				'type'              => 'integer',
				'description'       => __( 'The warranty plan to issue.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
			'customer_id'        => array(
				'required'          => true,
				'type'              => 'integer',
				'description'       => __( 'The customer this cover belongs to.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
			'covered_product_id' => array(
				'required'          => false,
				'type'              => 'integer',
				'description'       => __( 'The product being covered, if a specific one.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
			'order_id'           => array(
				'required'          => false,
				'type'              => 'integer',
				'description'       => __( 'An order to attribute the cover to. 0 for none.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
			'duration'           => array(
				'required'          => false,
				'type'              => 'integer',
				'minimum'           => 1,
				'description'       => __( "Overrides the plan's term.", 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
			'duration_unit'      => array(
				'required'          => false,
				'type'              => 'string',
				'enum'              => array( 'days', 'months', 'years' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'purchase_price'     => array(
				'required'    => false,
				'type'        => 'number',
				'minimum'     => 0,
				'description' => __( 'What was paid, if anything.', 'warranty-cart' ),
			),
			'purchase_date'      => array(
				'required'          => false,
				'type'              => 'string',
				'description'       => __( 'When cover begins. Defaults to now.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
		);
	}

	/**
	 * Issue a warranty by hand.
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response
	 */
	public function issue_warranty_record( WP_REST_Request $request ): WP_REST_Response {
		$warranties = new WarrantyLifecycle();

		$warranty_id = $warranties->issue(
			array(
				'product_id'         => (int) $request->get_param( 'product_id' ),
				'customer_id'        => (int) $request->get_param( 'customer_id' ),
				'covered_product_id' => (int) $request->get_param( 'covered_product_id' ),
				'order_id'           => (int) $request->get_param( 'order_id' ),
				'duration'           => (int) $request->get_param( 'duration' ),
				'duration_unit'      => (string) $request->get_param( 'duration_unit' ),
				'purchase_price'     => $request->get_param( 'purchase_price' ),
				'purchase_date'      => (string) $request->get_param( 'purchase_date' ),
			)
		);

		if ( is_wp_error( $warranty_id ) ) {
			$data = $warranty_id->get_error_data();

			return $this->error_response(
				$warranty_id->get_error_message(),
				is_array( $data ) && isset( $data['status'] ) ? (int) $data['status'] : 400
			);
		}

		return $this->success_response(
			array(
				'message'     => __( 'Warranty issued.', 'warranty-cart' ),
				'warranty_id' => $warranty_id,
			)
		);
	}

	/**
	 * Get create warranty arguments
	 */
	private function get_create_warranty_args(): array {
		return array(
			'name'                => array(
				'required'          => true,
				'type'              => 'string',
				'description'       => __( 'Warranty product name.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'description'         => array(
				'required'          => false,
				'type'              => 'string',
				'description'       => __( 'Warranty product description.', 'warranty-cart' ),
				'sanitize_callback' => 'wp_kses_post',
			),
			'price'               => array(
				'required'    => true,
				'type'        => 'number',
				'description' => __( 'Warranty product price.', 'warranty-cart' ),
				'minimum'     => 0,
			),
			'warranty_type'       => array(
				'required'          => true,
				'type'              => 'string',
				'enum'              => array( 'main_warranty', 'additional_service' ),
				'description'       => __( 'Type of warranty product.', 'warranty-cart' ),
				'sanitize_callback' => 'sanitize_text_field',
			),
			'duration'            => array(
				'required'    => true,
				'type'        => 'integer',
				'description' => __( 'Warranty duration value.', 'warranty-cart' ),
				'minimum'     => 1,
			),
			'duration_unit'       => array(
				'required'    => true,
				'type'        => 'string',
				'enum'        => array( 'days', 'months', 'years' ),
				'description' => __( 'Warranty duration unit.', 'warranty-cart' ),
			),
			'coverage_details'    => array(
				'required'          => false,
				'type'              => 'string',
				'description'       => __( 'Warranty coverage details.', 'warranty-cart' ),
				'sanitize_callback' => 'wp_kses_post',
			),
			'associated_warranty' => array(
				'required'    => false,
				'type'        => 'integer',
				'description' => __( 'Associated main warranty ID (for services).', 'warranty-cart' ),
			),
			'service_priority'    => array(
				'required'    => false,
				'type'        => 'string',
				'enum'        => array( 'required', 'recommended', 'optional' ),
				'description' => __( 'Service priority level.', 'warranty-cart' ),
			),

			/*
			 * The plan's picture.
			 *
			 * The storefront has rendered a plan's image for as long as the
			 * offer has existed, and nothing in this plugin could ever set one —
			 * an administrator had to leave for the WooCommerce product editor,
			 * find a product deliberately hidden from the catalogue, and attach
			 * it there. An attachment ID, because that is what the media library
			 * hands back and what `set_image_id()` wants; 0 clears it.
			 */
			'image_id'            => array(
				'required'          => false,
				'type'              => 'integer',
				'minimum'           => 0,
				'description'       => __( 'Attachment ID of the plan image. 0 removes it.', 'warranty-cart' ),
				'sanitize_callback' => 'absint',
			),
		);
	}

	/**
	 * The same fields, none of them mandatory.
	 *
	 * The update route reused the create arguments verbatim, so a PUT was
	 * rejected unless it carried the name, price, type, duration and unit —
	 * every time, even to change one thing. `update_warranty_product()` is
	 * written the other way round, guarding each field with
	 * `if ( null !== $value )` so that anything absent is left alone, and those
	 * guards could never fire: the request had been refused before the handler
	 * ran. Changing a plan's picture alone was impossible.
	 *
	 * @return array<string, mixed>
	 */
	private function get_update_warranty_args(): array {
		$args = $this->get_create_warranty_args();

		foreach ( $args as $name => $definition ) {
			$args[ $name ]['required'] = false;
		}

		return $args;
	}

	/**
	 * Get warranties
	 *
	 * Retrieves warranty products with pagination and filtering.
	 */
	public function get_warranties( WP_REST_Request $request ): WP_REST_Response {
		$page     = $request->get_param( 'page' );
		$per_page = $request->get_param( 'per_page' );
		$search   = $request->get_param( 'search' );
		$type     = $request->get_param( 'type' );

		// Build additional query arguments
		$query_args = array(
			'limit'   => absint( $per_page ),
			'page'    => $page,
			'orderby' => 'date',
			'order'   => 'DESC',
		);

		// Add search if provided
		if ( $search ) {
			$query_args['s'] = $search;
		}

		// Add type filter if provided
		if ( $type ) {
			$query_args['meta_query']['relation'] = 'AND';

			$query_args['meta_query'][] = array(
				'key'     => '_warranty_cart_product_type',
				'compare' => 'EXISTS',
			);
			$query_args['meta_query'][] = array(
				'key'     => '_warranty_cart_product_type',
				'value'   => $type,
				'compare' => '=',
			);
		}

		$products   = warranty_cart()->repository->get_warranty_products( $query_args );
		$warranties = array();

		foreach ( $products as $product ) {
			if ( ! $product instanceof WC_Product ) {
				continue;
			}

			$warranties[] = array(
				'id'                  => $product->get_id(),
				'name'                => $product->get_name(),
				'description'         => $product->get_description(),
				'price'               => $product->get_price(),
				'warranty_type'       => $product->get_meta( '_warranty_cart_product_type', true ),
				'duration'            => $product->get_meta( '_warranty_cart_duration', true ),
				'duration_unit'       => $product->get_meta( '_warranty_cart_duration_unit', true ),
				'coverage_details'    => $product->get_meta( '_warranty_cart_coverage_details', true ),
				'associated_warranty' => $product->get_meta( '_warranty_cart_associated_warranty', true ),
				'service_priority'    => $product->get_meta( '_warranty_cart_service_priority', true ),
				'image'               => self::product_image( $product ),
				'image_id'            => (int) $product->get_image_id(),
				'status'              => $product->get_status(),
				'stock_status'        => $product->get_stock_status(),
			);
		}

		// Get total count for pagination
		$count_args = $query_args;
		unset( $count_args['limit'], $count_args['page'] );
		$count_args['return'] = 'ids';

		$currency         = get_woocommerce_currency_symbol();
		$all_product_ids  = warranty_cart()->repository->get_warranty_product_ids( $count_args );
		$total_warranties = count( $all_product_ids );
		$total_pages      = $per_page > 0 ? ceil( $total_warranties / $per_page ) : 1;

		return $this->success_response(
			array(
				'warranties'  => $warranties,
				'total'       => $total_warranties,
				'page'        => $page,
				'per_page'    => $per_page,
				'total_pages' => $total_pages,
				'currency'    => $currency,
			)
		);
	}

	/**
	 * Get single warranty product
	 *
	 * Retrieves detailed information for a specific warranty product.
	 */
	public function get_warranty_product( WP_REST_Request $request ) {
		$warranty_id = $request->get_param( 'id' );

		$product = wc_get_product( $warranty_id );
		if ( ! $product ) {
			return $this->error_response( __( 'Warranty product not found.', 'warranty-cart' ), 404 );
		}

		$warranty_type = $product->get_meta( '_warranty_cart_product_type', true );
		if ( ! $warranty_type ) {
			return $this->error_response( __( 'Product is not a warranty product.', 'warranty-cart' ), 404 );
		}

		$warranty = array(
			'id'                  => $product->get_id(),
			'name'                => $product->get_name(),
			'description'         => $product->get_description(),
			'price'               => $product->get_price(),
			'warranty_type'       => $warranty_type,
			'duration'            => (int) $product->get_meta( '_warranty_cart_duration', true ),
			'duration_unit'       => $product->get_meta( '_warranty_cart_duration_unit', true ),
			'coverage_details'    => $product->get_meta( '_warranty_cart_coverage_details', true ),
			'associated_warranty' => (int) $product->get_meta( '_warranty_cart_associated_warranty', true ),
			'image'               => self::product_image( $product ),
			'image_id'            => (int) $product->get_image_id(),
			'service_priority'    => $product->get_meta( '_warranty_cart_service_priority', true ),
			'status'              => $product->get_status(),
			'stock_status'        => $product->get_stock_status(),
			'regular_price'       => $product->get_regular_price(),
			'sale_price'          => $product->get_sale_price(),
			'manage_stock'        => $product->get_manage_stock(),
			'stock_quantity'      => $product->get_stock_quantity(),
		);

		return $this->success_response( $warranty );
	}

	/**
	 * Create warranty product
	 *
	 * Creates a new warranty product with the provided data.
	 */
	public function create_warranty_product( WP_REST_Request $request ) {
		$name                = $request->get_param( 'name' );
		$description         = $request->get_param( 'description' );
		$price               = $request->get_param( 'price' );
		$warranty_type       = $request->get_param( 'warranty_type' );
		$duration            = $request->get_param( 'duration' );
		$duration_unit       = $request->get_param( 'duration_unit' );
		$coverage_details    = $request->get_param( 'coverage_details' );
		$associated_warranty = $request->get_param( 'associated_warranty' );
		$service_priority    = $request->get_param( 'service_priority' );

		// Create new WooCommerce product
		$product = new WC_Product_Simple();
		$product->set_name( $name );
		$product->set_description( $description );
		$product->set_regular_price( $price );
		$product->set_status( 'publish' );
		$product->set_catalog_visibility( 'hidden' ); // Hide from catalog
		$product->set_virtual( true ); // Virtual product
		$product->set_downloadable( false );

		$image_id = (int) $request->get_param( 'image_id' );

		if ( $image_id > 0 && wp_attachment_is_image( $image_id ) ) {
			$product->set_image_id( $image_id );
		}

		// Save the product
		$product_id = $product->save();

		if ( ! $product_id ) {
			return $this->error_response( __( 'Failed to create warranty product.', 'warranty-cart' ), 500 );
		}

		// Set warranty meta data
		$product->update_meta_data( '_warranty_cart_product_type', $warranty_type );
		$product->update_meta_data( '_warranty_cart_duration', $duration );
		$product->update_meta_data( '_warranty_cart_duration_unit', $duration_unit );
		$product->update_meta_data( '_warranty_cart_coverage_details', $coverage_details );

		if ( $associated_warranty ) {
			$product->update_meta_data( '_warranty_cart_associated_warranty', $associated_warranty );
		}

		if ( $service_priority ) {
			$product->update_meta_data( '_warranty_cart_service_priority', $service_priority );
		}

		$product->save();

		$warranty = array(
			'id'                  => $product_id,
			'name'                => $name,
			'description'         => $description,
			'price'               => $price,
			'warranty_type'       => $warranty_type,
			'duration'            => $duration,
			'duration_unit'       => $duration_unit,
			'coverage_details'    => $coverage_details,
			'associated_warranty' => $associated_warranty,
			'service_priority'    => $service_priority,
			'status'              => 'publish',
		);

		return $this->success_response( $warranty, 201 );
	}

	/**
	 * Update warranty product
	 *
	 * Updates an existing warranty product with new data.
	 */
	public function update_warranty_product( WP_REST_Request $request ) {
		$warranty_id = $request->get_param( 'id' );

		$product = wc_get_product( $warranty_id );
		if ( ! $product ) {
			return $this->error_response( __( 'Warranty product not found.', 'warranty-cart' ), 404 );
		}

		$warranty_type = $product->get_meta( '_warranty_cart_product_type', true );
		if ( ! $warranty_type ) {
			return $this->error_response( __( 'Product is not a warranty product.', 'warranty-cart' ), 404 );
		}

		// Update basic product fields
		$name = $request->get_param( 'name' );
		if ( null !== $name ) {
			$product->set_name( $name );
		}

		$description = $request->get_param( 'description' );
		if ( null !== $description ) {
			$product->set_description( $description );
		}

		$price = $request->get_param( 'price' );
		if ( null !== $price ) {
			$product->set_regular_price( $price );
		}

		// Update warranty meta fields
		$warranty_type_param = $request->get_param( 'warranty_type' );
		if ( null !== $warranty_type_param ) {
			$product->update_meta_data( '_warranty_cart_product_type', $warranty_type_param );
		}

		$duration = $request->get_param( 'duration' );
		if ( null !== $duration ) {
			$product->update_meta_data( '_warranty_cart_duration', $duration );
		}

		$duration_unit = $request->get_param( 'duration_unit' );
		if ( null !== $duration_unit ) {
			$product->update_meta_data( '_warranty_cart_duration_unit', $duration_unit );
		}

		$coverage_details = $request->get_param( 'coverage_details' );
		if ( null !== $coverage_details ) {
			$product->update_meta_data( '_warranty_cart_coverage_details', $coverage_details );
		}

		$associated_warranty = $request->get_param( 'associated_warranty' );
		if ( null !== $associated_warranty ) {
			$product->update_meta_data( '_warranty_cart_associated_warranty', $associated_warranty );
		}

		$service_priority = $request->get_param( 'service_priority' );
		if ( null !== $service_priority ) {
			$product->update_meta_data( '_warranty_cart_service_priority', $service_priority );
		}

		/*
		 * Only when the form sent one, so a request that leaves the field out
		 * does not silently strip a picture the shop set elsewhere. An explicit
		 * 0 is how the form says "remove it".
		 */
		$image_id = $request->get_param( 'image_id' );

		if ( null !== $image_id ) {
			$image_id = (int) $image_id;

			// Checked, because an ID that is not an image gives WooCommerce a
			// broken thumbnail everywhere the plan appears.
			$product->set_image_id( $image_id > 0 && wp_attachment_is_image( $image_id ) ? $image_id : '' );
		}

		$product->save();

		// Return updated warranty data
		$warranty = array(
			'id'                  => $product->get_id(),
			'name'                => $product->get_name(),
			'description'         => $product->get_description(),
			'price'               => $product->get_price(),
			'warranty_type'       => $product->get_meta( '_warranty_cart_product_type', true ),
			'duration'            => (int) $product->get_meta( '_warranty_cart_duration', true ),
			'duration_unit'       => $product->get_meta( '_warranty_cart_duration_unit', true ),
			'coverage_details'    => $product->get_meta( '_warranty_cart_coverage_details', true ),
			'associated_warranty' => (int) $product->get_meta( '_warranty_cart_associated_warranty', true ),
			'image'               => self::product_image( $product ),
			'image_id'            => (int) $product->get_image_id(),
			'service_priority'    => $product->get_meta( '_warranty_cart_service_priority', true ),
			'status'              => $product->get_status(),
		);

		return $this->success_response( $warranty );
	}

	/**
	 * Delete warranty product
	 *
	 * Deletes a warranty product if it has no associated orders.
	 */
	public function delete_warranty_product( WP_REST_Request $request ) {
		$warranty_id = $request->get_param( 'id' );

		$product = wc_get_product( $warranty_id );
		if ( ! $product ) {
			return $this->error_response( __( 'Warranty product not found.', 'warranty-cart' ), 404 );
		}

		$warranty_type = $product->get_meta( '_warranty_cart_product_type', true );
		if ( ! $warranty_type ) {
			return $this->error_response( __( 'Product is not a warranty product.', 'warranty-cart' ), 404 );
		}

		// Check if warranty has associated orders
		$order_count = wc_get_orders(
			array(
				'limit'      => 1,
				'return'     => 'ids',
				'meta_query' => array(
					array(
						'key'     => '_product_id',
						'value'   => $warranty_id,
						'compare' => '=',
					),
				),
			)
		);

		if ( ! empty( $order_count ) ) {
			return $this->error_response( __( 'Cannot delete warranty product that has been purchased.', 'warranty-cart' ), 400 );
		}

		// Delete the product
		$deleted = $product->delete( true );
		if ( ! $deleted ) {
			return $this->error_response( __( 'Failed to delete warranty product.', 'warranty-cart' ), 500 );
		}

		return $this->success_response(
			array(
				'message' => __( 'Warranty product deleted successfully.', 'warranty-cart' ),
				'id'      => $warranty_id,
			)
		);
	}

	/**
	 * The avatar for the customer on a warranty record.
	 *
	 * Keyed on the user ID where the warranty belongs to an account, and on
	 * the email otherwise — a warranty bought as a guest still has an address,
	 * which is what Gravatar and every avatar plugin filtering
	 * `get_avatar_url` actually hash.
	 *
	 * Empty when the site has avatars switched off, so the admin falls back to
	 * tinted initials rather than rendering a broken image.
	 *
	 * @param object $record A row from the warranties table.
	 *
	 * @return string A URL, or ''.
	 */
	private function avatar_url( object $record ): string {
		$customer_id = isset( $record->customer_id ) ? (int) $record->customer_id : 0;
		$identifier  = $customer_id > 0 ? $customer_id : ( $record->customer_email ?? '' );

		if ( ! $identifier ) {
			return '';
		}

		$url = \get_avatar_url( $identifier, array( 'size' => 64 ) );

		return is_string( $url ) ? $url : '';
	}

	/**
	 * Get warranty records
	 *
	 * Retrieves warranty records with pagination and filtering.
	 */
	public function get_warranty_records( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$table_name = $wpdb->prefix . 'warranty_cart_warranties';

		$page        = $request->get_param( 'page' );
		$per_page    = $request->get_param( 'per_page' );
		$search      = $request->get_param( 'search' );
		$status      = $request->get_param( 'status' );
		$customer_id = $request->get_param( 'customer_id' );
		$order_id    = $request->get_param( 'order_id' );

		/*
		 * The customer's name comes from usermeta, not from wp_users.
		 *
		 * This selected CONCAT(u.first_name, ' ', u.last_name). Neither is a
		 * column on wp_users — WordPress keeps them in wp_usermeta — so MySQL
		 * rejected the statement with "Unknown column 'u.first_name' in 'field
		 * list'", $wpdb suppressed the error, and get_results() returned an
		 * empty array.
		 *
		 * The Warranty Records screen has therefore always reported "Warranty
		 * Records (0)" and "No warranty records found matching your criteria",
		 * on every installation, no matter how many records the table held.
		 *
		 * Two more LEFT JOINs against usermeta rather than a correlated
		 * subquery, so the search filter below can go on matching the
		 * concatenated name in SQL.
		 *
		 * COALESCE with display_name because first/last name are optional in
		 * WordPress, and a customer who never filled them in should appear
		 * under the name they do have rather than as a blank row.
		 */
		$query        = "SELECT w.*,
			u.user_email as customer_email,
			COALESCE(
				NULLIF( TRIM( CONCAT( COALESCE( fn.meta_value, '' ), ' ', COALESCE( ln.meta_value, '' ) ) ), '' ),
				u.display_name
			) as customer_name,
			p.post_title as product_name,
			pm.meta_value as product_sku
		FROM " . $table_name . ' w
		LEFT JOIN ' . $wpdb->users . ' u ON w.customer_id = u.ID
		LEFT JOIN ' . $wpdb->usermeta . " fn ON u.ID = fn.user_id AND fn.meta_key = 'first_name'
		LEFT JOIN " . $wpdb->usermeta . " ln ON u.ID = ln.user_id AND ln.meta_key = 'last_name'
		LEFT JOIN " . $wpdb->posts . ' p ON w.product_id = p.ID
		LEFT JOIN ' . $wpdb->postmeta . " pm ON w.product_id = pm.post_id AND pm.meta_key = '_sku'
		WHERE 1=1";
		$query_params = array();

		/*
		 * Add filters.
		 *
		 * 'all' is the UI's "no status chosen" value, not a status.
		 *
		 * The Warranty Records screen initialises its filter to 'all' and sent
		 * it verbatim, so the default view filtered `WHERE status = 'all'` and
		 * returned nothing. The client no longer sends it; this rejects it too,
		 * so no other caller can reproduce the same empty screen.
		 */
		if ( $status && 'all' !== $status ) {
			$query         .= ' AND w.status = %s';
			$query_params[] = $status;
		}

		if ( $customer_id ) {
			$query         .= ' AND w.customer_id = %d';
			$query_params[] = $customer_id;
		}

		if ( $order_id ) {
			$query         .= ' AND w.order_id = %d';
			$query_params[] = $order_id;
		}

		if ( $search ) {
			/*
			 * Name, email, product, SKU — and the serial.
			 *
			 * The serial is what somebody standing at a counter with a broken
			 * appliance actually has. Matched on the normalised form as well as
			 * the raw one, because serials are printed in upper case with
			 * spaces and typed in neither.
			 *
			 * Same usermeta aliases as the SELECT — see the note there.
			 */
			$query       .= " AND (CONCAT( COALESCE( fn.meta_value, '' ), ' ', COALESCE( ln.meta_value, '' ) ) LIKE %s OR u.user_email LIKE %s OR p.post_title LIKE %s OR pm.meta_value LIKE %s OR w.serial_number LIKE %s)";
			$search_param = '%' . $wpdb->esc_like( $search ) . '%';
			$serial_param = '%' . $wpdb->esc_like( \WarrantyCart\Core\Serials::normalise( $search ) ) . '%';
			$query_params = array_merge(
				$query_params,
				array( $search_param, $search_param, $search_param, $search_param, $serial_param )
			);
		}

		/*
		 * The count is taken before pagination is appended, not by rewriting
		 * the paginated query afterwards.
		 *
		 * It used to be str_replace( 'SELECT *', 'SELECT COUNT(*)', $query ).
		 * The query begins `SELECT w.*,`, so that needle never matched and
		 * nothing was replaced; the "count query" was the full column list with
		 * its ORDER BY and LIMIT stripped, and $wpdb->get_var() returns the
		 * first column of the first row — so the reported total was a warranty
		 * id. Pagination was computed from it.
		 */
		$count_query = 'SELECT COUNT(*) FROM ( ' . $query . ' ) counted';

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$total_count = (int) $wpdb->get_var(
			empty( $query_params ) ? $count_query : $wpdb->prepare( $count_query, $query_params )
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// Add pagination
		$offset         = ( $page - 1 ) * $per_page;
		$query         .= ' ORDER BY w.created_at DESC LIMIT %d OFFSET %d';
		$query_params[] = $per_page;
		$query_params[] = $offset;

		// Execute query
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$warranty_records = $wpdb->get_results( $wpdb->prepare( $query, $query_params ) );

		$total_pages = $per_page > 0 ? ceil( $total_count / $per_page ) : 1;

		// Format records
		$records = array();
		foreach ( $warranty_records as $record ) {
			$records[] = array(
				'id'                => (int) $record->id,
				'order_id'          => (int) $record->order_id,
				'customer_id'       => (int) $record->customer_id,
				'customer_name'     => $record->customer_name,
				'customer_email'    => $record->customer_email,
				'customer_avatar'   => $this->avatar_url( $record ),
				'product_id'        => (int) $record->product_id,
				'product_name'      => $record->product_name,
				'product_sku'       => $record->product_sku,
				'serial_number'     => (string) ( $record->serial_number ?? '' ),
				'warranty_type'     => $record->warranty_type,
				'duration'          => (int) $record->duration,
				'duration_unit'     => $record->duration_unit,
				'purchase_date'     => $record->purchase_date,
				'expiration_date'   => $record->expiration_date,
				'purchase_price'    => (float) $record->purchase_price,
				'currency'          => $record->currency,
				'payment_method'    => $record->payment_method,
				'status'            => $record->status,
				'source'            => $record->source,
				'coverage_details'  => $record->coverage_details,
				'reminder_sent'     => (bool) $record->reminder_sent,
				'expired_notified'  => (bool) $record->expired_notified,
				'covered_product'   => self::covered_product_name( $record ),
				'renewal_count'     => (int) $record->renewal_count,
				'last_renewal_date' => $record->last_renewal_date,
				'created_at'        => $record->created_at,
				'updated_at'        => $record->updated_at,
			);
		}

		return $this->success_response(
			array(
				'records'     => $records,
				'total'       => (int) $total_count,
				'page'        => $page,
				'per_page'    => $per_page,
				'total_pages' => $total_pages,
			)
		);
	}

	/**
	 * Get single warranty record
	 *
	 * Retrieves detailed information for a specific warranty record.
	 */
	public function get_warranty_record( WP_REST_Request $request ) {
		global $wpdb;

		$table_name  = $wpdb->prefix . 'warranty_cart_warranties';
		$warranty_id = $request->get_param( 'id' );

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $warranty_id ) );

		if ( ! $record ) {
			return $this->error_response( __( 'Warranty record not found.', 'warranty-cart' ), 404 );
		}

		/*
		 * Resolve the names this table does not hold.
		 *
		 * `SELECT *` above returns the warranties table's own columns, and
		 * `product_name`, `product_sku`, `customer_name` and `customer_email`
		 * are none of them — the list query joins for those. So this endpoint
		 * answered with four nulls for as long as it has existed, which nothing
		 * noticed because its only caller was a dialog that already had the row
		 * from the list.
		 */
		$product = wc_get_product( (int) $record->product_id );
		$user    = $record->customer_id ? get_userdata( (int) $record->customer_id ) : false;
		$order   = wc_get_order( (int) $record->order_id );

		$record->product_name  = $product ? $product->get_name() : '';
		$record->product_sku   = $product ? $product->get_sku() : '';
		$record->customer_name = $user ? $user->display_name : ( $order ? trim( $order->get_formatted_billing_full_name() ) : '' );

		$record->customer_email = $user
			? $user->user_email
			: ( $order ? $order->get_billing_email() : '' );

		$warranty = array(
			'id'                => (int) $record->id,
			'order_id'          => (int) $record->order_id,
			'customer_id'       => (int) $record->customer_id,
			'customer_name'     => $record->customer_name,
			'customer_email'    => $record->customer_email,
			'customer_avatar'   => $this->avatar_url( $record ),
			'product_id'        => (int) $record->product_id,
			'product_name'      => $record->product_name,
			'product_sku'       => $record->product_sku,
			'serial_number'     => (string) ( $record->serial_number ?? '' ),
			'warranty_type'     => $record->warranty_type,
			'duration'          => (int) $record->duration,
			'duration_unit'     => $record->duration_unit,
			'purchase_date'     => $record->purchase_date,
			'expiration_date'   => $record->expiration_date,
			'purchase_price'    => (float) $record->purchase_price,
			'currency'          => $record->currency,
			'payment_method'    => $record->payment_method,
			'status'            => $record->status,
			'source'            => $record->source,
			'coverage_details'  => $record->coverage_details,
			'reminder_sent'     => (bool) $record->reminder_sent,
			'expired_notified'  => (bool) $record->expired_notified,
			'covered_product'   => self::covered_product_name( $record ),
			'renewal_count'     => (int) $record->renewal_count,
			'last_renewal_date' => $record->last_renewal_date,

			/*
			 * The single-record shape only. A history per row would be a query
			 * per row on a list that already pages fifty at a time, and the list
			 * has nowhere to show it.
			 */
			'renewals'          => self::renewal_history( (int) $record->id ),
			'created_at'        => $record->created_at,
			'updated_at'        => $record->updated_at,
		);

		return $this->success_response( $warranty );
	}

	/**
	 * Update warranty record
	 *
	 * Updates an existing warranty record with new data.
	 */
	public function update_warranty_record( WP_REST_Request $request ) {
		global $wpdb;

		$table_name  = $wpdb->prefix . 'warranty_cart_warranties';
		$warranty_id = $request->get_param( 'id' );

		// Check if record exists
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table_name} WHERE id = %d", $warranty_id ) );

		if ( ! $existing ) {
			return $this->error_response( __( 'Warranty record not found.', 'warranty-cart' ), 404 );
		}

		// Prepare update data
		$update_data   = array();
		$update_format = array();

		$status = $request->get_param( 'status' );
		if ( null !== $status ) {
			$update_data['status'] = $status;
			$update_format[]       = '%s';
		}

		$expiration_date = $request->get_param( 'expiration_date' );
		if ( null !== $expiration_date ) {
			$update_data['expiration_date'] = $expiration_date;
			$update_format[]                = '%s';
		}

		$reminder_sent = $request->get_param( 'reminder_sent' );
		if ( null !== $reminder_sent ) {
			$update_data['reminder_sent'] = $reminder_sent ? 1 : 0;
			$update_format[]              = '%d';
		}

		$expired_notified = $request->get_param( 'expired_notified' );
		if ( null !== $expired_notified ) {
			$update_data['expired_notified'] = $expired_notified ? 1 : 0;
			$update_format[]                 = '%d';
		}

		if ( empty( $update_data ) ) {
			return $this->error_response( __( 'No valid fields to update.', 'warranty-cart' ), 400 );
		}

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

		// Update record
		$result = $wpdb->update(
			$table_name,
			$update_data,
			array( 'id' => $warranty_id ),
			$update_format,
			array( '%d' )
		);

		if ( false === $result ) {
			return $this->error_response( __( 'Failed to update warranty record.', 'warranty-cart' ), 500 );
		}

		// Get updated record
		$updated_record = $this->get_warranty_record( $request );
		if ( is_wp_error( $updated_record ) ) {
			return $updated_record;
		}

		return $this->success_response(
			array(
				'message' => __( 'Warranty record updated successfully.', 'warranty-cart' ),
				'record'  => $updated_record->get_data(),
			)
		);
	}

	/**
	 * Renew warranty record
	 *
	 * Renews an existing warranty record by extending its expiration date.
	 */
	public function renew_warranty_record( WP_REST_Request $request ) {
		$warranty_id = $request->get_param( 'id' );

		// Check if Warranties has the renew method
		if ( ! method_exists( WarrantyLifecycle::class, 'renew_warranty' ) ) {
			return $this->error_response( __( 'Warranty renewal is not available.', 'warranty-cart' ), 400 );
		}

		$handler = new Warranties();
		$success = $handler->renew_warranty( $warranty_id );

		if ( $success ) {
			return $this->success_response(
				array(
					'message' => __( 'Warranty renewed successfully.', 'warranty-cart' ),
					'id'      => $warranty_id,
				)
			);
		}

		return $this->error_response( __( 'Failed to renew warranty.', 'warranty-cart' ), 500 );
	}

	/**
	 * Get warranty products (alias for get_warranties)
	 */
	public function get_warranty_products( WP_REST_Request $request ): WP_REST_Response {
		return $this->get_warranties( $request );
	}

	/**
	 * A warranty product's thumbnail.
	 *
	 * The featured image, at WooCommerce's own thumbnail size rather than full
	 * resolution: these are rendered at 40px in a table and about 64px in the
	 * storefront offer, and shipping a 2,000px original to fill either is a
	 * megabyte a customer waits for to see a postage stamp.
	 *
	 * Empty when a product has no image, which the caller has to expect —
	 * warranties are frequently sold without one, and a broken image icon is
	 * worse than no image at all.
	 *
	 * @param WC_Product $product The warranty product.
	 *
	 * @return string A URL, or ''.
	 */
	private static function product_image( WC_Product $product ): string {
		$image_id = $product->get_image_id();

		if ( ! $image_id ) {
			return '';
		}

		$src = wp_get_attachment_image_src( (int) $image_id, 'woocommerce_thumbnail' );

		return $src ? (string) $src[0] : '';
	}

	/**
	 * A warranty's renewals, shaped for the API.
	 *
	 * @param int $warranty_id The warranty.
	 *
	 * @return array<int, array<string, mixed>>
	 */
	private static function renewal_history( int $warranty_id ): array {
		$history = array();

		foreach ( WarrantyLifecycle::renewal_history( $warranty_id ) as $renewal ) {
			$history[] = array(
				'id'                  => (int) $renewal->id,
				'order_id'            => $renewal->order_id ? (int) $renewal->order_id : null,
				'order_url'           => $renewal->order_id
					? get_edit_post_link( (int) $renewal->order_id, 'raw' )
					: null,
				'previous_expiration' => $renewal->previous_expiration,
				'new_expiration'      => $renewal->new_expiration,
				'amount'              => null === $renewal->amount ? null : (float) $renewal->amount,
				'currency'            => $renewal->currency,
				'source'              => $renewal->source,
				'created_at'          => $renewal->created_at,
			);
		}

		return $history;
	}

	/**
	 * The name of the product a warranty was bought to cover.
	 *
	 * Empty for anything sold from the `[warranty_cart]` selector, and for every
	 * record written before `covered_product_id` existed — neither covered a
	 * particular item, so there is nothing to name.
	 *
	 * @param object $record A row from the warranties table.
	 *
	 * @return string
	 */
	private static function covered_product_name( object $record ): string {
		$product = wc_get_product( (int) ( $record->covered_product_id ?? 0 ) );

		return $product ? wp_strip_all_tags( $product->get_name() ) : '';
	}

	/**
	 * Export the warranty catalogue as CSV.
	 *
	 * Runs the list's own query rather than a second one written for the
	 * export, so the file holds exactly the rows the screen was showing —
	 * including whatever search and status filter were applied to it.
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function export_warranties_csv( WP_REST_Request $request ) {
		$data = $this->get_warranties( $this->unpaged( $request ) )->get_data();

		$warranties = $data['data']['warranties'] ?? array();

		if ( empty( $warranties ) ) {
			return $this->error_response( __( 'No warranties found to export.', 'warranty-cart' ), 404 );
		}

		$header = array(
			__( 'ID', 'warranty-cart' ),
			__( 'Name', 'warranty-cart' ),
			__( 'SKU', 'warranty-cart' ),
			__( 'Type', 'warranty-cart' ),
			__( 'Status', 'warranty-cart' ),
			__( 'Price', 'warranty-cart' ),
			__( 'Currency', 'warranty-cart' ),
			__( 'Duration', 'warranty-cart' ),
			__( 'Duration Unit', 'warranty-cart' ),
			__( 'Sales', 'warranty-cart' ),
			__( 'Description', 'warranty-cart' ),
		);

		$currency = \get_woocommerce_currency();
		$rows     = array();

		foreach ( $warranties as $warranty ) {
			$rows[] = array(
				$warranty['id'] ?? '',
				$warranty['name'] ?? '',
				$warranty['sku'] ?? '',
				$warranty['type'] ?? '',
				$warranty['status'] ?? '',
				\wc_format_decimal( (float) ( $warranty['price'] ?? 0 ), \wc_get_price_decimals() ),
				$currency,
				$warranty['duration'] ?? '',
				$warranty['duration_unit'] ?? '',
				$warranty['sales'] ?? 0,
				$warranty['description'] ?? '',
			);
		}

		return $this->file_response(
			$this->export_filename( 'warranties' ),
			Csv::build( $header, $rows ),
			'text/csv',
			$this->truncation_notice( (int) ( $data['data']['total'] ?? 0 ), self::EXPORT_LIMIT )
		);
	}

	/**
	 * Export the sold warranty records as CSV.
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function export_warranty_records_csv( WP_REST_Request $request ) {
		$data = $this->get_warranty_records( $this->unpaged( $request ) )->get_data();

		$records = $data['data']['records'] ?? array();

		if ( empty( $records ) ) {
			return $this->error_response( __( 'No warranty records found to export.', 'warranty-cart' ), 404 );
		}

		$header = array(
			__( 'Record ID', 'warranty-cart' ),
			__( 'Order ID', 'warranty-cart' ),
			__( 'Customer', 'warranty-cart' ),
			__( 'Email', 'warranty-cart' ),
			__( 'Warranty', 'warranty-cart' ),
			__( 'SKU', 'warranty-cart' ),
			__( 'Covers', 'warranty-cart' ),
			__( 'Type', 'warranty-cart' ),
			__( 'Status', 'warranty-cart' ),
			__( 'Purchased', 'warranty-cart' ),
			__( 'Expires', 'warranty-cart' ),
			__( 'Duration', 'warranty-cart' ),
			__( 'Duration Unit', 'warranty-cart' ),
			__( 'Price Paid', 'warranty-cart' ),
			__( 'Currency', 'warranty-cart' ),
			__( 'Payment Method', 'warranty-cart' ),
			__( 'Renewals', 'warranty-cart' ),
			__( 'Last Renewal', 'warranty-cart' ),
		);

		$rows = array();

		foreach ( $records as $record ) {
			$rows[] = array(
				$record['id'] ?? '',
				$record['order_id'] ?? '',
				$record['customer_name'] ?? '',
				$record['customer_email'] ?? '',
				$record['product_name'] ?? '',
				$record['product_sku'] ?? '',
				$record['covered_product'] ?? '',
				$record['warranty_type'] ?? '',
				$record['status'] ?? '',
				$record['purchase_date'] ?? '',
				$record['expiration_date'] ?? '',
				$record['duration'] ?? '',
				$record['duration_unit'] ?? '',
				\wc_format_decimal( (float) ( $record['purchase_price'] ?? 0 ), \wc_get_price_decimals() ),
				$record['currency'] ?? '',
				$record['payment_method'] ?? '',
				$record['renewal_count'] ?? 0,
				$record['last_renewal_date'] ?? '',
			);
		}

		return $this->file_response(
			$this->export_filename( 'warranty-records' ),
			Csv::build( $header, $rows ),
			'text/csv',
			$this->truncation_notice( (int) ( $data['data']['total'] ?? 0 ), self::EXPORT_LIMIT )
		);
	}

	/**
	 * The same request, asking for everything instead of one page.
	 *
	 * An export of the page you happen to be looking at is not an export. The
	 * filters are kept — the point is the file matching the screen — but the
	 * paging is dropped.
	 *
	 * @param WP_REST_Request $request The original request.
	 *
	 * @return WP_REST_Request
	 */
	private function unpaged( WP_REST_Request $request ): WP_REST_Request {
		$copy = clone $request;

		$copy->set_param( 'page', 1 );
		$copy->set_param( 'per_page', self::EXPORT_LIMIT );

		return $copy;
	}
}
