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

namespace Hafenstudios\Wellenbrecher\Rules;

use Hafenstudios\Wellenbrecher\Engine\Context;
use Hafenstudios\Wellenbrecher\Engine\RuleResult;
use Hafenstudios\Wellenbrecher\Signals\PowChallenge;
use Hafenstudios\Wellenbrecher\Support\IpHasher;

defined( 'ABSPATH' ) || exit;

/**
 * Proof-of-Work-Regel (Pro, Stufe 3, Technik A3). Feuert NUR, wenn dem
 * Besucher zuvor eine Challenge ausgespielt wurde (Transient auf dem IP-HMAC,
 * gesetzt vom Token-Endpoint bei Vorbelastung) UND die Loesung im Feld wb_pow
 * fehlt oder ungueltig ist: +25. Unauffaellige Besucher, denen keine Challenge
 * ausgespielt wurde, bekommen nie einen Treffer.
 *
 * Ohne WebCrypto (http-Kontext) kann der Browser nicht loesen; dann greift die
 * Regel wie bei jedem fehlenden JS-Signal nur als Score, nie als alleiniger
 * Block. In REST-/XML-RPC-Kontexten ist sie neutralisiert (skip_token_rules).
 */
class PowRule extends AbstractRule {

	public const ID = 'pow';

	public const DEFAULT_POINTS = 25;

	protected PowChallenge $challenge;

	protected IpHasher $hasher;

	/**
	 * @param PowChallenge $challenge Challenge-Dienst (Transient-Lookup + Pruefung).
	 * @param IpHasher     $hasher    IP-HMAC-Helfer.
	 * @param int          $points    Punkte.
	 */
	public function __construct( PowChallenge $challenge, IpHasher $hasher, int $points = self::DEFAULT_POINTS ) {
		parent::__construct( $points );
		$this->challenge = $challenge;
		$this->hasher    = $hasher;
	}

	public function id(): string {
		return self::ID;
	}

	public function stage(): int {
		return 3;
	}

	/**
	 * In REST-/XML-RPC-Kontexten neutralisiert (wie die Token-Regeln).
	 *
	 * @param Context $context Bewertungs-Kontext.
	 * @return bool
	 */
	public function applies( Context $context ): bool {
		return empty( $context->meta['skip_token_rules'] );
	}

	public function evaluate( Context $context ): RuleResult {
		$ip = $context->server( 'remote_addr' );
		if ( '' === $ip ) {
			return $this->miss();
		}

		$pending = $this->challenge->pending( $this->hasher->hash( $ip ) );
		if ( null === $pending ) {
			// Keine Challenge ausgespielt (unauffaellige IP) -> keine Strafe.
			return $this->miss();
		}

		$ok = PowChallenge::verify( $pending['nonce'], $pending['difficulty'], $context->signal( 'pow_solution' ) );
		return $ok ? $this->miss() : $this->hit();
	}
}
