<?php
/**
 * Missed-webhook self-heal.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\Webhook;

use TextTango\Api\Client;
use TextTango\Messages\MessageLog;
use TextTango\Sending\Queue;

defined( 'ABSPATH' ) || exit;

/**
 * Pulls the status of messages whose receipt never arrived (spec §19).
 *
 * Webhooks are best-effort. TextTango retries three times and then gives up, and
 * a site that was down, behind a firewall, or simply slow for those few minutes
 * loses the receipt permanently. Without this the message log would sit on
 * `sent` for ever and the merchant would have no idea whether the customer was
 * actually reached.
 *
 * Polling asks the API directly for anything left hanging, so delivery status
 * ends up correct whether or not the push ever landed.
 */
final class Reconciler {

	/**
	 * Action Scheduler hook for the recurring sweep.
	 */
	public const HOOK = 'texttango_reconcile_pending';

	/**
	 * How often the sweep runs, in seconds.
	 */
	public const INTERVAL = 900;

	/**
	 * How long a message may sit non-terminal before it is chased.
	 *
	 * Comfortably longer than TextTango's own retry schedule, so a receipt still
	 * in flight is not raced.
	 */
	public const GRACE_MINUTES = 15;

	/**
	 * Messages chased per sweep, so one run cannot exhaust the API's read limit.
	 */
	public const BATCH = 25;

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

	/**
	 * Make sure the recurring sweep exists.
	 */
	public static function ensure_scheduled(): void {
		if ( ! function_exists( 'as_has_scheduled_action' ) || ! function_exists( 'as_schedule_recurring_action' ) ) {
			return;
		}

		if ( as_has_scheduled_action( self::HOOK, array(), Queue::GROUP ) ) {
			return;
		}

		as_schedule_recurring_action( time() + self::INTERVAL, self::INTERVAL, self::HOOK, array(), Queue::GROUP );
	}

	/**
	 * Sweep the messages still waiting on a verdict.
	 *
	 * @param int $limit Maximum messages to check.
	 * @return array{checked: int, updated: int} What the sweep did.
	 */
	public static function run( int $limit = self::BATCH ): array {
		$rows = MessageLog::pending_reconciliation( self::GRACE_MINUTES, $limit );

		if ( empty( $rows ) ) {
			return array(
				'checked' => 0,
				'updated' => 0,
			);
		}

		$client = Client::from_settings();

		if ( is_wp_error( $client ) ) {
			return array(
				'checked' => 0,
				'updated' => 0,
			);
		}

		$checked = 0;
		$updated = 0;

		foreach ( $rows as $row ) {
			$uuid = (string) ( $row['texttango_message_uuid'] ?? '' );

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

			++$checked;

			$response = $client->message_status( $uuid );

			if ( is_wp_error( $response ) ) {
				// A read that fails now is simply chased again next sweep; nothing
				// about the message's state has changed.
				continue;
			}

			$result = Handler::apply( (int) $row['id'], $row, self::attributes( $response ) );

			if ( $result['handled'] ) {
				++$updated;
			}
		}

		return array(
			'checked' => $checked,
			'updated' => $updated,
		);
	}

	/**
	 * Flatten a v2 envelope into the shape the handler expects.
	 *
	 * A polled status and a pushed receipt then travel the same code path, so the
	 * progression and idempotency rules apply identically to both.
	 *
	 * @param array<string, mixed> $response Decoded API response.
	 * @return array<string, mixed>
	 */
	public static function attributes( array $response ): array {
		$attributes = $response['data']['attributes'] ?? $response['data'] ?? $response;

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