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

namespace Hafenstudios\Kurato\Mail;

defined( 'ABSPATH' ) || exit;

/**
 * Signierter Token fuer Abmelde- und Praeferenz-Links (K4c, K4d).
 *
 * Muster wie Leadlotse `DoiService`: `base64url( payload ) . "." . HMAC`, mit
 * `wp_salt`-signiertem HMAC. Die Nutzlast ist `contact_id . "|" . list_id`
 * (list_id = 0 bedeutet "alle Listen").
 *
 * **Zweckbindung** (K4d-2): Der HMAC-Praefix haengt vom Verwendungszweck ab.
 * Ein Abmelde-Token (`unsub`) und ein Preference-Center-Token (`pref`) tragen
 * dieselbe Nutzlast, aber unterschiedliche Signaturen. Dadurch geht ein
 * Preference-Token NICHT am Abmelde-Endpoint als One-Click-Abmeldung durch und
 * umgekehrt. Der Default-Zweck ist `unsub`, damit bereits versendete Abmelde-
 * Token (Praefix `kurato_unsub`, ohne Zweckangabe erzeugt) unveraendert gueltig
 * bleiben (Rueckwaertskompatibilitaet).
 *
 * Anders als der DOI-Token hat dieser Token bewusst KEINEN Ablauf: Abmeldelinks
 * in bereits versendeten Newslettern muessen dauerhaft gueltig bleiben, sonst
 * kann sich ein Empfaenger aus einer alten Mail nicht mehr abmelden. Die Absicht
 * schuetzt allein die HMAC-Signatur; der Token traegt keine Rechte ausser der
 * Abmeldung/Praeferenz-Ansicht des genannten Kontakts.
 */
final class LinkToken {

	/**
	 * Zweck: One-Click-Abmeldung (Default, rueckwaertskompatibel).
	 */
	public const PURPOSE_UNSUBSCRIBE = 'unsub';

	/**
	 * Zweck: Preference-Center-Ansicht/-Aktionen.
	 */
	public const PURPOSE_PREFERENCES = 'pref';

	/**
	 * HMAC-Praefix je Zweck (Domain-Trennung gegen andere Token und
	 * gegeneinander). Der `unsub`-Praefix bleibt exakt der historische Wert,
	 * damit alte Abmelde-Token gueltig bleiben.
	 *
	 * @var array<string, string>
	 */
	private const PREFIXES = array(
		self::PURPOSE_UNSUBSCRIBE => 'kurato_unsub',
		self::PURPOSE_PREFERENCES => 'kurato_pref',
	);

	/**
	 * Erzeugt einen signierten Token.
	 *
	 * @param int    $contact_id Kontakt-ID.
	 * @param int    $list_id    Listen-ID, oder 0 fuer "alle Listen".
	 * @param string $purpose    Zweck (PURPOSE_UNSUBSCRIBE | PURPOSE_PREFERENCES).
	 * @return string
	 */
	public function create( int $contact_id, int $list_id = 0, string $purpose = self::PURPOSE_UNSUBSCRIBE ): string {
		$contact_id = max( 0, $contact_id );
		$list_id    = max( 0, $list_id );
		$payload    = $contact_id . '|' . $list_id;
		$sig        = $this->sign( $payload, $purpose );

		return rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' ) . '.' . $sig;
	}

	/**
	 * Prueft einen Token und liefert Kontakt- und Listen-ID. Der Token muss zum
	 * erwarteten Zweck passen (Signatur-Praefix); sonst ist er ungueltig.
	 *
	 * @param string $token   Token aus dem Abmelde-/Praeferenz-Link.
	 * @param string $purpose Erwarteter Zweck (Default: Abmeldung).
	 * @return array{contact_id:int, list_id:int}|\WP_Error
	 */
	public function verify( string $token, string $purpose = self::PURPOSE_UNSUBSCRIBE ) {
		$invalid = new \WP_Error(
			'kurato_invalid_token',
			__( 'Der Link ist ungueltig. Bitte nutzen Sie den Link aus einer aktuellen Mail oder wenden Sie sich an uns.', 'kurato' ),
			array( 'status' => 400 )
		);

		$parts = explode( '.', $token, 2 );
		if ( 2 !== count( $parts ) ) {
			return $invalid;
		}

		list( $encoded, $sig ) = $parts;
		$payload               = base64_decode( strtr( $encoded, '-_', '+/' ), true );
		if ( false === $payload || ! hash_equals( $this->sign( $payload, $purpose ), $sig ) ) {
			return $invalid;
		}

		$segments = explode( '|', $payload );
		if ( 2 !== count( $segments ) ) {
			return $invalid;
		}

		list( $contact_id, $list_id ) = array_map( 'intval', $segments );
		if ( $contact_id <= 0 || $list_id < 0 ) {
			return $invalid;
		}

		return array(
			'contact_id' => $contact_id,
			'list_id'    => $list_id,
		);
	}

	/**
	 * HMAC-Signatur ueber die Nutzlast, gebunden an den Zweck.
	 *
	 * @param string $payload Nutzdaten.
	 * @param string $purpose Zweck (unbekannt faellt auf Abmeldung zurueck).
	 * @return string
	 */
	private function sign( string $payload, string $purpose = self::PURPOSE_UNSUBSCRIBE ): string {
		$prefix = self::PREFIXES[ $purpose ] ?? self::PREFIXES[ self::PURPOSE_UNSUBSCRIBE ];
		return hash_hmac( 'sha256', $prefix . '|' . $payload, (string) wp_salt( 'auth' ) );
	}
}
