<?php
/**
 * Click tracking token helper.
 *
 * @package Affilun\Application\Analytics
 */

declare(strict_types=1);

namespace Affilun\Application\Analytics;

/**
 * Issues and verifies lightweight HMAC tokens for public click tracking.
 */
final class ClickToken {

	/**
	 * Token version prefix.
	 */
	private const VERSION = 'v3';

	/**
	 * Default token lifetime in seconds.
	 */
	private const DEFAULT_TTL_SECONDS = 86400;

	/**
	 * Issue a token scoped to a saved product id, target URL, and optionally post id.
	 *
	 * @param int      $productId Persisted product id.
	 * @param int|null $issuedAt  Unix timestamp, or null for current time.
	 * @param string   $targetUrl Target URL rendered into the clicked button.
	 * @param int|null $postId    WordPress post id, or null when unavailable.
	 */
	public static function issue( int $productId, ?int $issuedAt = null, string $targetUrl = '', ?int $postId = null ): string {
		$issuedAt   = $issuedAt ?? time();
		$targetHash = self::targetHash( $targetUrl );
		$postId     = $postId !== null && $postId > 0 ? $postId : 0;
		$payload    = self::VERSION . ':' . $productId . ':' . $issuedAt . ':' . $targetHash . ':' . $postId;
		return $payload . ':' . self::signature( $payload );
	}

	/**
	 * Verify a token for the supplied product id, target URL, and post id.
	 *
	 * Tokens issued without a post id carry `0` and remain valid for any post.
	 *
	 * @param string   $token     Token candidate.
	 * @param int      $productId Persisted product id.
	 * @param string   $targetUrl Target URL reported by the click payload.
	 * @param int|null $postId    WordPress post id reported by the click payload.
	 * @param int|null $now       Unix timestamp, or null for current time.
	 */
	public static function verify( string $token, int $productId, string $targetUrl, ?int $postId = null, ?int $now = null ): bool {
		$parts = explode( ':', $token );
		if ( count( $parts ) !== 6 ) {
			return false;
		}
		if ( $parts[0] !== self::VERSION || $parts[1] !== (string) $productId ) {
			return false;
		}
		if ( preg_match( '/\A\d+\z/', $parts[2] ) !== 1 || preg_match( '/\A\d+\z/', $parts[4] ) !== 1 ) {
			return false;
		}
		if ( ! hash_equals( self::targetHash( $targetUrl ), $parts[3] ) ) {
			return false;
		}
		$tokenPostId = (int) $parts[4];
		if ( $tokenPostId > 0 && $tokenPostId !== (int) $postId ) {
			return false;
		}
		$issuedAt = (int) $parts[2];
		$now      = $now ?? time();
		if ( $issuedAt > $now + 300 || $issuedAt + self::DEFAULT_TTL_SECONDS < $now ) {
			return false;
		}
		$payload  = self::VERSION . ':' . $productId . ':' . $issuedAt . ':' . $parts[3] . ':' . $parts[4];
		$expected = self::signature( $payload );
		return hash_equals( $expected, $parts[5] );
	}

	/**
	 * Hash a target URL for compact token payloads.
	 *
	 * @param string $targetUrl Target URL.
	 */
	private static function targetHash( string $targetUrl ): string {
		return hash( 'sha256', trim( $targetUrl ) );
	}

	/**
	 * Compute the token HMAC.
	 *
	 * @param string $payload Token payload.
	 */
	private static function signature( string $payload ): string {
		return hash_hmac( 'sha256', $payload, self::secret() );
	}

	/**
	 * Resolve the best available WordPress secret material.
	 */
	private static function secret(): string {
		if ( function_exists( 'wp_salt' ) ) {
			$salt = (string) wp_salt( 'auth' );
			if ( $salt !== '' ) {
				return $salt;
			}
		}
		foreach ( array( 'AUTH_SALT', 'SECURE_AUTH_SALT', 'NONCE_SALT', 'LOGGED_IN_SALT' ) as $constant ) {
			if ( defined( $constant ) ) {
				$value = (string) constant( $constant );
				if ( $value !== '' ) {
					return $value;
				}
			}
		}
		return 'affilun-click-token-development-secret';
	}
}
