<?php
/**
 * Shared address CRUD over AJAX.
 *
 * @package Vanquish\MultipleAddresses\Frontend
 */

namespace Vanquish\MultipleAddresses\Frontend;

use Vanquish\MultipleAddresses\Com\Access;
use Vanquish\MultipleAddresses\Com\AddressForm;
use Vanquish\MultipleAddresses\Com\Addresses;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * The create/update/delete/set-default endpoints for a customer's address book.
 *
 * They live here rather than on a page class because both the My Account book
 * and the checkout selector use them: registering the same wp_ajax_ actions from
 * two page classes would run every handler twice.
 */
class AddressAjax {

	/**
	 * Nonce action shared with the frontend script.
	 */
	const NONCE = 'vanmuad_frontend';

	/**
	 * Address book service.
	 *
	 * @var Addresses
	 */
	private $addresses;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->addresses = new Addresses();

		add_action( 'wp_ajax_vanmuad_save_address', array( $this, 'save' ) );
		add_action( 'wp_ajax_vanmuad_delete_address', array( $this, 'delete' ) );
		add_action( 'wp_ajax_vanmuad_set_default_address', array( $this, 'set_default' ) );
	}

	/**
	 * Create or update an address.
	 *
	 * @return void
	 */
	public function save() {
		$this->verify();

		/*
		 * Sanitized here, at the door, and again per field further down.
		 *
		 * `sanitize_textarea_field` is the right one for this array: it strips
		 * tags and invalid UTF-8 exactly like `sanitize_text_field`, but keeps
		 * line breaks — which the delivery notes need and the single-line variant
		 * would silently flatten. AddressForm::validate() then applies the
		 * sanitizer each individual field actually calls for, so nothing relies on
		 * this one call being the right choice for every one of them.
		 *
		 * The cast covers a non-array being posted: validate() reads by field
		 * name, so anything that arrives under a key it does not know is dropped.
		 */
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify(), which runs above.
		$raw = isset( $_POST['fields'] ) ? map_deep( (array) wp_unslash( $_POST['fields'] ), 'sanitize_textarea_field' ) : array();

		$result = AddressForm::validate( $raw );

		if ( ! empty( $result['errors'] ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Please correct the highlighted fields.', 'vanquish-multiple-addresses-for-woocommerce' ),
					'errors'  => $result['errors'],
				),
				422
			);
		}

		$user_id    = $this->target_user_id();
		$address_id = $this->posted_address_id();

		// Only accept an id that really belongs to this user, so a forged id can
		// never make us write into someone else's book.
		if ( '' !== $address_id && ! $this->addresses->get( $user_id, $address_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Address not found.', 'vanquish-multiple-addresses-for-woocommerce' ) ), 404 );
		}

		/**
		 * Let premium veto or alter the save (e.g. a max-addresses cap).
		 *
		 * @param string|null $error      Error message, or null to allow.
		 * @param int         $user_id    User id.
		 * @param string      $address_id Address id ('' when creating).
		 */
		$veto = apply_filters( 'vanmuad_before_save_address', null, $user_id, $address_id );
		if ( $veto ) {
			wp_send_json_error( array( 'message' => $veto ), 403 );
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- verified in verify().
		$internal_name = isset( $_POST['internal_name'] ) ? sanitize_text_field( wp_unslash( $_POST['internal_name'] ) ) : '';
		$default_bill  = ! empty( $_POST['is_default_billing'] );
		$default_ship  = ! empty( $_POST['is_default_shipping'] );
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$saved_id = $this->addresses->save(
			$user_id,
			array(
				'id'                  => $address_id,
				'internal_name'       => $internal_name,
				'is_default_billing'  => $default_bill,
				'is_default_shipping' => $default_ship,
				'fields'              => $result['fields'],
			)
		);

		$saved = $this->addresses->get( $user_id, $saved_id );

		wp_send_json_success(
			array(
				'message'   => __( 'Address saved.', 'vanquish-multiple-addresses-for-woocommerce' ),
				'address'   => $saved,
				'label'     => self::label( $saved ),
				'formatted' => AddressForm::format( $saved['fields'] ),
			)
		);
	}

	/**
	 * Delete an address.
	 *
	 * @return void
	 */
	public function delete() {
		$this->verify();

		$user_id    = $this->target_user_id();
		$address_id = $this->posted_address_id();

		if ( ! $this->addresses->get( $user_id, $address_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Address not found.', 'vanquish-multiple-addresses-for-woocommerce' ) ), 404 );
		}

		$this->addresses->delete( $user_id, $address_id );

		wp_send_json_success( array( 'message' => __( 'Address deleted.', 'vanquish-multiple-addresses-for-woocommerce' ) ) );
	}

	/**
	 * Set (or clear) an address as the default for one purpose.
	 *
	 * @return void
	 */
	public function set_default() {
		$this->verify();

		$user_id    = $this->target_user_id();
		$address_id = $this->posted_address_id();

		if ( ! $this->addresses->get( $user_id, $address_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Address not found.', 'vanquish-multiple-addresses-for-woocommerce' ) ), 404 );
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- verified in verify().
		$purpose = isset( $_POST['purpose'] ) ? sanitize_key( wp_unslash( $_POST['purpose'] ) ) : '';
		$enable  = ! isset( $_POST['enable'] ) || ! empty( $_POST['enable'] );
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( ! in_array( $purpose, Addresses::purposes(), true ) ) {
			wp_send_json_error( array( 'message' => __( 'Unknown address purpose.', 'vanquish-multiple-addresses-for-woocommerce' ) ), 400 );
		}

		if ( $enable ) {
			$this->addresses->set_default( $user_id, $address_id, $purpose );
		} else {
			$this->addresses->unset_default( $user_id, $address_id, $purpose );
		}

		wp_send_json_success( array( 'message' => __( 'Default address updated.', 'vanquish-multiple-addresses-for-woocommerce' ) ) );
	}

	/**
	 * The dropdown label for an address: its name when set, otherwise a short
	 * one-line summary so the customer can still tell entries apart.
	 *
	 * Static on purpose: other classes need it too, and instantiating this class
	 * a second time would register the wp_ajax_ handlers twice (they would then
	 * run twice per request, saving or deleting twice).
	 *
	 * @param array $address Address record.
	 * @return string
	 */
	public static function label( array $address ) {
		if ( '' !== $address['internal_name'] ) {
			return $address['internal_name'];
		}

		$fields = $address['fields'];
		$parts  = array();

		foreach ( array( 'address_1', 'postcode', 'city' ) as $key ) {
			if ( ! empty( $fields[ $key ] ) ) {
				$parts[] = $fields[ $key ];
			}
		}

		return $parts ? implode( ', ', $parts ) : __( 'Address', 'vanquish-multiple-addresses-for-woocommerce' );
	}

	// -----------------------------------------------------------------------
	// Internals
	// -----------------------------------------------------------------------

	/**
	 * Whose address book these endpoints act on.
	 *
	 * The customer's own, unless something takes responsibility for saying
	 * otherwise — which is how the admin screens edit a customer's addresses
	 * without a second copy of this CRUD existing. Anything answering this filter
	 * owns the authorisation for the swap; nothing here assumes a posted id is
	 * allowed, because nothing here can know.
	 *
	 * @return int
	 */
	private function target_user_id() {
		/**
		 * Filter the user whose address book is being changed.
		 *
		 * @param int $user_id Defaults to the current user.
		 */
		return (int) apply_filters( 'vanmuad_ajax_user_id', get_current_user_id() );
	}

	/**
	 * The posted address id.
	 *
	 * @return string
	 */
	private function posted_address_id() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified in verify().
		return isset( $_POST['address_id'] ) ? sanitize_text_field( wp_unslash( $_POST['address_id'] ) ) : '';
	}

	/**
	 * Shared guard: logged-in user, valid nonce, and permission to change the
	 * address book.
	 *
	 * The permission check belongs here and not only in the interface: hiding a
	 * button stops nobody from posting to the endpoint directly, so a merchant's
	 * restriction has to be enforced where the change actually happens.
	 *
	 * @return void
	 */
	private function verify() {
		if ( ! is_user_logged_in() ) {
			wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'vanquish-multiple-addresses-for-woocommerce' ) ), 403 );
		}

		check_ajax_referer( self::NONCE, 'nonce' );

		if ( ! Access::can_manage() ) {
			wp_send_json_error( array( 'message' => __( 'You are not allowed to change your saved addresses.', 'vanquish-multiple-addresses-for-woocommerce' ) ), 403 );
		}
	}
}
