<?php
/**
 * REST endpoint controller for estimates.
 *
 * @since 1.0.0
 */

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

class ZINV_REST_Estimates {

	private string $namespace;
	private string $rest_base;
	private ZINV_Estimate $estimate;

	public function __construct( string $namespace ) {
		$this->namespace = $namespace;
		$this->rest_base = 'estimates';
		$this->estimate  = new ZINV_Estimate();

		add_filter( 'rest_pre_serve_request', [ $this, 'maybe_serve_pdf' ], 10, 3 );
	}

	public function register(): void {
		register_rest_route( $this->namespace, '/' . $this->rest_base, [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_items' ],
				'permission_callback' => [ $this, 'check_permission' ],
				'args'                => $this->get_collection_params(),
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_item' ],
				'permission_callback' => [ $this, 'check_permission' ],
				'args'                => $this->get_create_params(),
			],
		] );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_item' ],
				'permission_callback' => [ $this, 'check_permission' ],
				'args'                => $this->get_id_param(),
			],
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_item' ],
				'permission_callback' => [ $this, 'check_permission' ],
				'args'                => $this->get_update_params(),
			],
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'void_item' ],
				'permission_callback' => [ $this, 'check_permission' ],
				'args'                => $this->get_id_param(),
			],
		] );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)/send', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'send_item' ],
			'permission_callback' => [ $this, 'check_permission' ],
			'args'                => $this->get_id_param(),
		] );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)/approve', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'approve_item' ],
			'permission_callback' => [ $this, 'check_permission' ],
			'args'                => $this->get_id_param(),
		] );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)/decline', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'decline_item' ],
			'permission_callback' => [ $this, 'check_permission' ],
			'args'                => $this->get_id_param(),
		] );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)/convert', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'convert_item' ],
			// Convert creates an invoice → user must also be able to manage invoices.
			'permission_callback' => fn() => current_user_can( 'zinv_manage_estimates' ) && current_user_can( 'zinv_manage_invoices' ),
			'args'                => $this->get_id_param(),
		] );

		register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)/pdf', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'get_pdf' ],
			'permission_callback' => [ $this, 'check_permission' ],
			'args'                => $this->get_id_param(),
		] );
	}

	public function check_permission(): bool {
		return current_user_can( 'zinv_manage_estimates' );
	}

	public function get_items( WP_REST_Request $request ): WP_REST_Response {
		$args = [
			'page'      => $request->get_param( 'page' ) ?? 1,
			'per_page'  => min( $request->get_param( 'per_page' ) ?? 20, 100 ),
			'search'    => sanitize_text_field( $request->get_param( 'search' ) ?? '' ),
			'status'    => sanitize_text_field( $request->get_param( 'status' ) ?? '' ),
			'client_id' => $request->get_param( 'client_id' ) ? absint( $request->get_param( 'client_id' ) ) : 0,
			'date_from' => sanitize_text_field( $request->get_param( 'date_from' ) ?? '' ),
			'date_to'   => sanitize_text_field( $request->get_param( 'date_to' ) ?? '' ),
			'orderby'   => sanitize_text_field( $request->get_param( 'orderby' ) ?? 'created_at' ),
			'order'     => sanitize_text_field( $request->get_param( 'order' ) ?? 'DESC' ),
		];

		$result = $this->estimate->list( $args );

		$data = [
			'items'       => $result['items'],
			'total'       => (int) $result['total'],
			'total_pages' => (int) $result['total_pages'],
			'page'        => (int) $result['page'],
			'per_page'    => (int) $result['per_page'],
		];

		$response = new WP_REST_Response( $data, 200 );
		$response->header( 'X-WP-Total', (int) $result['total'] );
		$response->header( 'X-WP-TotalPages', (int) $result['total_pages'] );

		return $response;
	}

	public function get_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id       = absint( $request->get_param( 'id' ) );
		$estimate = $this->estimate->get( $id );

		if ( ! $estimate ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		return new WP_REST_Response( $estimate, 200 );
	}

	public function create_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$client_id = absint( $request->get_param( 'client_id' ) );

		if ( ! $client_id ) {
			return new WP_Error(
				'zinv_estimate_client_required',
				__( 'Client is required.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		$line_items = $request->get_param( 'line_items' ) ?? [];
		if ( ! is_array( $line_items ) ) {
			$line_items = [];
		}

		foreach ( $line_items as &$item ) {
			$item['description'] = sanitize_text_field( $item['description'] ?? '' );
		}
		unset( $item );

		$line_item_error = $this->validate_line_items( $line_items );
		if ( is_wp_error( $line_item_error ) ) {
			return $line_item_error;
		}

		$discount_type_raw = sanitize_text_field( $request->get_param( 'discount_type' ) ?? '' );
		if ( 'percent' === $discount_type_raw ) {
			$discount_type_raw = 'percentage';
		}

		$discount_value_raw = $request->get_param( 'discount_value' );
		$discount_value     = is_numeric( $discount_value_raw ) ? (string) $discount_value_raw : '0';
		if ( bccomp( $discount_value, '0', 8 ) < 0 ) {
			return new WP_Error( 'zinv_estimate_invalid_discount_value', __( 'Discount value cannot be negative.', 'zoventic-invoices' ), [ 'status' => 400 ] );
		}

		if ( '' !== $discount_type_raw && ! in_array( $discount_type_raw, [ 'percentage', 'fixed' ], true ) ) {
			return new WP_Error( 'zinv_estimate_invalid_discount_type', __( 'Invalid discount type.', 'zoventic-invoices' ), [ 'status' => 400 ] );
		}

		// Validate template against the allowlist (Pro-aware).
		$template_error = $this->validate_template( $request->get_param( 'template' ) );
		if ( is_wp_error( $template_error ) ) {
			return $template_error;
		}

		$data = [
			'client_id'      => $client_id,
			'issue_date'     => sanitize_text_field( $request->get_param( 'issue_date' ) ?? '' ),
			'expiry_date'    => sanitize_text_field( $request->get_param( 'expiry_date' ) ?? '' ),
			'currency'       => sanitize_text_field( $request->get_param( 'currency' ) ?? 'USD' ),
			'discount_type'  => $discount_type_raw,
			'discount_value' => $discount_value,
			'template'       => sanitize_text_field( $request->get_param( 'template' ) ?? 'clean-sheet' ),
			'document_title' => sanitize_text_field( $request->get_param( 'document_title' ) ?? '' ),
			'notes'          => sanitize_textarea_field( $request->get_param( 'notes' ) ?? '' ),
			'terms'          => sanitize_textarea_field( $request->get_param( 'terms' ) ?? '' ),
			'auto_convert'   => (bool) $request->get_param( 'auto_convert' ),
		];

		$id = $this->estimate->create( $data, $line_items );

		if ( ! $id ) {
			return new WP_Error(
				'zinv_estimate_create_failed',
				__( 'Failed to create estimate.', 'zoventic-invoices' ),
				[ 'status' => 500 ]
			);
		}

		$estimate = $this->estimate->get( $id );
		do_action( 'zinv_estimate_created', $id, $data );
		$response = new WP_REST_Response( $estimate, 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $id ) ) );

		return $response;
	}

	public function update_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id      = absint( $request->get_param( 'id' ) );
		$version = absint( $request->get_param( 'version' ) );

		if ( ! $version ) {
			return new WP_Error(
				'zinv_estimate_version_required',
				__( 'Version is required for optimistic locking.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		$existing = $this->estimate->get( $id );
		if ( ! $existing ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		$data        = [];
		$sanitizers  = [
			'client_id'      => 'absint',
			'issue_date'     => 'sanitize_text_field',
			'expiry_date'    => 'sanitize_text_field',
			'discount_value' => static fn( $v ) => is_numeric( $v ) ? (string) $v : '0',
			'notes'          => 'sanitize_textarea_field',
			'terms'          => 'sanitize_textarea_field',
			'currency'       => 'sanitize_text_field',
			'template'       => 'sanitize_text_field',
			'document_title' => 'sanitize_text_field',
			'auto_convert'   => static fn( $v ) => (bool) $v,
		];

		foreach ( $sanitizers as $field => $fn ) {
			$value = $request->get_param( $field );
			if ( null !== $value ) {
				$data[ $field ] = $fn( $value );
			}
		}

		// discount_type: sanitize + whitelist (same as create_item)
		$discount_type_raw = $request->get_param( 'discount_type' );
		if ( null !== $discount_type_raw ) {
			$dt = sanitize_text_field( $discount_type_raw );
			if ( 'percent' === $dt ) {
				$dt = 'percentage';
			}
			if ( ! in_array( $dt, [ '', 'percentage', 'fixed' ], true ) ) {
				return new WP_Error( 'zinv_estimate_invalid_discount_type', __( 'Invalid discount type.', 'zoventic-invoices' ), [ 'status' => 400 ] );
			}
			$data['discount_type'] = $dt;
		}

		if ( isset( $data['discount_value'] ) && bccomp( $data['discount_value'], '0', 8 ) < 0 ) {
			return new WP_Error( 'zinv_estimate_invalid_discount_value', __( 'Discount value cannot be negative.', 'zoventic-invoices' ), [ 'status' => 400 ] );
		}

		// Validate template against the allowlist when one is being changed.
		if ( isset( $data['template'] ) ) {
			$template_error = $this->validate_template( $data['template'] );
			if ( is_wp_error( $template_error ) ) {
				return $template_error;
			}
		}

		$line_items = $request->get_param( 'line_items' );
		if ( null !== $line_items && ! is_array( $line_items ) ) {
			$line_items = [];
		}

		if ( null !== $line_items ) {
			foreach ( $line_items as &$item ) {
				$item['description'] = sanitize_text_field( $item['description'] ?? '' );
			}
			unset( $item );

			$line_item_error = $this->validate_line_items( $line_items );
			if ( is_wp_error( $line_item_error ) ) {
				return $line_item_error;
			}
		}

		$updated = $this->estimate->update( $id, $data, $line_items, $version );

		if ( ! $updated ) {
			return new WP_Error(
				'zinv_estimate_version_conflict',
				__( 'Update failed. The record may have been modified by another user.', 'zoventic-invoices' ),
				[ 'status' => 409 ]
			);
		}

		do_action( 'zinv_estimate_updated', $id );
		return new WP_REST_Response( $this->estimate->get( $id ), 200 );
	}

	public function void_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id       = absint( $request->get_param( 'id' ) );
		$existing = $this->estimate->get( $id );

		if ( ! $existing ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		$voided = $this->estimate->void( $id );

		if ( ! $voided ) {
			return new WP_Error(
				'zinv_estimate_void_failed',
				__( 'Cannot void this estimate.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		do_action( 'zinv_estimate_voided', $id );
		return new WP_REST_Response( [
			'voided'   => true,
			'previous' => $existing,
		], 200 );
	}

	public function send_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id       = absint( $request->get_param( 'id' ) );
		$existing = $this->estimate->get( $id );

		if ( ! $existing ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		$sent = $this->estimate->send( $id );

		if ( ! $sent ) {
			return new WP_Error(
				'zinv_estimate_send_failed',
				__( 'Cannot send this estimate.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		do_action( 'zinv_estimate_sent', $id );
		return new WP_REST_Response( $this->estimate->get( $id ), 200 );
	}

	public function approve_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id = absint( $request->get_param( 'id' ) );
		$existing = $this->estimate->get( $id );

		if ( ! $existing ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		$approved = $this->estimate->approve( $id );

		if ( ! $approved ) {
			return new WP_Error(
				'zinv_estimate_approve_failed',
				__( 'Cannot approve this estimate.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		do_action( 'zinv_estimate_approved', $id );

		// Auto-convert to invoice if the flag is set.
		$refreshed = $this->estimate->get( $id );
		if ( $refreshed && ! empty( $refreshed['auto_convert'] ) ) {
			$invoice_id = $this->estimate->convert_to_invoice( $id );
			if ( $invoice_id ) {
				do_action( 'zinv_estimate_converted', $id, $invoice_id );
			}
		}

		return new WP_REST_Response( $this->estimate->get( $id ), 200 );
	}

	public function decline_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id = absint( $request->get_param( 'id' ) );
		$existing = $this->estimate->get( $id );

		if ( ! $existing ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		$declined = $this->estimate->decline( $id );

		if ( ! $declined ) {
			return new WP_Error(
				'zinv_estimate_decline_failed',
				__( 'Cannot decline this estimate.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		do_action( 'zinv_estimate_declined', $id );
		return new WP_REST_Response( $this->estimate->get( $id ), 200 );
	}

	public function convert_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id = absint( $request->get_param( 'id' ) );
		$existing = $this->estimate->get( $id );

		if ( ! $existing ) {
			return new WP_Error(
				'zinv_estimate_not_found',
				__( 'Estimate not found.', 'zoventic-invoices' ),
				[ 'status' => 404 ]
			);
		}

		if ( 'converted' === ( $existing['status'] ?? '' ) ) {
			return new WP_Error(
				'zinv_estimate_already_converted',
				__( 'This estimate has already been converted to an invoice.', 'zoventic-invoices' ),
				[ 'status' => 409 ]
			);
		}

		$convertible_statuses = [ 'sent', 'approved' ];
		if ( ! in_array( $existing['status'] ?? '', $convertible_statuses, true ) ) {
			return new WP_Error(
				'zinv_estimate_not_convertible',
				sprintf(
					/* translators: %s: estimate status */
					__( 'Cannot convert an estimate with status "%s". Must be sent or approved.', 'zoventic-invoices' ),
					$existing['status'] ?? ''
				),
				[ 'status' => 409 ]
			);
		}

		$invoice_id = $this->estimate->convert_to_invoice( $id );

		if ( ! $invoice_id ) {
			return new WP_Error(
				'zinv_estimate_convert_failed',
				__( 'Cannot convert this estimate to invoice.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		$invoice = ( new ZINV_Invoice() )->get( $invoice_id );

		do_action( 'zinv_estimate_converted', $id, $invoice_id );
		return new WP_REST_Response( [
			'converted' => true,
			'invoice'   => $invoice,
			'estimate'  => $this->estimate->get( $id ),
		], 201 );
	}

	public function get_pdf( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id       = absint( $request->get_param( 'id' ) );
		$estimate = $this->estimate->get( $id );

		if ( ! $estimate ) {
			return new WP_Error( 'zinv_estimate_not_found', __( 'Estimate not found.', 'zoventic-invoices' ), [ 'status' => 404 ] );
		}

		if ( ! class_exists( 'ZINV_PDF_Generator' ) ) {
			return new WP_Error( 'zinv_pdf_not_available', __( 'PDF generation is not available.', 'zoventic-invoices' ), [ 'status' => 500 ] );
		}

		$disposition_raw = sanitize_text_field( (string) ( $request->get_param( 'disposition' ) ?? $request->get_param( 'download' ) ) );
		$disposition     = ( 'attachment' === $disposition_raw || '1' === $disposition_raw || 'true' === $disposition_raw ) ? 'attachment' : 'inline';

		$response = new WP_REST_Response( null, 200 );
		$response->header( 'X-ZINV-ESTIMATE-PDF', (string) $id );
		$response->header( 'X-ZINV-ESTIMATE-PDF-DISPOSITION', $disposition );
		return $response;
	}

	/**
	 * Stream the estimate PDF on rest_pre_serve_request, bypassing JSON encoding.
	 *
	 * Triggered only for the /estimates/{id}/pdf route — identified by the
	 * X-ZINV-ESTIMATE-PDF header that get_pdf() sets. Returning true tells
	 * WP_REST_Server::serve_request() the response is already served.
	 *
	 * @param bool             $served  Whether the request was already served.
	 * @param WP_HTTP_Response $result  The response object.
	 * @param WP_REST_Request  $request The request object.
	 * @return bool
	 */
	public function maybe_serve_pdf( $served, $result, $request ) {
		if ( $served || ! ( $result instanceof WP_HTTP_Response ) ) {
			return $served;
		}

		$headers       = $result->get_headers();
		$headers_lower = array_change_key_case( (array) $headers, CASE_LOWER );

		if ( empty( $headers_lower['x-zinv-estimate-pdf'] ) ) {
			return $served;
		}

		$id          = absint( $headers_lower['x-zinv-estimate-pdf'] );
		$disposition = sanitize_text_field( (string) ( $headers_lower['x-zinv-estimate-pdf-disposition'] ?? 'inline' ) );
		$estimate    = $this->estimate->get( $id );
		if ( ! $estimate ) {
			return $served;
		}

		try {
			ZINV_PDF_Generator::stream_estimate( $id, $estimate['template'] ?? 'clean-sheet', $disposition );
		} catch ( \Exception $e ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( 'Zoventic Invoices PDF generation failed for estimate ' . $id . ': ' . $e->getMessage() );
			status_header( 500 );
			header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
			$error_data = [ 'status' => 500 ];
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				$error_data['debug'] = $e->getMessage();
			}
			echo wp_json_encode( [
				'code'    => 'zinv_pdf_generation_failed',
				'message' => __( 'Failed to generate PDF.', 'zoventic-invoices' ),
				'data'    => $error_data,
			] );
		}
		return true;
	}

	/**
	 * Validate a requested template slug against the Pro-aware allowlist.
	 *
	 * Returns null when the param is absent/empty (CRUD applies the default)
	 * or valid; a WP_Error otherwise.
	 *
	 * @param mixed $template Raw 'template' request param.
	 * @return WP_Error|null
	 */
	private function validate_template( $template ): ?WP_Error {
		if ( null === $template || '' === $template ) {
			return null;
		}

		$slug = sanitize_text_field( (string) $template );

		if ( ! class_exists( 'ZINV_PDF_Generator' ) || ! ZINV_PDF_Generator::is_valid_template( $slug ) ) {
			return new WP_Error(
				'zinv_estimate_invalid_template',
				__( 'The selected template is not available.', 'zoventic-invoices' ),
				[ 'status' => 400 ]
			);
		}

		return null;
	}

	private function validate_line_items( array &$line_items ): true|WP_Error {
		foreach ( $line_items as $index => &$item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$item['description'] = sanitize_text_field( $item['description'] ?? '' );
			$item['quantity']    = isset( $item['quantity'] )   ? (float) $item['quantity']   : 0;
			$item['unit_price']  = isset( $item['unit_price'] ) ? (float) $item['unit_price'] : 0;
			$item['tax_rate']    = isset( $item['tax_rate'] )   ? (float) $item['tax_rate']   : 0;

			$qty   = $item['quantity'];
			$price = $item['unit_price'];
			$tax   = $item['tax_rate'];

			if ( $qty < 0 ) {
				return new WP_Error(
					'zinv_line_item_invalid_quantity',
					sprintf(
						/* translators: %d: line item index (1-based) */
						__( 'Line item %d: quantity cannot be negative.', 'zoventic-invoices' ),
						$index + 1
					),
					[ 'status' => 400 ]
				);
			}

			if ( $price < 0 ) {
				return new WP_Error(
					'zinv_line_item_invalid_price',
					sprintf(
						/* translators: %d: line item index (1-based) */
						__( 'Line item %d: unit price cannot be negative.', 'zoventic-invoices' ),
						$index + 1
					),
					[ 'status' => 400 ]
				);
			}

			if ( $tax < 0 || $tax > 100 ) {
				return new WP_Error(
					'zinv_line_item_invalid_tax',
					sprintf(
						/* translators: %d: line item index (1-based) */
						__( 'Line item %d: tax rate must be between 0 and 100.', 'zoventic-invoices' ),
						$index + 1
					),
					[ 'status' => 400 ]
				);
			}
		}

		return true;
	}

	private function get_id_param(): array {
		return [
			'id' => [
				'type'              => 'integer',
				'required'          => true,
				'validate_callback' => fn( $v ) => is_numeric( $v ) && absint( $v ) > 0,
				'sanitize_callback' => 'absint',
			],
		];
	}

	private function get_collection_params(): array {
		return [
			'page' => [
				'type'              => 'integer',
				'default'           => 1,
				'sanitize_callback' => 'absint',
				'validate_callback' => fn( $v ) => is_numeric( $v ) && absint( $v ) >= 1,
			],
			'per_page' => [
				'type'              => 'integer',
				'default'           => 20,
				'sanitize_callback' => 'absint',
				'validate_callback' => fn( $v ) => is_numeric( $v ) && absint( $v ) >= 1 && absint( $v ) <= 100,
			],
			'search' => [
				'type'              => 'string',
				'default'           => '',
				'sanitize_callback' => 'sanitize_text_field',
			],
			'status' => [
				'type'              => 'string',
				'default'           => '',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => fn( $v ) => empty( $v ) || in_array( $v, [ 'draft', 'sent', 'approved', 'declined', 'expired', 'converted', 'void' ], true ),
			],
			'client_id' => [
				'type'              => 'integer',
				'default'           => 0,
				'sanitize_callback' => 'absint',
			],
			'date_from' => [
				'type'              => 'string',
				'default'           => '',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => fn( $v ) => empty( $v ) || (bool) \DateTime::createFromFormat( 'Y-m-d', $v ),
			],
			'date_to' => [
				'type'              => 'string',
				'default'           => '',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => fn( $v ) => empty( $v ) || (bool) \DateTime::createFromFormat( 'Y-m-d', $v ),
			],
			'orderby' => [
				'type'              => 'string',
				'default'           => 'created_at',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => fn( $v ) => in_array( $v, [ 'id', 'estimate_number', 'client_id', 'status', 'issue_date', 'expiry_date', 'total', 'created_at' ], true ),
			],
			'order' => [
				'type'              => 'string',
				'default'           => 'DESC',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => fn( $v ) => in_array( strtoupper( $v ), [ 'ASC', 'DESC' ], true ),
			],
		];
	}

	private function get_create_params(): array {
		return [
			'client_id' => [
				'type'              => 'integer',
				'required'          => true,
				'sanitize_callback' => 'absint',
				'validate_callback' => fn( $v ) => is_numeric( $v ) && absint( $v ) > 0,
			],
		];
	}

	private function get_update_params(): array {
		return [
			'id' => [
				'type'              => 'integer',
				'required'          => true,
				'validate_callback' => fn( $v ) => is_numeric( $v ) && absint( $v ) > 0,
				'sanitize_callback' => 'absint',
			],
			'version' => [
				'type'              => 'integer',
				'required'          => true,
				'validate_callback' => fn( $v ) => is_numeric( $v ) && absint( $v ) > 0,
				'sanitize_callback' => 'absint',
			],
		];
	}
}
