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

namespace Hafenstudios\Kurato\Preferences;

use Hafenstudios\Core\Core;
use Hafenstudios\Core\Events\EventBus;
use Hafenstudios\Kurato\Db\ContactRepository;
use Hafenstudios\Kurato\Db\ListRepository;
use Hafenstudios\Kurato\Db\MailEventRepository;
use Hafenstudios\Kurato\Db\SubscriptionRepository;
use Hafenstudios\Kurato\Mail\LinkToken;

defined( 'ABSPATH' ) || exit;

/**
 * Preference Center light (K4-Konzept Abschnitt 8, K4d-2).
 *
 * Oeffentliche, per signiertem Link erreichbare Seite (kein Login). Zeigt dem
 * Kontakt seine Listen mit Abo-Status und erlaubt: Abmelden je Liste, "Alle
 * abmelden", Wiederanmelden je Liste (nur zurueck auf einen frueheren
 * bestaetigten Stand, kein Neu-Abo ohne Double-Opt-in) und einen
 * Tracking-Consent-Toggle (Default aus).
 *
 * Technik: Query-Var `kurato_prefs` plus `template_redirect`-Handler, der eine
 * eigenstaendige, escapte HTML-Seite rendert (kein Theme, keine Admin-Assets,
 * Sie-Form). Aktionen laufen als POST-Formulare, die den zweckgebundenen Token
 * mitfuehren (LinkToken::PURPOSE_PREFERENCES). Absicherung ausschliesslich ueber
 * den HMAC-Token (keine Nonce), Rate-Limit pro IP wie beim Abmelde-Endpoint.
 *
 * Nicht final, damit Tests den Consent-Log-Aufruf per Test-Double kapseln
 * koennen (Muster wie die Repositories).
 */
class PreferenceCenter {

	/**
	 * Query-Var, die die Preference-Center-Seite ausloest.
	 */
	public const QUERY_VAR = 'kurato_prefs';

	/**
	 * Textversion des Tracking-Einwilligungstexts fuer den Nachweis. Der finale
	 * Rechtstext kommt spaeter vom Inhaber (offene Entscheidung 4 im K4-Konzept);
	 * bei einer Textaenderung wird diese Version erhoeht.
	 */
	private const TRACKING_TEXT_VERSION = '1';

	/**
	 * Schwelle des IP-Rate-Limits (Anfragen pro Zeitfenster).
	 */
	private const RATE_LIMIT = 30;

	private SubscriptionRepository $subscriptions;

	private ContactRepository $contacts;

	private ListRepository $lists;

	private LinkToken $tokens;

	private ?MailEventRepository $events;

	/**
	 * @param SubscriptionRepository   $subscriptions Abo-Repository.
	 * @param ContactRepository        $contacts      Kontakt-Repository.
	 * @param ListRepository           $lists         Listen-Repository.
	 * @param LinkToken                $tokens        Token-Dienst (zustandslos).
	 * @param MailEventRepository|null $events        Ereignis-Log (optional, K4e-1).
	 */
	public function __construct( SubscriptionRepository $subscriptions, ContactRepository $contacts, ListRepository $lists, LinkToken $tokens, ?MailEventRepository $events = null ) {
		$this->subscriptions = $subscriptions;
		$this->contacts      = $contacts;
		$this->lists         = $lists;
		$this->tokens        = $tokens;
		$this->events        = $events;
	}

	/**
	 * Verdrahtet Query-Var und Seiten-Handler.
	 *
	 * @return void
	 */
	public function register(): void {
		add_filter(
			'query_vars',
			static function ( $vars ) {
				$vars[] = self::QUERY_VAR;
				return $vars;
			}
		);
		add_action( 'template_redirect', array( $this, 'maybe_render' ) );
	}

	/**
	 * URL der Preference-Center-Seite fuer einen zweckgebundenen Token.
	 *
	 * @param string $token Preference-Token (LinkToken mit PURPOSE_PREFERENCES).
	 * @return string
	 */
	public static function url( string $token ): string {
		return add_query_arg(
			array(
				self::QUERY_VAR => 1,
				'token'         => $token,
			),
			home_url( '/' )
		);
	}

	/**
	 * Erzeugt den zweckgebundenen Preference-Token fuer einen Kontakt.
	 *
	 * @param LinkToken $tokens     Token-Dienst.
	 * @param int       $contact_id Kontakt-ID.
	 * @return string
	 */
	public static function token_for( LinkToken $tokens, int $contact_id ): string {
		return $tokens->create( $contact_id, 0, LinkToken::PURPOSE_PREFERENCES );
	}

	/**
	 * template_redirect: rendert die Seite bzw. verarbeitet ein POST-Formular.
	 *
	 * @return void
	 */
	public function maybe_render(): void {
		if ( '' === (string) get_query_var( self::QUERY_VAR ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Token-gesichert (kein Login, keine Nonce).
		$token = isset( $_REQUEST['token'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['token'] ) ) : '';

		if ( $this->rate_limited() ) {
			$this->render_message_page(
				__( 'Zu viele Anfragen', 'kurato' ),
				__( 'Es wurden zu viele Anfragen gesendet. Bitte versuchen Sie es in einigen Minuten erneut.', 'kurato' )
			);
			exit;
		}

		$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';

		if ( 'POST' === $method ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Token-gesichert (kein Login, keine Nonce).
			$result = $this->handle_post( $token, wp_unslash( $_POST ) );
			// Post/Redirect/Get: nach der Aktion zurueck auf die GET-Ansicht.
			wp_safe_redirect(
				add_query_arg(
					array(
						self::QUERY_VAR => 1,
						'token'         => $token,
						'msg'           => $result,
					),
					home_url( '/' )
				)
			);
			exit;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nur Anzeige nach Redirect.
		$msg = isset( $_GET['msg'] ) ? sanitize_key( wp_unslash( $_GET['msg'] ) ) : '';
		$this->render( $token, $msg );
		exit;
	}

	/**
	 * Verarbeitet eine Preference-Aktion (rein, ohne HTTP/Rendering, damit
	 * testbar). Prueft zuerst den zweckgebundenen Token; bei ungueltigem Token
	 * passiert NICHTS (keine Seiteneffekte).
	 *
	 * @param string               $token Preference-Token.
	 * @param array<string, mixed> $post  POST-Daten (bereits unslashed).
	 * @return string Ergebnis-Code fuer die Statusmeldung.
	 */
	public function handle_post( string $token, array $post ): string {
		$claim = $this->tokens->verify( $token, LinkToken::PURPOSE_PREFERENCES );
		if ( is_wp_error( $claim ) ) {
			return 'invalid';
		}
		$contact_id = (int) $claim['contact_id'];
		if ( $contact_id <= 0 ) {
			return 'invalid';
		}

		$op = isset( $post['op'] ) ? sanitize_key( (string) $post['op'] ) : '';

		switch ( $op ) {
			case 'unsubscribe_list':
				$list_id = isset( $post['list_id'] ) ? absint( $post['list_id'] ) : 0;
				return $list_id > 0 ? $this->do_unsubscribe( $contact_id, $list_id ) : 'noop';

			case 'unsubscribe_all':
				return $this->do_unsubscribe( $contact_id, 0 );

			case 'resubscribe_list':
				$list_id = isset( $post['list_id'] ) ? absint( $post['list_id'] ) : 0;
				return $list_id > 0 ? $this->do_resubscribe( $contact_id, $list_id ) : 'noop';

			case 'save_tracking':
				return $this->do_tracking( $contact_id, ! empty( $post['tracking_consent'] ) );

			default:
				return 'noop';
		}
	}

	// --- Aktionen ----------------------------------------------------------

	/**
	 * Abmeldung je Liste oder global (list_id = 0). Schreibt den Consent-Widerruf
	 * ins Consent-Log und feuert das Flotten-Event (Muster UnsubscribeController).
	 *
	 * @param int $contact_id Kontakt-ID.
	 * @param int $list_id    Listen-ID, oder 0 fuer alle.
	 * @return string
	 */
	private function do_unsubscribe( int $contact_id, int $list_id ): string {
		if ( 0 === $list_id ) {
			$this->subscriptions->unsubscribe_all( $contact_id );
		} else {
			$this->subscriptions->unsubscribe( $list_id, $contact_id );
		}

		$email = $this->contact_email( $contact_id );
		if ( '' !== $email ) {
			$this->record_consent(
				array(
					'source'        => 'kurato:preferences',
					'subject_email' => $email,
					'purpose'       => 'newsletter',
					'consent_text'  => __( 'Widerruf der Einwilligung ueber das Preference Center (Abmeldung).', 'kurato' ),
					'text_version'  => '1',
					'reference'     => 'unsubscribe',
				)
			);
		}

		// Ereignis-Log (K4e-1): Abmeldung ohne Kampagnenbezug (campaign_id 0 -> NULL).
		if ( null !== $this->events ) {
			$this->events->record( 0, $contact_id, 'unsubscribe', array( 'detail' => 0 === $list_id ? 'all' : 'list:' . $list_id ) );
		}

		EventBus::emit(
			'subscription_unsubscribed',
			array(
				'contact_id' => $contact_id,
				'list_id'    => $list_id,
				'scope'      => 0 === $list_id ? 'all' : 'list',
			)
		);

		return 0 === $list_id ? 'unsub_all' : 'unsub_list';
	}

	/**
	 * Wiederanmeldung je Liste. Nur zulaessig, wenn das Abo aktuell abgemeldet ist
	 * UND schon einmal bestaetigt war (confirmed_at gesetzt) - sonst muesste ein
	 * Double-Opt-in laufen. Protokolliert die Wiederherstellung im Consent-Log.
	 *
	 * @param int $contact_id Kontakt-ID.
	 * @param int $list_id    Listen-ID.
	 * @return string
	 */
	private function do_resubscribe( int $contact_id, int $list_id ): string {
		$sub = $this->subscriptions->find( $list_id, $contact_id );
		if ( null === $sub || 'unsubscribed' !== (string) ( $sub['status'] ?? '' ) || '' === (string) ( $sub['confirmed_at'] ?? '' ) ) {
			// Kein Neu-Abo ohne DOI: nur ein frueher bestaetigter Stand darf zurueck.
			return 'resub_denied';
		}

		if ( ! $this->subscriptions->resubscribe( $list_id, $contact_id ) ) {
			return 'resub_denied';
		}

		$email = $this->contact_email( $contact_id );
		if ( '' !== $email ) {
			$this->record_consent(
				array(
					'source'        => 'kurato:preferences',
					'subject_email' => $email,
					'purpose'       => 'newsletter',
					'consent_text'  => __( 'Erneute Anmeldung ueber das Preference Center: Wiederherstellung einer zuvor bestaetigten Einwilligung.', 'kurato' ),
					'text_version'  => '1',
					'reference'     => 'resubscribe',
				)
			);
		}

		return 'resub_ok';
	}

	/**
	 * Tracking-Consent-Toggle. Schreibt hs_contacts.tracking_consent und
	 * protokolliert JEDE Aenderung (Erteilen wie Widerruf) im Consent-Log
	 * (Consent-Typ mail_tracking). Ist der gewuenschte Zustand bereits gesetzt,
	 * passiert nichts.
	 *
	 * @param int  $contact_id Kontakt-ID.
	 * @param bool $tracking   Gewuenschter Zustand (Checkbox angehakt = true).
	 * @return string
	 */
	private function do_tracking( int $contact_id, bool $tracking ): string {
		$contact = $this->contacts->get( $contact_id );
		if ( null === $contact ) {
			return 'noop';
		}

		$current = (int) ( $contact['tracking_consent'] ?? 0 );
		$desired = $tracking ? 1 : 0;
		if ( $current === $desired ) {
			return 'tracking_unchanged';
		}

		$this->contacts->set_tracking_consent( $contact_id, 1 === $desired );

		$email = sanitize_email( (string) ( $contact['email'] ?? '' ) );
		if ( '' !== $email ) {
			$this->record_consent(
				array(
					'source'        => 'kurato:preferences',
					'subject_email' => $email,
					'purpose'       => 'mail_tracking',
					// Ehrliche, verstaendliche Formulierung; finaler Rechtstext folgt vom Inhaber.
					'consent_text'  => 1 === $desired
						? __( 'Einwilligung erteilt: Oeffnungen und Klicks in unseren Newslettern duerfen gemessen werden.', 'kurato' )
						: __( 'Einwilligung widerrufen: Oeffnungen und Klicks in unseren Newslettern werden nicht mehr gemessen.', 'kurato' ),
					'text_version'  => self::TRACKING_TEXT_VERSION,
					'reference'     => 1 === $desired ? 'tracking_granted' : 'tracking_revoked',
				)
			);
		}

		return 1 === $desired ? 'tracking_on' : 'tracking_off';
	}

	/**
	 * Schreibt einen Consent-Nachweis. Eigene Methode als Test-Seam: Tests
	 * ueberschreiben sie, um den Consent-Log-Aufruf ohne echtes wpdb zu pruefen.
	 *
	 * @param array<string, mixed> $data Consent-Daten (Core-ConsentLog-Format).
	 * @return int Consent-ID (0 bei Fehler).
	 */
	protected function record_consent( array $data ): int {
		return Core::instance()->consent()->record( $data );
	}

	/**
	 * Loest die (sanitisierte) E-Mail eines Kontakts auf, oder '' wenn keine.
	 *
	 * @param int $contact_id Kontakt-ID.
	 * @return string
	 */
	private function contact_email( int $contact_id ): string {
		$contact = $this->contacts->get( $contact_id );
		return null !== $contact ? sanitize_email( (string) ( $contact['email'] ?? '' ) ) : '';
	}

	// --- Rate-Limit --------------------------------------------------------

	/**
	 * Prueft und erhoeht das IP-Rate-Limit (Muster UnsubscribeController).
	 *
	 * @return bool true, wenn blockiert.
	 */
	private function rate_limited(): bool {
		$ip  = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		$key = 'kurato_prefs_' . md5( $ip );

		$count = (int) get_transient( $key );
		if ( $count >= self::RATE_LIMIT ) {
			return true;
		}
		set_transient( $key, $count + 1, 10 * MINUTE_IN_SECONDS );
		return false;
	}

	// --- Rendering ---------------------------------------------------------

	/**
	 * Rendert die Preference-Center-Seite (GET). Bei ungueltigem Token eine
	 * freundliche Fehlerseite.
	 *
	 * @param string $token Preference-Token.
	 * @param string $msg   Statuscode aus dem PRG-Redirect.
	 * @return void
	 */
	private function render( string $token, string $msg = '' ): void {
		$claim = $this->tokens->verify( $token, LinkToken::PURPOSE_PREFERENCES );
		if ( is_wp_error( $claim ) ) {
			$this->render_message_page(
				__( 'Link ungueltig', 'kurato' ),
				__( 'Dieser Link ist ungueltig. Bitte nutzen Sie den Link aus einer aktuellen Mail oder wenden Sie sich an uns.', 'kurato' )
			);
			return;
		}

		$contact_id = (int) $claim['contact_id'];
		$subs       = $this->subscriptions->for_contact( $contact_id );
		$contact    = $this->contacts->get( $contact_id );
		$tracking   = null !== $contact ? (int) ( $contact['tracking_consent'] ?? 0 ) : 0;

		$this->render_head( __( 'Newsletter-Einstellungen', 'kurato' ) );

		echo '<div class="hs-card">';
		echo '<h1>' . esc_html__( 'Ihre Newsletter-Einstellungen', 'kurato' ) . '</h1>';
		echo '<p class="hs-lead">' . esc_html__( 'Hier verwalten Sie, welche Newsletter Sie von uns erhalten und ob wir Oeffnungen und Klicks messen duerfen.', 'kurato' ) . '</p>';

		$this->render_notice( $msg );

		$this->render_lists( $token, $subs );
		$this->render_tracking( $token, 1 === $tracking );

		echo '</div>';
		$this->render_foot();
	}

	/**
	 * Listet die Abos des Kontakts mit Status und Aktionen.
	 *
	 * @param string                          $token Preference-Token.
	 * @param array<int, array<string, mixed>> $subs  Abos (mit list_label).
	 * @return void
	 */
	private function render_lists( string $token, array $subs ): void {
		echo '<h2>' . esc_html__( 'Ihre Listen', 'kurato' ) . '</h2>';

		if ( array() === $subs ) {
			echo '<p>' . esc_html__( 'Sie sind aktuell in keiner unserer Listen eingetragen.', 'kurato' ) . '</p>';
			return;
		}

		$has_active = false;
		echo '<ul class="hs-lists">';
		foreach ( $subs as $sub ) {
			$list_id  = (int) $sub['list_id'];
			$status   = (string) $sub['status'];
			$label    = (string) ( $sub['list_label'] ?? '' );
			$was_conf = '' !== (string) ( $sub['confirmed_at'] ?? '' );

			echo '<li class="hs-list-row">';
			echo '<span class="hs-list-label">' . esc_html( $label ) . '</span> ';
			echo '<span class="hs-badge hs-badge--' . esc_attr( $status ) . '">' . esc_html( $this->status_label( $status ) ) . '</span>';

			if ( 'confirmed' === $status ) {
				$has_active = true;
				echo $this->action_form( $token, 'unsubscribe_list', $list_id, __( 'Abmelden', 'kurato' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
			} elseif ( 'unsubscribed' === $status && $was_conf ) {
				echo $this->action_form( $token, 'resubscribe_list', $list_id, __( 'Wieder anmelden', 'kurato' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
			}

			echo '</li>';
		}
		echo '</ul>';

		if ( $has_active ) {
			echo '<form class="hs-form-inline" method="post" action="' . esc_url( home_url( '/' ) ) . '">';
			echo $this->hidden_fields( $token, 'unsubscribe_all', 0 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
			echo '<button type="submit" class="hs-button hs-button--link">' . esc_html__( 'Von allen Newslettern abmelden', 'kurato' ) . '</button>';
			echo '</form>';
		}
	}

	/**
	 * Rendert den Tracking-Consent-Toggle als eigenes POST-Formular.
	 *
	 * @param string $token   Preference-Token.
	 * @param bool   $enabled Aktueller Zustand.
	 * @return void
	 */
	private function render_tracking( string $token, bool $enabled ): void {
		echo '<h2>' . esc_html__( 'Messung von Oeffnungen und Klicks', 'kurato' ) . '</h2>';
		echo '<form method="post" action="' . esc_url( home_url( '/' ) ) . '">';
		echo $this->hidden_fields( $token, 'save_tracking', 0 ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
		echo '<label class="hs-check">';
		echo '<input type="checkbox" name="tracking_consent" value="1"' . ( $enabled ? ' checked="checked"' : '' ) . ' /> ';
		echo esc_html__( 'Ja, Sie duerfen messen, ob ich Ihre Newsletter oeffne und welche Links ich anklicke.', 'kurato' );
		echo '</label>';
		echo '<p class="hs-hint">' . esc_html__( 'Diese Einwilligung ist freiwillig und unabhaengig von Ihrem Newsletter-Abo. Sie koennen sie jederzeit hier wieder aendern.', 'kurato' ) . '</p>';
		echo '<button type="submit" class="hs-button">' . esc_html__( 'Speichern', 'kurato' ) . '</button>';
		echo '</form>';
	}

	/**
	 * Ein kompaktes Aktions-Formular (ein Button, versteckte Felder).
	 *
	 * @param string $token   Preference-Token.
	 * @param string $op      Operation.
	 * @param int    $list_id Listen-ID.
	 * @param string $label   Button-Beschriftung.
	 * @return string
	 */
	private function action_form( string $token, string $op, int $list_id, string $label ): string {
		$out  = '<form class="hs-form-inline" method="post" action="' . esc_url( home_url( '/' ) ) . '">';
		$out .= $this->hidden_fields( $token, $op, $list_id );
		$out .= '<button type="submit" class="hs-button hs-button--small">' . esc_html( $label ) . '</button>';
		$out .= '</form>';
		return $out;
	}

	/**
	 * Versteckte Formularfelder (Query-Var, Token, Operation, Listen-ID).
	 *
	 * @param string $token   Preference-Token.
	 * @param string $op      Operation.
	 * @param int    $list_id Listen-ID.
	 * @return string
	 */
	private function hidden_fields( string $token, string $op, int $list_id ): string {
		$out  = '<input type="hidden" name="' . esc_attr( self::QUERY_VAR ) . '" value="1" />';
		$out .= '<input type="hidden" name="token" value="' . esc_attr( $token ) . '" />';
		$out .= '<input type="hidden" name="op" value="' . esc_attr( $op ) . '" />';
		if ( $list_id > 0 ) {
			$out .= '<input type="hidden" name="list_id" value="' . esc_attr( (string) $list_id ) . '" />';
		}
		return $out;
	}

	/**
	 * Statusmeldung nach einer Aktion.
	 *
	 * @param string $msg Statuscode.
	 * @return void
	 */
	private function render_notice( string $msg ): void {
		$map = array(
			'unsub_list'         => array( 'ok', __( 'Sie wurden von diesem Newsletter abgemeldet.', 'kurato' ) ),
			'unsub_all'          => array( 'ok', __( 'Sie wurden von allen Newslettern abgemeldet.', 'kurato' ) ),
			'resub_ok'           => array( 'ok', __( 'Sie sind wieder angemeldet. Willkommen zurueck.', 'kurato' ) ),
			'resub_denied'       => array( 'warn', __( 'Diese Liste konnte nicht wiederhergestellt werden. Bitte melden Sie sich ueber unser Formular neu an.', 'kurato' ) ),
			'tracking_on'        => array( 'ok', __( 'Vielen Dank. Wir duerfen Oeffnungen und Klicks nun messen.', 'kurato' ) ),
			'tracking_off'       => array( 'ok', __( 'Erledigt. Wir messen Oeffnungen und Klicks nicht mehr.', 'kurato' ) ),
			'tracking_unchanged' => array( 'ok', __( 'Ihre Einstellung wurde uebernommen.', 'kurato' ) ),
			'invalid'            => array( 'warn', __( 'Der Link ist ungueltig. Bitte nutzen Sie den Link aus einer aktuellen Mail.', 'kurato' ) ),
		);
		if ( '' === $msg || ! isset( $map[ $msg ] ) ) {
			return;
		}
		echo '<div class="hs-notice hs-notice--' . esc_attr( $map[ $msg ][0] ) . '">' . esc_html( $map[ $msg ][1] ) . '</div>';
	}

	/**
	 * Menschliche Statusbezeichnung.
	 *
	 * @param string $status Status.
	 * @return string
	 */
	private function status_label( string $status ): string {
		switch ( $status ) {
			case 'confirmed':
				return __( 'angemeldet', 'kurato' );
			case 'unsubscribed':
				return __( 'abgemeldet', 'kurato' );
			case 'pending':
				return __( 'noch nicht bestaetigt', 'kurato' );
			default:
				return $status;
		}
	}

	/**
	 * Rendert eine schlichte Meldungsseite (Fehler/Hinweis) und beendet die Seite.
	 *
	 * @param string $title   Ueberschrift.
	 * @param string $message Text.
	 * @return void
	 */
	private function render_message_page( string $title, string $message ): void {
		$this->render_head( $title );
		echo '<div class="hs-card">';
		echo '<h1>' . esc_html( $title ) . '</h1>';
		echo '<p>' . esc_html( $message ) . '</p>';
		echo '</div>';
		$this->render_foot();
	}

	/**
	 * Kopf der eigenstaendigen HTML-Seite (kein Theme, Inline-CSS).
	 *
	 * @param string $title Seitentitel.
	 * @return void
	 */
	private function render_head( string $title ): void {
		// HTTP-Header nur setzen, wenn noch nichts gesendet wurde (defensiv).
		if ( ! headers_sent() ) {
			header( 'Content-Type: text/html; charset=utf-8' );
			nocache_headers();
		}
		$site = get_bloginfo( 'name' );

		echo '<!DOCTYPE html>' . "\n";
		echo '<html lang="de"><head>';
		echo '<meta charset="utf-8" />';
		echo '<meta name="viewport" content="width=device-width, initial-scale=1" />';
		echo '<meta name="robots" content="noindex, nofollow" />';
		echo '<title>' . esc_html( $title ) . '</title>';
		echo '<style>'
			. 'body{margin:0;background:#f4f4f4;color:#222;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;}'
			. '.hs-wrap{max-width:640px;margin:0 auto;padding:32px 16px;}'
			. '.hs-card{background:#fff;border-radius:8px;padding:32px;box-shadow:0 1px 3px rgba(0,0,0,.08);}'
			. 'h1{font-size:22px;margin:0 0 8px;}h2{font-size:17px;margin:28px 0 12px;}'
			. '.hs-lead{color:#555;margin:0 0 8px;}.hs-hint{color:#777;font-size:13px;margin:8px 0 16px;}'
			. '.hs-lists{list-style:none;padding:0;margin:0;}'
			. '.hs-list-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:10px 0;border-bottom:1px solid #eee;}'
			. '.hs-list-label{flex:1;min-width:160px;font-weight:bold;}'
			. '.hs-badge{font-size:12px;padding:2px 8px;border-radius:10px;background:#eee;color:#555;}'
			. '.hs-badge--confirmed{background:#e4f4e4;color:#256029;}'
			. '.hs-badge--unsubscribed{background:#f6e2e2;color:#8a2b2b;}'
			. '.hs-form-inline{display:inline;margin:0;}'
			. '.hs-check{display:block;margin:8px 0;}'
			. '.hs-button{background:#1f6feb;color:#fff;border:0;border-radius:6px;padding:10px 18px;font-size:15px;cursor:pointer;}'
			. '.hs-button--small{padding:5px 12px;font-size:13px;}'
			. '.hs-button--link{background:none;color:#1f6feb;padding:8px 0;text-decoration:underline;}'
			. '.hs-notice{padding:10px 14px;border-radius:6px;margin:16px 0;}'
			. '.hs-notice--ok{background:#e4f4e4;color:#256029;}'
			. '.hs-notice--warn{background:#fbf1d8;color:#7a5b12;}'
			. '.hs-foot{color:#999;font-size:12px;text-align:center;margin-top:24px;}'
			. '</style>';
		echo '</head><body><div class="hs-wrap">';

		// Absender/Seitentitel als Kontext oben.
		if ( '' !== (string) $site ) {
			echo '<p class="hs-foot" style="margin-top:0;">' . esc_html( (string) $site ) . '</p>';
		}
	}

	/**
	 * Fuss der eigenstaendigen HTML-Seite.
	 *
	 * @return void
	 */
	private function render_foot(): void {
		echo '</div></body></html>';
	}
}
