<?php
/**
 * Pushing consenting customers to TextTango as contacts.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Contacts;

use TextTango\Api\Client;
use TextTango\Consent\Consent;
use TextTango\Consent\Recipient;
use TextTango\Consent\Suppressions;
use TextTango\Sending\Queue;
use TextTango\Support\Settings;
use WC_Order;
use WP_Error;

defined( 'ABSPATH' ) || exit;

/**
 * Keeps the mapped TextTango group in step with who has agreed to marketing
 * (spec §7).
 *
 * Consent decides whether the data leaves the site at all, not merely whether a
 * message is sent later. A customer who declines is never pushed, and one who
 * withdraws is removed from the group — so the platform list and the consent
 * record say the same thing, and declining actually stops the transfer.
 *
 * Every sync runs on the queue. A customer finishing checkout must not wait on
 * a contact API call, and a failure there must never affect their order.
 */
final class ContactSync {

	/**
	 * Action Scheduler hook that syncs one customer.
	 */
	public const HOOK = 'texttango_sync_contact';

	/**
	 * Register hooks.
	 */
	public function register(): void {
		add_action( self::HOOK, array( self::class, 'run' ), 10, 2 );

		// Consent is the trigger: granting pushes, withdrawing removes.
		add_action( 'texttango_consent_recorded', array( $this, 'on_user_consent' ), 10, 3 );
		add_action( 'texttango_order_consent_recorded', array( $this, 'on_order_consent' ), 10, 3 );
	}

	/**
	 * Whether syncing is switched on and pointed at a group.
	 */
	public static function is_enabled(): bool {
		return (bool) Settings::get( 'contacts_sync_enabled', false ) && '' !== self::group_id();
	}

	/**
	 * The TextTango group customers are synced into.
	 */
	public static function group_id(): string {
		return trim( (string) Settings::get( 'contacts_group_id', '' ) );
	}

	/**
	 * Queue a sync after a registered customer's preference changes.
	 *
	 * @param int    $user_id User id.
	 * @param bool   $granted Whether they agreed.
	 * @param string $source  Where it was captured.
	 */
	public function on_user_consent( $user_id, $granted, $source ): void {
		unset( $source );

		if ( ! self::is_enabled() ) {
			return;
		}

		self::enqueue( 'user', (int) $user_id );
	}

	/**
	 * Queue a sync after a guest order records a preference.
	 *
	 * @param WC_Order|mixed $order   Order.
	 * @param bool           $granted Whether they agreed.
	 * @param string         $source  Where it was captured.
	 */
	public function on_order_consent( $order, $granted, $source ): void {
		unset( $granted, $source );

		// A customer's order already fired the user hook; only guests need this.
		if ( ! self::is_enabled() || ! $order instanceof WC_Order || $order->get_customer_id() > 0 ) {
			return;
		}

		self::enqueue( 'order', $order->get_id() );
	}

	/**
	 * Schedule a sync.
	 *
	 * @param string $kind Either 'user' or 'order'.
	 * @param int    $id   User or order id.
	 */
	public static function enqueue( string $kind, int $id ): void {
		if ( $id <= 0 ) {
			return;
		}

		if ( ! Queue::is_available() ) {
			self::run( $kind, $id );

			return;
		}

		as_enqueue_async_action( self::HOOK, array( $kind, $id ), Queue::GROUP );
	}

	/**
	 * Sync one customer. Entry point for the queue.
	 *
	 * @param mixed $kind Either 'user' or 'order'.
	 * @param mixed $id   User or order id.
	 */
	public static function run( $kind, $id ): void {
		$kind = (string) $kind;
		$id   = (int) $id;

		if ( 'number' === $kind ) {
			$subject = self::from_number( $id );
		} elseif ( 'order' === $kind ) {
			$subject = self::from_order( $id );
		} else {
			$subject = self::from_user( $id );
		}

		if ( null === $subject ) {
			return;
		}

		if ( $subject['consented'] && ! Suppressions::contains( $subject['mobile'] ) ) {
			self::push( $kind, $id, $subject );

			return;
		}

		self::withdraw( $kind, $id, $subject );
	}

	/**
	 * Create or update the contact.
	 *
	 * @param string               $kind    Either 'user' or 'order'.
	 * @param int                  $id      User or order id.
	 * @param array<string, mixed> $subject Customer details.
	 */
	private static function push( string $kind, int $id, array $subject ): void {
		$client = Client::from_settings();

		if ( is_wp_error( $client ) ) {
			return;
		}

		$uuid = (string) $subject['uuid'];

		if ( '' !== $uuid ) {
			$client->update_contact( $uuid, self::attributes( $subject ) );

			return;
		}

		$created = $client->create_contact(
			array_merge(
				self::attributes( $subject ),
				array( 'contact_group_id' => self::group_id() )
			)
		);

		if ( ! is_wp_error( $created ) ) {
			self::remember( $kind, $id, ContactRepository::extract_uuid( $created ) );

			return;
		}

		/*
		 * The number is already in the group. Creating is refused rather than
		 * upserted, and the refusal does not name the contact it clashed with, so
		 * the id has to be searched for — otherwise this customer would be retried
		 * on every consent change for ever without the link being learned.
		 */
		if ( self::is_duplicate( $created ) ) {
			$found = $client->find_contact( (string) $subject['mobile'], self::group_id() );

			if ( ! is_wp_error( $found ) ) {
				self::remember( $kind, $id, ContactRepository::extract_uuid( $found ) );
			}
		}
	}

	/**
	 * Remove a contact whose consent has gone away.
	 *
	 * @param string               $kind    Either 'user' or 'order'.
	 * @param int                  $id      User or order id.
	 * @param array<string, mixed> $subject Customer details.
	 */
	private static function withdraw( string $kind, int $id, array $subject ): void {
		$uuid = (string) $subject['uuid'];

		if ( '' === $uuid ) {
			return;
		}

		$client = Client::from_settings();

		if ( is_wp_error( $client ) ) {
			return;
		}

		$client->delete_contact( $uuid );

		// Forgotten locally either way. A contact already gone from the platform
		// would otherwise be retried on every future change.
		self::remember( $kind, $id, '' );
	}

	/**
	 * Store, or clear, the contact UUID.
	 *
	 * @param string $kind One of 'user', 'order' or 'number'.
	 * @param int    $id   User id, order id, or consent list row id.
	 * @param string $uuid Contact UUID.
	 */
	private static function remember( string $kind, int $id, string $uuid ): void {
		if ( 'number' === $kind ) {
			ContactRepository::set_uuid_for_number( $id, $uuid );

			return;
		}

		if ( 'order' === $kind ) {
			$order = wc_get_order( $id );

			if ( $order instanceof WC_Order ) {
				ContactRepository::set_uuid_for_order( $order, $uuid );
			}

			return;
		}

		ContactRepository::set_uuid_for_user( $id, $uuid );
	}

	/**
	 * Whether a create failed because the number is already in the group.
	 *
	 * @param WP_Error $error Error from the client.
	 */
	public static function is_duplicate( WP_Error $error ): bool {
		$data = $error->get_error_data();
		$code = is_array( $data ) && ! empty( $data['api_code'] ) ? (string) $data['api_code'] : '';

		return 'DUPLICATE_CONTACT' === $code
			|| 409 === (int) ( is_array( $data ) ? ( $data['status'] ?? 0 ) : 0 );
	}

	/**
	 * The contact fields to send.
	 *
	 * @param array<string, mixed> $subject Customer details.
	 * @return array<string, mixed>
	 */
	public static function attributes( array $subject ): array {
		return array_filter(
			array(
				'mobile'     => (string) $subject['mobile'],
				'first_name' => (string) $subject['first_name'],
				'last_name'  => (string) $subject['last_name'],
				'email'      => (string) $subject['email'],
			),
			static fn( $value ): bool => '' !== $value
		);
	}

	/**
	 * A bare number from the consent list.
	 *
	 * There is no name and no email to send — the record is the number, which is
	 * all a plain subscriber list ever held. `consented` is unconditionally true
	 * because presence on that list *is* the consent; whether the number has
	 * since been suppressed is checked by the caller, as it is for everyone else.
	 *
	 * @param int $row_id Consent list row id.
	 * @return array<string, mixed>|null
	 */
	private static function from_number( int $row_id ): ?array {
		$row = ContactRepository::consent_row( $row_id );

		if ( null === $row || '' === (string) $row['recipient'] ) {
			return null;
		}

		return array(
			'mobile'     => (string) $row['recipient'],
			'first_name' => '',
			'last_name'  => '',
			'email'      => '',
			'consented'  => true,
			'uuid'       => (string) ( $row['contact_uuid'] ?? '' ),
		);
	}

	/**
	 * Gather a registered customer's details.
	 *
	 * @param int $user_id User id.
	 * @return array<string, mixed>|null
	 */
	private static function from_user( int $user_id ): ?array {
		$user = get_user_by( 'id', $user_id );

		if ( ! $user ) {
			return null;
		}

		$mobile = Recipient::for_user( $user_id );

		if ( '' === $mobile ) {
			return null;
		}

		return array(
			'mobile'     => $mobile,
			'first_name' => $user->first_name,
			'last_name'  => $user->last_name,
			'email'      => $user->user_email,
			'consented'  => Consent::user_has_consented( $user_id ),
			'uuid'       => ContactRepository::uuid_for_user( $user_id ),
		);
	}

	/**
	 * Gather a guest's details from the order they consented on.
	 *
	 * @param int $order_id Order id.
	 * @return array<string, mixed>|null
	 */
	private static function from_order( int $order_id ): ?array {
		$order = wc_get_order( $order_id );

		if ( ! $order instanceof WC_Order ) {
			return null;
		}

		$mobile = Recipient::for_order( $order );

		if ( '' === $mobile ) {
			return null;
		}

		return array(
			'mobile'     => $mobile,
			'first_name' => $order->get_billing_first_name(),
			'last_name'  => $order->get_billing_last_name(),
			'email'      => $order->get_billing_email(),
			'consented'  => Consent::STATE_GRANTED === Consent::state_for_order( $order ),
			'uuid'       => ContactRepository::uuid_for_order( $order ),
		);
	}
}
