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

namespace Hafenstudios\Kurato\Cockpit;

defined( 'ABSPATH' ) || exit;

/**
 * Server-zu-Server-Signatur (Dienst <-> Site) nach COCKPIT-API.md.
 *
 * Signaturbasis:
 *   method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
 * Signatur = hex( hmac_sha256( site_key, basis ) ).
 *
 * Pruefung: Timestamp-Fenster +-300 s, Nonce-Einmaligkeit (Transient, 10 min),
 * key_version, dann Signatur (hash_equals). Beide Richtungen nutzen dasselbe
 * Schema; compute() signiert auch ausgehende Calls der Site.
 *
 * `path` ist per CP1-Festlegung die REST-Route ohne /wp-json-Praefix und ohne
 * Query (z. B. /kurato-cockpit/v1/verify) - der Dienst (CP2) signiert identisch.
 */
class SignatureVerifier {

	private const SKEW      = 300;
	private const NONCE_TTL = 600;

	private Clock $clock;

	/**
	 * @param Clock $clock Zeit-Seam.
	 */
	public function __construct( Clock $clock ) {
		$this->clock = $clock;
	}

	/**
	 * Berechnet die Signatur (hex). Rein, in beide Richtungen genutzt.
	 *
	 * @param string $method    HTTP-Methode.
	 * @param string $path      REST-Route (siehe Klassenkommentar).
	 * @param string $timestamp Unix-Zeitstempel als String.
	 * @param string $nonce     Einmal-Wert.
	 * @param string $body      Roher Request-Body.
	 * @param string $site_key  Geheimnis.
	 * @return string
	 */
	public static function compute( string $method, string $path, string $timestamp, string $nonce, string $body, string $site_key ): string {
		$base = strtoupper( $method ) . "\n" . $path . "\n" . $timestamp . "\n" . $nonce . "\n" . hash( 'sha256', $body );
		return hash_hmac( 'sha256', $base, $site_key );
	}

	/**
	 * Prueft eine eingehende Server-Signatur.
	 *
	 * @param string                $method      HTTP-Methode.
	 * @param string                $path        REST-Route.
	 * @param string                $body        Roher Body.
	 * @param array<string, string> $headers     Keys: timestamp, nonce, key_version, signature.
	 * @param string                $site_key    Geheimnis.
	 * @param int                   $key_version Erwartete Schluessel-Version.
	 * @return true|\WP_Error
	 */
	public function verify( string $method, string $path, string $body, array $headers, string $site_key, int $key_version ) {
		$timestamp = (string) ( $headers['timestamp'] ?? '' );
		$nonce     = (string) ( $headers['nonce'] ?? '' );
		$kv        = (int) ( $headers['key_version'] ?? 0 );
		$signature = (string) ( $headers['signature'] ?? '' );

		if ( '' === $timestamp || '' === $nonce || '' === $signature || '' === $site_key ) {
			return $this->fail();
		}
		if ( $kv !== $key_version ) {
			return $this->fail();
		}

		$now = $this->clock->now();
		if ( abs( $now - (int) $timestamp ) > self::SKEW ) {
			return $this->fail();
		}

		// Nonce-Einmaligkeit (Replay-Schutz). Wird erst nach gueltiger Signatur
		// verbraucht, damit geratene Nonces mit falscher Signatur nichts verbrennen.
		$nonce_key = 'kurato_cockpit_nonce_' . hash( 'sha256', $nonce );
		if ( false !== get_transient( $nonce_key ) ) {
			return $this->fail();
		}

		$expected = self::compute( $method, $path, $timestamp, $nonce, $body, $site_key );
		if ( ! hash_equals( $expected, $signature ) ) {
			return $this->fail();
		}

		set_transient( $nonce_key, 1, self::NONCE_TTL );
		return true;
	}

	/**
	 * Generischer Signatur-Fehler.
	 *
	 * @return \WP_Error
	 */
	private function fail(): \WP_Error {
		return new \WP_Error(
			'invalid_signature',
			__( 'Die Anfrage konnte nicht verifiziert werden.', 'kurato' ),
			array( 'status' => 401 )
		);
	}
}
