<?php
/**
 * Variant Telemetry Reporter.
 *
 * Sends an anonymous {site_url, variant} snapshot to the aipt-sys backend so
 * the legacy ↔ next rollout can be observed in aggregate. No authentication,
 * no metrics, no version metadata — the minimum viable signal.
 *
 *  - reportIfDue()               — fires only when the install's current variant
 *                                  differs from the last successfully reported
 *                                  variant. Sends once per state change.
 *  - reportVariantChanged($uid)  — always fires; updates the marker on success.
 *
 * Best-effort by contract: failures NEVER bubble out and never poison the
 * marker (a failed dispatch retries on the next call instead of locking the
 * site out).
 *
 * @package AIPT\Core\Telemetry
 * @since   2.6.2
 */

declare(strict_types=1);

namespace AIPT\Core\Telemetry;

use AIPT\Core\Services\UiVariantService;

defined( 'ABSPATH' ) || exit;

/**
 * UI variant rollout telemetry reporter.
 *
 * Static helpers only — instantiation deliberately disabled.
 *
 * @since 2.6.2
 */
final class VariantTelemetryReporter {

	/**
	 * Persistent marker storing the LAST variant value that successfully
	 * reached the backend. reportIfDue() compares the install's current
	 * variant to this marker — if they match, no HTTP is fired. This is
	 * the "send once per state change" behavior the product wants:
	 *  - First admin page load on a fresh install     → fires (marker empty)
	 *  - F5 with no change                            → no fire (marker matches)
	 *  - User flips variant in the UI                 → fires (marker mismatches)
	 *  - F5 right after a flip                        → no fire (marker now matches)
	 *
	 * autoload=no — only loaded from the DB on admin_init's reportIfDue()
	 * call, never on front-end requests.
	 *
	 * @since 2.6.2
	 */
	private const LAST_REPORTED_OPTION = 'aipt_variant_telemetry_last_reported';

	/**
	 * Short failure backoff. Without this, a downed/blocked backend would
	 * trigger a synchronous 5s wp_remote_post() timeout on EVERY admin page
	 * load because the marker stays unchanged on failure and reportIfDue()
	 * runs on every admin_init. The backoff caps the retry rate at most
	 * once per FAILURE_BACKOFF_TTL during an outage so admin pages stay
	 * snappy even when api.aiforwoo.com is unreachable.
	 *
	 * Stored as a transient (auto-expires) rather than an option so a
	 * recovered backend doesn't require manual cleanup.
	 *
	 * @since 2.6.2
	 */
	private const FAILURE_BACKOFF_TRANSIENT = 'aipt_variant_telemetry_failure_backoff';

	/**
	 * Failure backoff TTL — 15 minutes. Short enough to recover quickly
	 * after a transient outage, long enough to avoid hammering during a
	 * sustained one (4 attempts/hour worst case).
	 *
	 * @since 2.6.2
	 */
	private const FAILURE_BACKOFF_TTL = 900;

	/**
	 * Backend route slug. Resolves to
	 * https://api.aiforwoo.com/api/v2/variant-telemetry via VariantTelemetryClient.
	 *
	 * @since 2.6.2
	 */
	private const REMOTE_ENDPOINT = 'variant-telemetry';

	/**
	 * Daily/admin_init entry point. Fires ONLY when the install's current
	 * resolved variant differs from the last successfully reported value.
	 *
	 * @since 2.6.2
	 */
	public static function reportIfDue(): void {
		if ( self::is_backing_off() ) {
			return; 
		}

		$service = new UiVariantService();
		$current = self::resolve_variant( $service, null );
		$last    = (string) get_option( self::LAST_REPORTED_OPTION, '' );

		if ( $current === $last ) {
			return; 
		}

		self::dispatch( $service, $current );
	}

	/**
	 * Variant-change entry point. Fires regardless of the marker because
	 * a flip is explicit user intent worth confirming end-to-end. Still
	 * honors the failure backoff so a downed backend can't make the
	 * controller endpoint hang on every modal click.
	 *
	 * @since 2.6.2
	 *
	 * @param int|null $user_id User whose preference flipped. Falls back to
	 *                          get_current_user_id() when null.
	 * @return void
	 */
	public static function reportVariantChanged( ?int $user_id = null ): void {
		if ( self::is_backing_off() ) {
			return; 
		}

		$service = new UiVariantService();
		$current = self::resolve_variant( $service, $user_id );

		self::dispatch( $service, $current );
	}

	/**
	 * Whether a recent dispatch failure is still cooling down.
	 *
	 * @since 2.6.2
	 */
	private static function is_backing_off(): bool {
		return (bool) get_transient( self::FAILURE_BACKOFF_TRANSIENT );
	}

	/**
	 * Build payload, ship it, persist the marker on success only.
	 * Failed dispatches leave the marker unchanged so the next ping retries.
	 *
	 * @since 2.6.2
	 *
	 * @param UiVariantService $service Resolver source (kept for symmetry).
	 * @param string           $variant Already-resolved variant value to ship.
	 */
	private static function dispatch( UiVariantService $service, string $variant ): void {
		unset( $service ); 

		$payload = array(
			'site_url' => get_site_url(),
			'variant'  => $variant,
		);

		try {
			$client   = new VariantTelemetryClient();
			$response = $client->send( self::REMOTE_ENDPOINT, $payload );
		} catch ( \Throwable $e ) {

			self::mark_failure();
			return;
		}

		if ( ! is_array( $response ) || empty( $response['success'] ) ) {
			self::mark_failure();
			return;
		}

		update_option( self::LAST_REPORTED_OPTION, $variant, false );
	}

	/**
	 * Stamp the failure backoff transient so subsequent reportIfDue() and
	 * reportVariantChanged() calls short-circuit until the TTL expires.
	 * The stored value (current timestamp) is unused — presence alone gates.
	 *
	 * @since 2.6.2
	 */
	private static function mark_failure(): void {
		set_transient( self::FAILURE_BACKOFF_TRANSIENT, time(), self::FAILURE_BACKOFF_TTL );
	}

	/**
	 * Resolve the install's current variant for the given user (or the
	 * current user when $user_id is null).
	 *
	 * @since 2.6.2
	 */
	private static function resolve_variant( UiVariantService $service, ?int $user_id ): string {
		$user_id = (int) ( $user_id ?? get_current_user_id() );

		return $service->resolveForUser( max( 0, $user_id ) );
	}
}
