<?php
/**
 * @package Hafenstudios\Wellenbrecher
 */

namespace Hafenstudios\Wellenbrecher\Signals;

defined( 'ABSPATH' ) || exit;

/**
 * Eskalierender Proof-of-Work (Pro, Technik A3, ALTCHA-Muster). Der Token-
 * Endpoint spielt eine Challenge NUR fuer auffaellige IPs aus (Eskalation);
 * signals.js loest sie per WebCrypto (SHA-256-Brute-Force) und legt die Loesung
 * ins Feld wb_pow. Die PowRule prueft serverseitig gegen den auf den IP-HMAC
 * gesetzten Transient.
 *
 * Cache-fest: die Challenge kommt vom ungecachten Token-Endpoint, nichts
 * Dynamisches wird ins Seiten-HTML gebacken.
 */
class PowChallenge {

	/**
	 * Transient-Praefix (Schluessel = IP-HMAC, nie Klartext).
	 */
	public const TRANSIENT_PREFIX = 'wb_pow_';

	/**
	 * Lebensdauer der Challenge (grosszuegig genug fuer Ausfuellen + Absenden).
	 */
	public const TTL = 3600;

	public const DEFAULT_DIFFICULTY = 4;

	/**
	 * Spielt eine Challenge aus und haelt sie serverseitig als Transient auf dem
	 * IP-HMAC fest.
	 *
	 * @param string $ip_hmac    IP-HMAC des Besuchers.
	 * @param int    $difficulty Anzahl fuehrender Hex-Nullen (geklemmt 3..6).
	 * @return array{nonce: string, difficulty: int, signature: string}
	 */
	public function issue( string $ip_hmac, int $difficulty ): array {
		$difficulty = max( 3, min( 6, $difficulty ) );
		$nonce      = bin2hex( random_bytes( 16 ) );

		set_transient(
			self::TRANSIENT_PREFIX . $ip_hmac,
			array(
				'nonce'      => $nonce,
				'difficulty' => $difficulty,
			),
			self::TTL
		);

		return array(
			'nonce'      => $nonce,
			'difficulty' => $difficulty,
			// Signatur zur Manipulationsfestigkeit der Wire-Form (Technik A3);
			// die Pruefung selbst laeuft ueber den Transient (WB-Entscheidung).
			'signature'  => hash_hmac( 'sha256', $nonce . '|' . $difficulty, (string) wp_salt( 'nonce' ) ),
		);
	}

	/**
	 * Wurde dieser IP eine Challenge ausgespielt? Liefert die Challenge-Daten
	 * oder null.
	 *
	 * @param string $ip_hmac IP-HMAC.
	 * @return array{nonce: string, difficulty: int}|null
	 */
	public function pending( string $ip_hmac ): ?array {
		if ( '' === $ip_hmac ) {
			return null;
		}
		$stored = get_transient( self::TRANSIENT_PREFIX . $ip_hmac );
		if ( ! is_array( $stored ) || empty( $stored['nonce'] ) ) {
			return null;
		}
		return array(
			'nonce'      => (string) $stored['nonce'],
			'difficulty' => (int) ( $stored['difficulty'] ?? self::DEFAULT_DIFFICULTY ),
		);
	}

	/**
	 * Prueft eine Loesung: SHA-256(nonce . solution) muss mit difficulty
	 * fuehrenden Hex-Nullen beginnen. Rein und ohne WordPress-Abhaengigkeiten.
	 *
	 * @param string $nonce      Challenge-Nonce.
	 * @param int    $difficulty Anzahl fuehrender Hex-Nullen.
	 * @param string $solution   Eingereichte Loesung.
	 * @return bool
	 */
	public static function verify( string $nonce, int $difficulty, string $solution ): bool {
		if ( '' === $nonce || '' === $solution ) {
			return false;
		}
		$difficulty = max( 1, min( 8, $difficulty ) );
		$prefix     = str_repeat( '0', $difficulty );
		$hash       = hash( 'sha256', $nonce . $solution );
		return 0 === strncmp( $hash, $prefix, $difficulty );
	}
}
