<?php
/**
 * Customer address book — storage and CRUD (native user meta, no ACF).
 *
 * @package Vanquish\MultipleAddresses\Com
 */

namespace Vanquish\MultipleAddresses\Com;

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

/**
 * Stores each customer's saved addresses in a single user meta entry (an array
 * keyed by a per-user incremental id). This is the free core: every read and
 * write in the plugin goes through here, so the storage format lives in exactly
 * one place.
 *
 * An address has NO billing/shipping type. It carries two independent "use this
 * one by default" flags instead, and the same address may hold both — which is
 * the common case, since most people bill and ship to the same place. Typing the
 * address itself would force them to save it twice, and nothing else was gained
 * by it: what tells two addresses apart is their name, not a hidden flag.
 *
 * Stored shape (user meta `vanmuad_addresses`):
 *   [
 *     '3' => [
 *       'id'                  => '3',
 *       'internal_name'       => 'Home',
 *       'is_default_billing'  => true,
 *       'is_default_shipping' => true,
 *       'fields'              => [ 'first_name' => '…', 'address_1' => '…', … ],
 *     ],
 *     …
 *   ]
 */
class Addresses {

	/**
	 * User meta key holding the address book.
	 */
	const META_KEY = 'vanmuad_addresses';

	/**
	 * User meta key holding the last assigned numeric id (monotonic per user).
	 */
	const SEQ_KEY = 'vanmuad_addresses_seq';

	/**
	 * The two purposes an address can be the default for.
	 *
	 * @return string[]
	 */
	public static function purposes() {
		return array( 'billing', 'shipping' );
	}

	/**
	 * The meta flag that marks an address as the default for a purpose.
	 *
	 * @param string $purpose 'billing' or 'shipping'.
	 * @return string
	 */
	public static function default_flag( $purpose ) {
		return 'shipping' === $purpose ? 'is_default_shipping' : 'is_default_billing';
	}

	/**
	 * The fields an address holds. Kept explicit so storage, the frontend form
	 * and the CSV import/export all share one source of truth — extending this
	 * list is enough to make a new field persist, import and export.
	 *
	 * @return string[]
	 */
	public static function field_names() {
		/**
		 * Filter the fields an address is made of.
		 *
		 * @param string[] $fields Field names.
		 */
		return apply_filters(
			'vanmuad_address_field_names',
			array(
				'first_name',
				'last_name',
				'company',
				'country',
				'address_1',
				'address_2',
				'city',
				'state',
				'postcode',
				'phone',
				'email',
			)
		);
	}

	/**
	 * All addresses for a user.
	 *
	 * This is the single point where an address book is read, which is why the
	 * conversion of a legacy book hangs off it: every surface — frontend, admin,
	 * AJAX, premium modules, anything added later — passes through here, so none
	 * of them has to remember to trigger it.
	 *
	 * @param int $user_id User id.
	 * @return array[] Address records keyed by id.
	 */
	public function all( $user_id ) {
		Migration::maybe_run( $user_id );

		$stored = get_user_meta( $user_id, self::META_KEY, true );

		return is_array( $stored ) ? $stored : array();
	}

	/**
	 * A single address by id.
	 *
	 * @param int    $user_id    User id.
	 * @param string $address_id Address id.
	 * @return array|null
	 */
	public function get( $user_id, $address_id ) {
		$all = $this->all( $user_id );

		return isset( $all[ $address_id ] ) ? $all[ $address_id ] : null;
	}

	/**
	 * The address flagged as the default for a purpose (or null when none is).
	 *
	 * @param int    $user_id User id.
	 * @param string $purpose 'billing' or 'shipping'.
	 * @return array|null
	 */
	public function get_default( $user_id, $purpose ) {
		$flag = self::default_flag( $purpose );

		foreach ( $this->all( $user_id ) as $address ) {
			if ( ! empty( $address[ $flag ] ) ) {
				return $address;
			}
		}

		return null;
	}

	/**
	 * Create or update an address.
	 *
	 * @param int   $user_id User id.
	 * @param array $data    Address data: 'id' (optional, update when present),
	 *                       'internal_name', 'is_default_billing',
	 *                       'is_default_shipping', 'fields'.
	 * @return string The saved address id.
	 */
	public function save( $user_id, array $data ) {
		$all = $this->all( $user_id );

		// Update an existing record, or mint a new id.
		if ( ! empty( $data['id'] ) && isset( $all[ $data['id'] ] ) ) {
			$id = (string) $data['id'];
		} else {
			$id = $this->next_id( $user_id );
		}

		$record = array(
			'id'                  => $id,
			'internal_name'       => isset( $data['internal_name'] ) ? sanitize_text_field( $data['internal_name'] ) : '',
			'is_default_billing'  => ! empty( $data['is_default_billing'] ),
			'is_default_shipping' => ! empty( $data['is_default_shipping'] ),
			'fields'              => $this->sanitize_fields( isset( $data['fields'] ) ? (array) $data['fields'] : array() ),
		);

		$all[ $id ] = $record;

		// Only one default per purpose.
		foreach ( self::purposes() as $purpose ) {
			if ( ! empty( $record[ self::default_flag( $purpose ) ] ) ) {
				$all = $this->clear_other_defaults( $all, $purpose, $id );
			}
		}

		$this->persist( $user_id, $all );

		return $id;
	}

	/**
	 * Delete an address.
	 *
	 * @param int    $user_id    User id.
	 * @param string $address_id Address id.
	 * @return bool Whether something was removed.
	 */
	public function delete( $user_id, $address_id ) {
		$all = $this->all( $user_id );

		if ( ! isset( $all[ $address_id ] ) ) {
			return false;
		}

		unset( $all[ $address_id ] );
		$this->persist( $user_id, $all );

		return true;
	}

	/**
	 * Flag an address as the default for a purpose.
	 *
	 * @param int    $user_id    User id.
	 * @param string $address_id Address id.
	 * @param string $purpose    'billing' or 'shipping'.
	 * @return bool
	 */
	public function set_default( $user_id, $address_id, $purpose ) {
		$all = $this->all( $user_id );

		if ( ! isset( $all[ $address_id ] ) ) {
			return false;
		}

		$flag                        = self::default_flag( $purpose );
		$all[ $address_id ][ $flag ] = true;
		$all                         = $this->clear_other_defaults( $all, $purpose, $address_id );

		$this->persist( $user_id, $all );

		return true;
	}

	/**
	 * Remove the default flag for a purpose from an address.
	 *
	 * @param int    $user_id    User id.
	 * @param string $address_id Address id.
	 * @param string $purpose    'billing' or 'shipping'.
	 * @return bool
	 */
	public function unset_default( $user_id, $address_id, $purpose ) {
		$all = $this->all( $user_id );

		if ( ! isset( $all[ $address_id ] ) ) {
			return false;
		}

		$all[ $address_id ][ self::default_flag( $purpose ) ] = false;
		$this->persist( $user_id, $all );

		return true;
	}

	/**
	 * How many addresses a user has.
	 *
	 * @param int $user_id User id.
	 * @return int
	 */
	public function count( $user_id ) {
		return count( $this->all( $user_id ) );
	}

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

	/**
	 * Sanitize the WooCommerce-style fields, dropping anything unknown.
	 *
	 * @param array $fields Raw fields.
	 * @return array
	 */
	private function sanitize_fields( array $fields ) {
		$clean = array();

		foreach ( self::field_names() as $name ) {
			if ( ! isset( $fields[ $name ] ) ) {
				continue;
			}

			$value = $fields[ $name ];

			$sanitized = 'email' === $name ? sanitize_email( $value ) : sanitize_text_field( $value );

			/**
			 * Filter a sanitized address field.
			 *
			 * Lets a field added through `vanmuad_address_field_names` be cleaned
			 * differently — a multi-line note, for instance, which the default
			 * single-line sanitizer would flatten.
			 *
			 * @param string $sanitized The sanitized value.
			 * @param string $name      Field name.
			 * @param mixed  $value     The raw value.
			 */
			$clean[ $name ] = apply_filters( 'vanmuad_sanitize_address_field', $sanitized, $name, $value );
		}

		return $clean;
	}

	/**
	 * Turn off a purpose's default flag on every other address.
	 *
	 * Both ids are cast to string before comparing: PHP turns numeric array keys
	 * into integers, while $keep_id arrives as a string (from the request, or from
	 * next_id()). A strict comparison of 3 against '3' is true, so the loop would
	 * clear the flag on the very address being made default.
	 *
	 * @param array  $all     All addresses.
	 * @param string $purpose 'billing' or 'shipping'.
	 * @param string $keep_id Address id to keep as default.
	 * @return array
	 */
	private function clear_other_defaults( array $all, $purpose, $keep_id ) {
		$flag = self::default_flag( $purpose );

		foreach ( $all as $id => $address ) {
			if ( (string) $id !== (string) $keep_id ) {
				$all[ $id ][ $flag ] = false;
			}
		}

		return $all;
	}

	/**
	 * Next per-user address id (never reused, so links stay stable).
	 *
	 * @param int $user_id User id.
	 * @return string
	 */
	private function next_id( $user_id ) {
		$seq = (int) get_user_meta( $user_id, self::SEQ_KEY, true );
		$seq++;
		update_user_meta( $user_id, self::SEQ_KEY, $seq );

		return (string) $seq;
	}

	/**
	 * Write the address book back to user meta.
	 *
	 * @param int   $user_id User id.
	 * @param array $all     All addresses.
	 * @return void
	 */
	private function persist( $user_id, array $all ) {
		update_user_meta( $user_id, self::META_KEY, $all );
	}
}
