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

namespace Hafenstudios\Kurato\Mail;

defined( 'ABSPATH' ) || exit;

/**
 * Signierter Token fuer das Open-/Click-Tracking hinter dem doppelten
 * Consent-Gate (K4-Konzept Abschnitt 7, K4e-2).
 *
 * Muster wie {@see LinkToken} (Leadlotse-DoiService-Linie):
 * `base64url( payload ) . "." . HMAC`, HMAC ueber `wp_salt('auth')`. Der Token
 * ist zweckgebunden ueber einen eigenen HMAC-Praefix je Zweck, damit ein
 * Open-Token nicht am Click-Endpoint durchgeht und umgekehrt, und damit ein
 * Tracking-Token nichts am Abmelde-/Preference-Token gemein hat (dort andere
 * Praefixe).
 *
 * - Open-Pixel: Nutzlast `campaign_id . "|" . contact_id`, Praefix
 *   `kurato_track_o`.
 * - Click-Redirect: Nutzlast `campaign_id . "|" . contact_id . "|" . link_index`,
 *   Praefix `kurato_track_c`.
 *
 * Bewusst KEIN Ablauf (wie LinkToken): die Links stehen in bereits versendeten
 * Mails und muessen dauerhaft aufloesbar bleiben. Der Token traegt keine Rechte
 * ausser dem Zaehlen eines Ereignisses bzw. der Aufloesung eines beim Rendern
 * hinterlegten Redirect-Ziels; Missbrauch faengt zusaetzlich das Consent-Gate
 * und die Link-Registry (kein Open-Redirect) ab.
 */
final class TrackingToken {

	/**
	 * Zweck: Open-Pixel.
	 */
	public const PURPOSE_OPEN = 'open';

	/**
	 * Zweck: Click-Redirect.
	 */
	public const PURPOSE_CLICK = 'click';

	/**
	 * HMAC-Praefix je Zweck (Domain-Trennung).
	 *
	 * @var array<string, string>
	 */
	private const PREFIXES = array(
		self::PURPOSE_OPEN  => 'kurato_track_o',
		self::PURPOSE_CLICK => 'kurato_track_c',
	);

	/**
	 * Erzeugt einen Open-Pixel-Token fuer (Kampagne, Kontakt).
	 *
	 * @param int $campaign_id Kampagnen-ID.
	 * @param int $contact_id  Kontakt-ID.
	 * @return string
	 */
	public function create_open( int $campaign_id, int $contact_id ): string {
		return $this->encode( self::PURPOSE_OPEN, array( max( 0, $campaign_id ), max( 0, $contact_id ) ) );
	}

	/**
	 * Erzeugt einen Click-Redirect-Token fuer (Kampagne, Kontakt, Link-Index).
	 *
	 * @param int $campaign_id Kampagnen-ID.
	 * @param int $contact_id  Kontakt-ID.
	 * @param int $link_index  Index in der Link-Registry der Kampagne (0-basiert).
	 * @return string
	 */
	public function create_click( int $campaign_id, int $contact_id, int $link_index ): string {
		return $this->encode( self::PURPOSE_CLICK, array( max( 0, $campaign_id ), max( 0, $contact_id ), max( 0, $link_index ) ) );
	}

	/**
	 * Prueft einen Open-Token.
	 *
	 * @param string $token Token aus dem Pixel-Aufruf.
	 * @return array{campaign_id:int, contact_id:int}|\WP_Error
	 */
	public function verify_open( string $token ) {
		$parts = $this->decode( $token, self::PURPOSE_OPEN, 2 );
		if ( is_wp_error( $parts ) ) {
			return $parts;
		}
		return array(
			'campaign_id' => $parts[0],
			'contact_id'  => $parts[1],
		);
	}

	/**
	 * Prueft einen Click-Token.
	 *
	 * @param string $token Token aus dem Redirect-Aufruf.
	 * @return array{campaign_id:int, contact_id:int, link_index:int}|\WP_Error
	 */
	public function verify_click( string $token ) {
		$parts = $this->decode( $token, self::PURPOSE_CLICK, 3 );
		if ( is_wp_error( $parts ) ) {
			return $parts;
		}
		return array(
			'campaign_id' => $parts[0],
			'contact_id'  => $parts[1],
			'link_index'  => $parts[2],
		);
	}

	/**
	 * Baut Token aus einer Zweck-Nutzlast.
	 *
	 * @param string          $purpose Zweck.
	 * @param array<int, int> $segments Ganzzahlige Nutzlast-Segmente.
	 * @return string
	 */
	private function encode( string $purpose, array $segments ): string {
		$payload = implode( '|', array_map( 'strval', $segments ) );
		return rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' ) . '.' . $this->sign( $payload, $purpose );
	}

	/**
	 * Prueft und zerlegt einen Token in genau `$expected` Ganzzahl-Segmente.
	 *
	 * @param string $token    Token.
	 * @param string $purpose  Erwarteter Zweck.
	 * @param int    $expected Erwartete Segmentanzahl.
	 * @return array<int, int>|\WP_Error
	 */
	private function decode( string $token, string $purpose, int $expected ) {
		$invalid = new \WP_Error( 'kurato_invalid_tracking_token', 'invalid', array( 'status' => 400 ) );

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

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

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

		$out = array();
		foreach ( $segments as $segment ) {
			if ( '' === $segment || ! ctype_digit( $segment ) ) {
				return $invalid;
			}
			$out[] = (int) $segment;
		}

		// Kampagne und Kontakt muessen positiv sein (Segment 0 und 1).
		if ( $out[0] <= 0 || $out[1] <= 0 ) {
			return $invalid;
		}

		return $out;
	}

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