<?php
/**
 * Conversion of address books saved by the legacy plugin.
 *
 * @package Vanquish\MultipleAddresses\Com
 */

namespace Vanquish\MultipleAddresses\Com;

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

/**
 * Converts a customer's address book from the format used by the previous
 * plugin ("WooCommerce Multiple Customer Addresses & Shipping") to this one.
 *
 * It runs lazily, per customer, from Addresses::all() — the single point where
 * the plugin reads an address book. Hooking a list of "trigger pages" instead
 * would silently miss an AJAX endpoint, a REST call or a premium module; going
 * through the one read path covers every surface, present and future. A bulk
 * migration on activation was rejected for the opposite reason: a shop with tens
 * of thousands of customers would time out.
 *
 * The legacy data is never deleted. It costs a few bytes, it lets the conversion
 * be repeated if anything turns out wrong in production, and it means an
 * unhappy merchant can always go back.
 *
 * Legacy shape (user meta `_wcmca_additional_addresses`), a plain list of:
 *   [
 *     'type'                       => 'billing'|'shipping',
 *     'address_id'                 => uniqid(),
 *     'address_internal_name'      => 'Home',
 *     '<type>_first_name'          => '…',   // fields prefixed by the type
 *     '<type>_address_1'           => '…',
 *     '<type>_is_default_address'  => 1|0,
 *   ]
 */
class Migration {

	/**
	 * User meta written by the legacy plugin.
	 */
	const LEGACY_KEY = '_wcmca_additional_addresses';

	/**
	 * User meta flag marking a customer as converted.
	 */
	const FLAG_KEY = 'vanmuad_migrated_legacy';

	/**
	 * Customers already handled in this request.
	 *
	 * @var array<int,bool>
	 */
	private static $done = array();

	/**
	 * Convert a customer's address book if it has not been converted yet.
	 *
	 * @param int $user_id User id.
	 * @return void
	 */
	public static function maybe_run( $user_id ) {
		$user_id = (int) $user_id;

		if ( $user_id <= 0 || isset( self::$done[ $user_id ] ) ) {
			return;
		}

		self::$done[ $user_id ] = true;

		if ( get_user_meta( $user_id, self::FLAG_KEY, true ) ) {
			return;
		}

		$legacy = get_user_meta( $user_id, self::LEGACY_KEY, true );

		// Nothing to convert. The flag is still written, so this customer never
		// pays for the lookup again.
		if ( ! is_array( $legacy ) || empty( $legacy ) ) {
			update_user_meta( $user_id, self::FLAG_KEY, 1 );
			return;
		}

		self::convert( $user_id, $legacy );

		update_user_meta( $user_id, self::FLAG_KEY, 1 );
	}

	/**
	 * Force a conversion again, ignoring the flag. Useful for support.
	 *
	 * @param int $user_id User id.
	 * @return void
	 */
	public static function force( $user_id ) {
		unset( self::$done[ (int) $user_id ] );
		delete_user_meta( $user_id, self::FLAG_KEY );

		self::maybe_run( $user_id );
	}

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

	/**
	 * Convert the legacy records and merge them into the customer's book.
	 *
	 * @param int   $user_id User id.
	 * @param array $legacy  Legacy records.
	 * @return void
	 */
	private static function convert( $user_id, array $legacy ) {
		$existing = get_user_meta( $user_id, Addresses::META_KEY, true );
		$existing = is_array( $existing ) ? $existing : array();

		// Signatures of what the customer already has, so re-running the
		// conversion cannot produce a second copy of the same address.
		$seen = array();
		foreach ( $existing as $id => $address ) {
			$seen[ self::signature( isset( $address['fields'] ) ? (array) $address['fields'] : array() ) ] = (string) $id;
		}

		$seq = (int) get_user_meta( $user_id, Addresses::SEQ_KEY, true );

		foreach ( $legacy as $record ) {
			if ( ! is_array( $record ) || empty( $record['type'] ) ) {
				continue;
			}

			$type   = 'shipping' === $record['type'] ? 'shipping' : 'billing';
			$fields = self::extract_fields( $record, $type );

			// A record with nothing in it is not worth carrying over.
			if ( '' === trim( implode( '', $fields ) ) ) {
				continue;
			}

			$is_default = ! empty( $record[ $type . '_is_default_address' ] );
			$flag       = Addresses::default_flag( $type );
			$signature  = self::signature( $fields );

			// The same address saved once for billing and once for shipping — the
			// duplication this plugin's model exists to remove — becomes a single
			// entry carrying both flags. Customers migrating arrive with the
			// problem already fixed rather than carried over.
			if ( isset( $seen[ $signature ] ) ) {
				$id = $seen[ $signature ];

				if ( $is_default ) {
					$existing[ $id ][ $flag ] = true;
				}

				// Keep whichever name is actually filled in.
				if ( '' === $existing[ $id ]['internal_name'] ) {
					$existing[ $id ]['internal_name'] = self::name( $record );
				}

				continue;
			}

			$seq++;
			$id = (string) $seq;

			$existing[ $id ] = array(
				'id'                  => $id,
				'internal_name'       => self::name( $record ),
				'is_default_billing'  => 'billing' === $type && $is_default,
				'is_default_shipping' => 'shipping' === $type && $is_default,
				'fields'              => $fields,
			);

			$seen[ $signature ] = $id;
		}

		// Only one address may be the default for each purpose; legacy data can
		// disagree if it was edited outside the old plugin's UI.
		foreach ( Addresses::purposes() as $purpose ) {
			$existing = self::keep_one_default( $existing, Addresses::default_flag( $purpose ) );
		}

		update_user_meta( $user_id, Addresses::META_KEY, $existing );
		update_user_meta( $user_id, Addresses::SEQ_KEY, $seq );
	}

	/**
	 * Pull the address fields out of a legacy record, dropping the type prefix.
	 *
	 * @param array  $record Legacy record.
	 * @param string $type   'billing' or 'shipping'.
	 * @return array Fields keyed by plain name.
	 */
	private static function extract_fields( array $record, $type ) {
		$fields = array();

		foreach ( Addresses::field_names() as $name ) {
			$key = $type . '_' . $name;

			if ( ! isset( $record[ $key ] ) ) {
				continue;
			}

			$value = $record[ $key ];

			if ( ! is_scalar( $value ) ) {
				continue;
			}

			$fields[ $name ] = 'email' === $name
				? sanitize_email( (string) $value )
				: sanitize_text_field( (string) $value );
		}

		return $fields;
	}

	/**
	 * The customer's name for a legacy record.
	 *
	 * @param array $record Legacy record.
	 * @return string
	 */
	private static function name( array $record ) {
		return isset( $record['address_internal_name'] )
			? sanitize_text_field( (string) $record['address_internal_name'] )
			: '';
	}

	/**
	 * A comparable fingerprint of an address.
	 *
	 * The customer's own name for the address is deliberately excluded: the same
	 * place saved twice is often labelled differently ("Home" and "Casa"), and
	 * those should still merge.
	 *
	 * @param array $fields Address fields.
	 * @return string
	 */
	private static function signature( array $fields ) {
		$normalized = array();

		foreach ( Addresses::field_names() as $name ) {
			$value              = isset( $fields[ $name ] ) ? (string) $fields[ $name ] : '';
			$normalized[ $name ] = strtolower( trim( preg_replace( '/\s+/', ' ', $value ) ) );
		}

		return md5( (string) wp_json_encode( $normalized ) );
	}

	/**
	 * Leave the flag on at most one address.
	 *
	 * @param array  $addresses Address book.
	 * @param string $flag      Flag key.
	 * @return array
	 */
	private static function keep_one_default( array $addresses, $flag ) {
		$found = false;

		foreach ( $addresses as $id => $address ) {
			if ( empty( $address[ $flag ] ) ) {
				continue;
			}

			if ( $found ) {
				$addresses[ $id ][ $flag ] = false;
				continue;
			}

			$found = true;
		}

		return $addresses;
	}
}
