<?php
/**
 * Serial numbers over HTTP.
 *
 * @package WarrantyCart
 */

namespace WarrantyCart\REST\Controllers;

use WarrantyCart\Core\GuestAccess;
use WarrantyCart\Frontend\MyAccount;
use WarrantyCart\Core\Serials as SerialStore;
use WarrantyCart\REST\Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

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

/**
 * Registering a serial, and finding the cover that carries it.
 *
 * Two audiences, as with claims. A customer registers the number on the back of
 * the thing they bought; the shop looks a number up when somebody arrives
 * holding a broken appliance and no order number.
 */
class Serials extends Controller {

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

	/**
	 * Register the routes.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		/*
		 * The owner recording a serial against their own cover.
		 *
		 * Open to anyone for the same reason the guest claim routes are: a
		 * customer who checked out without registering has nothing to
		 * authenticate with, and proves the warranty is theirs with the order
		 * number and the address it was placed with. Ownership is checked
		 * below, not by the permission callback, because the answer depends on
		 * the body.
		 */
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'register_serial' ),
					'permission_callback' => '__return_true',
					'args'                => array(
						'warranty_id' => array(
							'required'          => true,
							'type'              => 'integer',
							'description'       => __( 'The warranty the item is covered by.', 'warranty-cart' ),
							'sanitize_callback' => 'absint',
						),
						'serial'      => array(
							'required'          => true,
							'type'              => 'string',
							'description'       => __( 'The serial number printed on the item.', 'warranty-cart' ),
							'sanitize_callback' => 'sanitize_text_field',
						),
						'email'       => array(
							'required'          => false,
							'type'              => 'string',
							'description'       => __( 'For a customer with no account, the address the order was placed with.', 'warranty-cart' ),
							'sanitize_callback' => 'sanitize_email',
						),
					),
				),
			)
		);

		// The shop looking one up.
		register_rest_route(
			$this->namespace,
			'/admin/' . $this->rest_base . '/(?P<serial>[A-Za-z0-9\-\/_. ]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'lookup' ),
					'permission_callback' => array( $this, 'check_admin_permission' ),
					'args'                => array(
						'serial' => array(
							'required'          => true,
							'type'              => 'string',
							'sanitize_callback' => 'sanitize_text_field',
						),
					),
				),
			)
		);
	}

	/**
	 * Record a serial against a warranty the caller owns.
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response
	 */
	public function register_serial( WP_REST_Request $request ): WP_REST_Response {
		global $wpdb;

		$warranty_id = (int) $request->get_param( 'warranty_id' );
		$email       = (string) $request->get_param( 'email' );

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

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
		$warranty = $wpdb->get_row(
			$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $warranty_id )
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery

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

		if ( ! $this->owns( $warranty, $email ) ) {
			return $this->error_response(
				__( 'That warranty belongs to a different account.', 'warranty-cart' ),
				403
			);
		}

		/*
		 * Refused where the shop has switched serials off.
		 *
		 * The form is not rendered then, so reaching here means a posted
		 * request — and accepting it would leave data in a column the admin has
		 * decided not to show anybody.
		 */
		if ( ! MyAccount::serials_enabled() ) {
			return $this->error_response(
				__( 'Serial numbers are not being recorded.', 'warranty-cart' ),
				403
			);
		}

		$serial = (string) $request->get_param( 'serial' );

		/*
		 * Warned about, not refused.
		 *
		 * A shop replacing a unit under warranty legitimately ends up with the
		 * old serial on a closed policy and the new one on its replacement, so
		 * refusing the second would block the ordinary case. Saying so lets the
		 * customer notice they have typed the number of something else.
		 */
		$duplicate = SerialStore::taken( $serial, $warranty_id );

		$stored = SerialStore::set( $warranty_id, $serial );

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

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

		return $this->success_response(
			array(
				'message'   => '' === $stored
					? __( 'Serial number removed.', 'warranty-cart' )
					: __( 'Serial number saved.', 'warranty-cart' ),
				'serial'    => $stored,
				'duplicate' => $duplicate,
			)
		);
	}

	/**
	 * Find the cover carrying a serial.
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response
	 */
	public function lookup( WP_REST_Request $request ): WP_REST_Response {
		$warranty = SerialStore::find( (string) $request->get_param( 'serial' ) );

		if ( ! $warranty ) {
			return $this->error_response(
				__( 'No warranty carries that serial number.', 'warranty-cart' ),
				404
			);
		}

		$product  = wc_get_product( (int) $warranty->product_id );
		$customer = $warranty->customer_id ? get_userdata( (int) $warranty->customer_id ) : false;

		return $this->success_response(
			array(
				'warranty' => array(
					'id'            => (int) $warranty->id,
					'serial_number' => (string) $warranty->serial_number,
					'order_id'      => (int) $warranty->order_id,
					'product_name'  => $product ? $product->get_name() : __( 'Warranty', 'warranty-cart' ),
					'customer_name' => $customer ? $customer->display_name : __( 'Guest', 'warranty-cart' ),
					'status'        => (string) $warranty->status,
					'purchase_date' => (string) $warranty->purchase_date,
					'expiration'    => (string) $warranty->expiration_date,
				),
			)
		);
	}

	/**
	 * Whether the caller may write to this warranty.
	 *
	 * @param object $warranty The warranty row.
	 * @param string $email    The address a guest gave, if any.
	 *
	 * @return bool
	 */
	private function owns( object $warranty, string $email ): bool {
		$viewer = get_current_user_id();

		if ( $viewer > 0 && (int) $warranty->customer_id === $viewer ) {
			return true;
		}

		// Staff record serials too — a phone-in registration has to be typed by
		// somebody, and today that somebody is the shop.
		if ( current_user_can( apply_filters( 'warranty_cart_rest_capability', 'manage_warranty_cart', null ) )
			|| current_user_can( 'manage_options' ) ) {
			return true;
		}

		return '' !== $email && GuestAccess::owns( $warranty, $email );
	}
}
