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

namespace Hafenstudios\Kurato\Admin;

use Hafenstudios\Core\Core;
use Hafenstudios\Core\Mail\Message;
use Hafenstudios\Kurato\Campaign\AudienceSelector;
use Hafenstudios\Kurato\Campaign\CampaignMaterializer;
use Hafenstudios\Kurato\Campaign\CampaignStats;
use Hafenstudios\Kurato\Campaign\MailPostType;
use Hafenstudios\Kurato\Campaign\SegmentQuery;
use Hafenstudios\Kurato\Db\CampaignRepository;
use Hafenstudios\Kurato\Db\ListRepository;
use Hafenstudios\Kurato\Db\MailEventRepository;
use Hafenstudios\Kurato\Db\QueueRepository;
use Hafenstudios\Kurato\Db\SegmentRepository;
use Hafenstudios\Kurato\Db\SubscriptionRepository;
use Hafenstudios\Kurato\Mail\DnsCheckService;
use Hafenstudios\Kurato\Mail\DriverFactory;
use Hafenstudios\Kurato\Mail\EmailRenderer;
use Hafenstudios\Kurato\Mail\LinkToken;
use Hafenstudios\Kurato\Preferences\PreferenceCenter;

defined( 'ABSPATH' ) || exit;

/**
 * Kampagnen-Verwaltung (Etappe K4c-3): der Betreiber-Kreis vom Anlegen bis zum
 * Start des Versands.
 *
 * Serverseitig ueber admin-post (D3, kein JS-Framework). Die Uebersichtsseite
 * listet alle Kampagnen mit Status und Fortschritt; die versteckte Detailseite
 * fuehrt je Kampagne durch Empfaengerauswahl, Zustellbarkeits-Ampel (SPF/DKIM),
 * Testversand und Start. Der Inhalt (Betreff = Post-Title, Body = Blocks) wird im
 * Block-Editor des CPT `hs_mail` geschrieben.
 *
 * Rechte: Ansehen ueber `hs_view_contacts`, jede Aenderung (Anlegen, Empfaenger,
 * Test, Start, Pause/Abbruch) ueber `hs_send_mails`. Jede Aktion ist per Nonce und
 * Capability abgesichert und endet in `wp_safe_redirect` mit Statusmeldung.
 */
final class CampaignsPage {

	public const SLUG        = 'kurato-campaigns';
	public const DETAIL_SLUG = 'kurato-campaign';

	public const ACTION_CREATE   = 'kurato_create_campaign';
	public const ACTION_AUDIENCE = 'kurato_save_audience';
	public const ACTION_TEST     = 'kurato_test_campaign';
	public const ACTION_START    = 'kurato_start_campaign';
	public const ACTION_STATUS   = 'kurato_campaign_status';

	private const DNS_TRANSIENT = 'kurato_dns_check';

	private CampaignRepository $campaigns;

	private CampaignMaterializer $materializer;

	private ListRepository $lists;

	private SubscriptionRepository $subscriptions;

	private QueueRepository $queue;

	private SegmentRepository $segments;

	private SegmentQuery $segment_query;

	private MailEventRepository $events;

	/**
	 * @param CampaignRepository     $campaigns     Kampagnen-Repository.
	 * @param CampaignMaterializer   $materializer  Empfaenger-Materialisierung.
	 * @param ListRepository         $lists         Listen-Repository.
	 * @param SubscriptionRepository $subscriptions Abo-Repository (Vorschau-Zahl).
	 * @param QueueRepository        $queue         Queue-Repository (Fortschritt).
	 * @param SegmentRepository      $segments      Segment-Repository (K4d).
	 * @param SegmentQuery           $segment_query Segment-Auswertung (K4d).
	 * @param MailEventRepository    $events        Ereignis-Log (Statistik, K4e-2).
	 */
	public function __construct(
		CampaignRepository $campaigns,
		CampaignMaterializer $materializer,
		ListRepository $lists,
		SubscriptionRepository $subscriptions,
		QueueRepository $queue,
		SegmentRepository $segments,
		SegmentQuery $segment_query,
		MailEventRepository $events
	) {
		$this->campaigns     = $campaigns;
		$this->materializer  = $materializer;
		$this->lists         = $lists;
		$this->subscriptions = $subscriptions;
		$this->queue         = $queue;
		$this->segments      = $segments;
		$this->segment_query = $segment_query;
		$this->events        = $events;
	}

	/**
	 * Registriert die admin-post-Handler.
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( 'admin_post_' . self::ACTION_CREATE, array( $this, 'handle_create' ) );
		add_action( 'admin_post_' . self::ACTION_AUDIENCE, array( $this, 'handle_save_audience' ) );
		add_action( 'admin_post_' . self::ACTION_TEST, array( $this, 'handle_test' ) );
		add_action( 'admin_post_' . self::ACTION_START, array( $this, 'handle_start' ) );
		add_action( 'admin_post_' . self::ACTION_STATUS, array( $this, 'handle_status' ) );
	}

	/**
	 * URL der Detailseite einer Kampagne.
	 *
	 * @param int $campaign_id Kampagnen-ID.
	 * @return string
	 */
	public static function detail_url( int $campaign_id ): string {
		return admin_url( add_query_arg( array( 'page' => self::DETAIL_SLUG, 'campaign' => $campaign_id ), 'admin.php' ) );
	}

	// --- Uebersicht --------------------------------------------------------

	/**
	 * Rendert die Kampagnen-Uebersicht.
	 *
	 * @return void
	 */
	public function render(): void {
		if ( ! current_user_can( 'hs_view_contacts' ) ) {
			wp_die( esc_html__( 'Sie haben keine Berechtigung, Kampagnen einzusehen.', 'kurato' ) );
		}

		$can_send = current_user_can( 'hs_send_mails' );

		echo '<div class="wrap hs-kurato-campaigns">';
		echo '<h1 class="wp-heading-inline">' . esc_html__( 'Kampagnen', 'kurato' ) . '</h1>';
		if ( $can_send ) {
			echo ' ' . $this->create_button( 'page-title-action' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Button intern escaped.
		}
		echo '<hr class="wp-header-end" />';

		$this->render_notice();

		$result = $this->campaigns->all( 1, 50 );
		if ( array() === $result['items'] ) {
			$this->render_empty_state( $can_send );
			echo '</div>';
			return;
		}

		$this->render_table( $result['items'] );
		echo '</div>';
	}

	/**
	 * Leerer Zustand mit Hans.
	 *
	 * @param bool $can_send Darf der Nutzer Kampagnen anlegen?
	 * @return void
	 */
	private function render_empty_state( bool $can_send ): void {
		$image = Core::instance()->hans()->image_url( 30 );

		echo '<div class="hs-empty-state" style="text-align:center;padding:48px 0;">';
		if ( '' !== $image ) {
			echo '<img src="' . esc_url( $image ) . '" alt="" width="160" height="160" style="height:auto;" />';
		}
		echo '<p style="font-size:15px;margin:16px 0;">' . esc_html__( 'Noch keine Kampagne. Legen Sie Ihre erste Newsletter-Mail an und schreiben Sie los.', 'kurato' ) . '</p>';
		if ( $can_send ) {
			echo $this->create_button( 'button button-primary button-hero' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Button intern escaped.
		}
		echo '</div>';
	}

	/**
	 * Die Kampagnen-Tabelle.
	 *
	 * @param array<int, array<string, mixed>> $items Kampagnen.
	 * @return void
	 */
	private function render_table( array $items ): void {
		echo '<table class="wp-list-table widefat fixed striped">';
		echo '<thead><tr>';
		echo '<th scope="col">' . esc_html__( 'Betreff', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Status', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Fortschritt', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Angelegt', 'kurato' ) . '</th>';
		echo '</tr></thead><tbody>';

		foreach ( $items as $campaign ) {
			$id      = (int) $campaign['id'];
			$post_id = (int) $campaign['post_id'];
			$subject = (string) get_the_title( $post_id );
			if ( '' === trim( $subject ) ) {
				$subject = __( '(ohne Betreff)', 'kurato' );
			}

			echo '<tr>';
			echo '<td><strong><a href="' . esc_url( self::detail_url( $id ) ) . '">' . esc_html( $subject ) . '</a></strong></td>';
			echo '<td>' . $this->status_badge( (string) $campaign['status'] ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Badge intern escaped.
			echo '<td>' . esc_html( $this->progress_text( $id, $campaign ) ) . '</td>';
			echo '<td>' . esc_html( $this->format_date( (string) $campaign['created_at'] ) ) . '</td>';
			echo '</tr>';
		}

		echo '</tbody></table>';
	}

	// --- Detailseite -------------------------------------------------------

	/**
	 * Rendert die Detail-/Einstellungsseite einer Kampagne.
	 *
	 * @return void
	 */
	public function render_detail(): void {
		if ( ! current_user_can( 'hs_view_contacts' ) ) {
			wp_die( esc_html__( 'Sie haben keine Berechtigung, Kampagnen einzusehen.', 'kurato' ) );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nur Auswahl der angezeigten Kampagne.
		$id       = isset( $_GET['campaign'] ) ? absint( wp_unslash( $_GET['campaign'] ) ) : 0;
		$campaign = $this->campaigns->get( $id );

		echo '<div class="wrap hs-kurato-campaign">';

		if ( null === $campaign ) {
			echo '<h1>' . esc_html__( 'Kampagne', 'kurato' ) . '</h1>';
			echo '<div class="notice notice-error"><p>' . esc_html__( 'Diese Kampagne wurde nicht gefunden.', 'kurato' ) . '</p></div>';
			echo '<p><a href="' . esc_url( admin_url( 'admin.php?page=' . self::SLUG ) ) . '">' . esc_html__( 'Zurueck zur Uebersicht', 'kurato' ) . '</a></p>';
			echo '</div>';
			return;
		}

		$post_id = (int) $campaign['post_id'];
		$subject = (string) get_the_title( $post_id );
		if ( '' === trim( $subject ) ) {
			$subject = __( '(ohne Betreff)', 'kurato' );
		}
		$can_send = current_user_can( 'hs_send_mails' );

		echo '<h1 class="wp-heading-inline">' . esc_html( $subject ) . '</h1>';
		echo ' ' . $this->status_badge( (string) $campaign['status'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Badge intern escaped.
		echo '<hr class="wp-header-end" />';

		echo '<p><a href="' . esc_url( admin_url( 'admin.php?page=' . self::SLUG ) ) . '">&laquo; ' . esc_html__( 'Zurueck zur Uebersicht', 'kurato' ) . '</a></p>';

		$this->render_notice();

		// Inhalt bearbeiten.
		$edit_link = get_edit_post_link( $post_id, 'url' );
		echo '<h2>' . esc_html__( 'Inhalt', 'kurato' ) . '</h2>';
		echo '<p>' . esc_html__( 'Betreff und Inhalt der Mail schreiben Sie im Editor. Der Betreff ist der Titel.', 'kurato' ) . '</p>';
		if ( $can_send && $edit_link ) {
			echo '<p><a class="button" href="' . esc_url( $edit_link ) . '">' . esc_html__( 'Inhalt bearbeiten', 'kurato' ) . '</a></p>';
		}

		$this->render_progress_section( $id, $campaign );
		$this->render_statistics_section( $id, $campaign );
		$this->render_audience_section( $id, $campaign, $can_send );
		$this->render_dns_section();
		$this->render_test_section( $id, $can_send );
		$this->render_control_section( $id, $campaign, $can_send );

		echo '</div>';
	}

	/**
	 * Fortschrittsanzeige (nur, sobald materialisiert).
	 *
	 * @param int                  $id       Kampagnen-ID.
	 * @param array<string, mixed> $campaign Kampagne.
	 * @return void
	 */
	private function render_progress_section( int $id, array $campaign ): void {
		$status = (string) $campaign['status'];
		if ( ! in_array( $status, array( 'sending', 'paused', 'sent', 'aborted' ), true ) ) {
			return;
		}
		$counts = $this->queue->counts_for_campaign( $id );
		$total  = array_sum( $counts );
		if ( 0 === $total ) {
			return;
		}
		$done = (int) $counts['sent'] + (int) $counts['failed'] + (int) $counts['skipped'];

		echo '<h2>' . esc_html__( 'Fortschritt', 'kurato' ) . '</h2>';
		echo '<p>';
		printf(
			/* translators: 1: erledigte Empfaenger, 2: Gesamtzahl */
			esc_html__( '%1$d von %2$d Empfaengern verarbeitet.', 'kurato' ),
			(int) $done,
			(int) $total
		);
		echo '</p>';
		echo '<ul class="hs-muted" style="margin-left:16px;list-style:disc;">';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Gesendet: %d', 'kurato' ), (int) $counts['sent'] ) ) . '</li>';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Ausstehend: %d', 'kurato' ), (int) $counts['pending'] + (int) $counts['sending'] ) ) . '</li>';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Fehlgeschlagen: %d', 'kurato' ), (int) $counts['failed'] ) ) . '</li>';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Uebersprungen: %d', 'kurato' ), (int) $counts['skipped'] ) ) . '</li>';
		echo '</ul>';
	}

	/**
	 * Statistik-Abschnitt (ab Status sending, K4e-2). Server-Ereignisse
	 * (Abmeldungen, Bounces, Beschwerden) immer; Oeffnungen/Klicks nur bei
	 * aktivem Site-Tracking und dann mit ehrlicher Bezugsgroesse "N Empfaenger
	 * mit Tracking-Einwilligung" (keine Prozent-Schoenrechnung auf die
	 * Gesamtempfaengerzahl).
	 *
	 * @param int                  $id       Kampagnen-ID.
	 * @param array<string, mixed> $campaign Kampagne.
	 * @return void
	 */
	private function render_statistics_section( int $id, array $campaign ): void {
		if ( ! in_array( (string) $campaign['status'], array( 'sending', 'paused', 'sent', 'aborted' ), true ) ) {
			return;
		}

		echo '<h2>' . esc_html__( 'Statistik', 'kurato' ) . '</h2>';

		$tracking_on = (bool) $this->settings()->get( 'tracking.enabled', false );
		$stats       = CampaignStats::summarize(
			$this->queue->counts_for_campaign( $id ),
			$this->events->counts_for_campaign( $id ),
			$tracking_on,
			$tracking_on ? $this->queue->tracking_consented_count( $id ) : 0
		);

		// Server-Ereignisse bleiben im kostenlosen Umfang (Projektlead-Korrektur zu
		// K-P1): Abmeldungen, Bounces und Beschwerden sind Zustellbarkeit, nicht
		// Marketing-Auswertung. Wer nicht sehen darf, dass seine Liste hart bounct,
		// kann seinen Absender-Ruf nicht schuetzen - das gehoert nie hinter eine
		// Kaufschranke, genau wie die DSGVO-Werkzeuge.
		$this->render_statistics_events( $stats );

		// Oeffnungen und Klicks (die eigentliche Auswertung) gehoeren zu Kurato Pro.
		// Ohne Lizenz bleibt die Flaeche sichtbar und zeigt als solche
		// gekennzeichnete Beispieldaten; es wird keine echte Zahl verwischt.
		if ( ! ProArea::enabled() ) {
			echo ProArea::frosted( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Flaeche intern escaped.
				ProArea::CONTEXT_CAMPAIGN_STATS,
				__( 'Sehen, was diese Kampagne bewirkt hat', 'kurato' ),
				__( 'Kurato Pro weist Oeffnungen und Klicks dieser Kampagne mit ehrlicher Bezugsgroesse aus - nur Empfaenger mit Tracking-Einwilligung, ohne Prozent-Schoenrechnung.', 'kurato' ),
				$this->sample_statistics_html()
			);
			return;
		}

		$this->render_statistics_tracking( $stats );
	}

	/**
	 * Server-Ereignisse der Kampagne: Abmeldungen, Bounces, Beschwerden. Immer
	 * sichtbar, auch ohne Lizenz.
	 *
	 * @param array<string, mixed> $stats Zusammenfassung aus CampaignStats.
	 * @return void
	 */
	private function render_statistics_events( array $stats ): void {
		echo '<ul class="hs-muted" style="margin-left:16px;list-style:disc;">';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Abmeldungen: %d', 'kurato' ), (int) $stats['unsubscribes'] ) ) . '</li>';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Bounces (unzustellbar): %d', 'kurato' ), (int) $stats['bounces'] ) ) . '</li>';
		echo '<li>' . esc_html( sprintf( /* translators: %d: Anzahl */ __( 'Beschwerden: %d', 'kurato' ), (int) $stats['complaints'] ) ) . '</li>';
		echo '</ul>';
	}

	/**
	 * Oeffnungen und Klicks (Pro). Getrennt, damit die verschlossene Flaeche
	 * dieselbe Darstellung mit Beispielwerten nutzen kann.
	 *
	 * @param array<string, mixed> $stats Zusammenfassung aus CampaignStats.
	 * @return void
	 */
	private function render_statistics_tracking( array $stats ): void {
		if ( ! $stats['tracking'] ) {
			echo '<p class="description">' . esc_html__( 'Oeffnungen und Klicks werden nicht gemessen, weil das Tracking in den Versand-Einstellungen ausgeschaltet ist.', 'kurato' ) . '</p>';
			return;
		}

		if ( 0 === (int) $stats['consented'] ) {
			echo '<p class="description">' . esc_html__( 'Kein Empfaenger dieser Kampagne hat der Messung von Oeffnungen und Klicks zugestimmt. Oeffnungs- und Klickzahlen werden daher nicht ausgewiesen.', 'kurato' ) . '</p>';
			return;
		}

		echo '<ul class="hs-muted" style="margin-left:16px;list-style:disc;">';
		echo '<li>' . esc_html(
			sprintf(
				/* translators: 1: Oeffnungen, 2: Empfaenger mit Einwilligung */
				__( '%1$d Oeffnungen von %2$d Empfaengern mit Tracking-Einwilligung', 'kurato' ),
				(int) $stats['opens'],
				(int) $stats['consented']
			)
		) . '</li>';
		echo '<li>' . esc_html(
			sprintf(
				/* translators: 1: Klicks, 2: Empfaenger mit Einwilligung */
				__( '%1$d Klicks von %2$d Empfaengern mit Tracking-Einwilligung', 'kurato' ),
				(int) $stats['clicks'],
				(int) $stats['consented']
			)
		) . '</li>';
		echo '</ul>';
		echo '<p class="description">' . esc_html__( 'Bezugsgroesse ist bewusst nur die Zahl der Empfaenger mit Tracking-Einwilligung, nicht die Gesamtzahl der Empfaenger.', 'kurato' ) . '</p>';
	}

	/**
	 * Beispiel-Statistik fuer die verschlossene Flaeche: erfundene Rohwerte,
	 * absichtlich durch denselben Summierer und dieselbe Darstellung wie die
	 * echten Zahlen. Es wird nichts aus der Datenbank gelesen.
	 *
	 * @return string
	 */
	private function sample_statistics_html(): string {
		$stats = CampaignStats::summarize(
			array( 'sent' => 412, 'failed' => 3, 'skipped' => 7 ),
			array( 'unsubscribe' => 4, 'bounce_hard' => 2, 'bounce_soft' => 1, 'complaint' => 0, 'open' => 168, 'click' => 39 ),
			true,
			260
		);

		ob_start();
		$this->render_statistics_tracking( $stats );

		return (string) ob_get_clean();
	}

	/**
	 * Empfaenger-Auswahl (Ziel- und Ausschluss-Listen) plus Vorschau-Zahl.
	 *
	 * @param int                  $id       Kampagnen-ID.
	 * @param array<string, mixed> $campaign Kampagne.
	 * @param bool                 $can_send Darf der Nutzer aendern?
	 * @return void
	 */
	private function render_audience_section( int $id, array $campaign, bool $can_send ): void {
		$audience  = $this->decode_audience( (string) $campaign['audience'] );
		$lists     = $this->lists->all();
		$segments  = $this->segments->all();
		$editable  = $can_send && $this->is_editable( (string) $campaign['status'] );

		echo '<h2>' . esc_html__( 'Empfaenger', 'kurato' ) . '</h2>';

		if ( array() === $lists && array() === $segments ) {
			echo '<p>' . esc_html__( 'Es gibt noch keine Listen. Legen Sie zuerst unter "Listen" eine an.', 'kurato' ) . '</p>';
			return;
		}

		$segment_matches = $this->segment_contact_ids( $audience['segments'] );
		$preview         = AudienceSelector::preview_count( $this->subscriptions, $audience['lists'], $audience['exclude_lists'], $segment_matches );
		echo '<p><strong>' . esc_html( sprintf( /* translators: %s: Zahl */ __( 'Voraussichtlich %s Empfaenger', 'kurato' ), number_format_i18n( $preview ) ) ) . '</strong> ';
		echo '<span class="hs-muted">' . esc_html__( '(bestaetigte Abos der gewaehlten Listen und Segmente, Ausschluesse abgezogen)', 'kurato' ) . '</span></p>';

		if ( ! $editable ) {
			$this->render_audience_readonly( $lists, $segments, $audience );
			return;
		}

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_AUDIENCE ) . '" />';
		echo '<input type="hidden" name="campaign" value="' . esc_attr( (string) $id ) . '" />';
		wp_nonce_field( self::ACTION_AUDIENCE . '_' . $id );

		echo '<table class="form-table"><tbody>';

		if ( array() !== $lists ) {
			echo '<tr><th scope="row">' . esc_html__( 'Listen', 'kurato' ) . '</th><td><fieldset>';
			foreach ( $lists as $list ) {
				$lid     = (int) $list['id'];
				$checked = in_array( $lid, $audience['lists'], true );
				echo '<label style="display:block;margin-bottom:4px;"><input type="checkbox" name="lists[]" value="' . esc_attr( (string) $lid ) . '"' . checked( $checked, true, false ) . ' /> ' . esc_html( (string) $list['label'] ) . '</label>';
			}
			echo '</fieldset></td></tr>';
		}

		if ( array() !== $segments ) {
			echo '<tr><th scope="row">' . esc_html__( 'Segmente', 'kurato' ) . '</th><td><fieldset>';
			echo '<p class="description">' . esc_html__( 'Kontakte, die zusaetzlich zu den Listen einem Segment entsprechen und irgendwo bestaetigt abonniert sind.', 'kurato' ) . '</p>';
			foreach ( $segments as $segment ) {
				$sid     = (int) $segment['id'];
				$checked = in_array( $sid, $audience['segments'], true );
				echo '<label style="display:block;margin-bottom:4px;"><input type="checkbox" name="segments[]" value="' . esc_attr( (string) $sid ) . '"' . checked( $checked, true, false ) . ' /> ' . esc_html( (string) $segment['label'] ) . '</label>';
			}
			echo '</fieldset></td></tr>';
		}

		if ( array() !== $lists ) {
			echo '<tr><th scope="row">' . esc_html__( 'Ausschluss-Listen', 'kurato' ) . '</th><td><fieldset>';
			echo '<p class="description">' . esc_html__( 'Wer in einer dieser Listen bestaetigt ist, erhaelt die Mail nicht.', 'kurato' ) . '</p>';
			foreach ( $lists as $list ) {
				$lid     = (int) $list['id'];
				$checked = in_array( $lid, $audience['exclude_lists'], true );
				echo '<label style="display:block;margin-bottom:4px;"><input type="checkbox" name="exclude_lists[]" value="' . esc_attr( (string) $lid ) . '"' . checked( $checked, true, false ) . ' /> ' . esc_html( (string) $list['label'] ) . '</label>';
			}
			echo '</fieldset></td></tr>';
		}
		echo '</tbody></table>';

		echo '<p><button type="submit" class="button button-primary">' . esc_html__( 'Empfaenger speichern', 'kurato' ) . '</button></p>';
		echo '</form>';
	}

	/**
	 * Nur-Lese-Anzeige der gewaehlten Listen und Segmente (Kampagne laeuft
	 * bereits/ist fertig).
	 *
	 * @param array<int, array<string, mixed>>                                            $lists    Listen.
	 * @param array<int, array<string, mixed>>                                            $segments Segmente.
	 * @param array{lists:array<int,int>,exclude_lists:array<int,int>,segments:array<int,int>} $audience Auswahl.
	 * @return void
	 */
	private function render_audience_readonly( array $lists, array $segments, array $audience ): void {
		$list_labels = array();
		foreach ( $lists as $list ) {
			$list_labels[ (int) $list['id'] ] = (string) $list['label'];
		}
		$names = array();
		foreach ( $audience['lists'] as $lid ) {
			if ( isset( $list_labels[ $lid ] ) ) {
				$names[] = $list_labels[ $lid ];
			}
		}
		echo '<p>' . esc_html__( 'Gewaehlte Listen:', 'kurato' ) . ' ' . esc_html( array() === $names ? __( 'keine', 'kurato' ) : implode( ', ', $names ) ) . '</p>';

		$seg_labels = array();
		foreach ( $segments as $segment ) {
			$seg_labels[ (int) $segment['id'] ] = (string) $segment['label'];
		}
		$seg_names = array();
		foreach ( $audience['segments'] as $sid ) {
			if ( isset( $seg_labels[ $sid ] ) ) {
				$seg_names[] = $seg_labels[ $sid ];
			}
		}
		echo '<p>' . esc_html__( 'Gewaehlte Segmente:', 'kurato' ) . ' ' . esc_html( array() === $seg_names ? __( 'keine', 'kurato' ) : implode( ', ', $seg_names ) ) . '</p>';
	}

	/**
	 * Loest die gewaehlten Segmente zu Kontakt-IDs auf und schneidet sie mit den
	 * irgendwo bestaetigten Abonnenten (gleiche Semantik wie der Materializer,
	 * fuer die Vorschau-Zahl).
	 *
	 * @param array<int, int> $segment_ids Segment-IDs.
	 * @return array<int, int> Kontakt-IDs.
	 */
	private function segment_contact_ids( array $segment_ids ): array {
		if ( array() === $segment_ids ) {
			return array();
		}
		$matches = array();
		foreach ( $segment_ids as $segment_id ) {
			$segment = $this->segments->get( (int) $segment_id );
			if ( null === $segment ) {
				continue;
			}
			foreach ( $this->segment_query->contact_ids( $segment['definition'] ) as $contact_id ) {
				$matches[ (int) $contact_id ] = (int) $contact_id;
			}
		}
		if ( array() === $matches ) {
			return array();
		}
		$confirmed_any = array();
		foreach ( $this->subscriptions->confirmed_contact_ids_any() as $contact_id ) {
			$confirmed_any[ (int) $contact_id ] = true;
		}
		return array_values( array_intersect_key( $matches, $confirmed_any ) );
	}

	/**
	 * Zustellbarkeits-Ampel fuer die Absender-Domain (SPF/DKIM/DMARC).
	 *
	 * @return void
	 */
	private function render_dns_section(): void {
		$settings  = $this->settings();
		$from       = (string) $settings->get( 'mail.sender.from_email', '' );
		$transport = (string) $settings->get( 'mail.transport', 'wp_mail' );

		echo '<h2>' . esc_html__( 'Zustellbarkeit (SPF, DKIM, DMARC)', 'kurato' ) . '</h2>';

		if ( '' === $from || false === strpos( $from, '@' ) ) {
			echo '<p class="notice notice-warning" style="padding:8px 12px;">' . esc_html__( 'Es ist noch keine Absender-Adresse hinterlegt. Bitte tragen Sie sie in den Versand-Einstellungen ein.', 'kurato' ) . '</p>';
			return;
		}

		$domain = strtolower( substr( strrchr( $from, '@' ), 1 ) );
		$check  = $this->cached_dns_check( $domain, $transport );

		echo '<table class="wp-list-table widefat striped" style="max-width:720px;"><tbody>';
		foreach ( array( 'spf' => 'SPF', 'dkim' => 'DKIM', 'dmarc' => 'DMARC' ) as $key => $label ) {
			$item = $check[ $key ];
			echo '<tr><td style="width:80px;"><strong>' . esc_html( $label ) . '</strong></td>';
			echo '<td>' . $this->dns_badge( (string) $item['status'] ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Badge intern escaped.
			echo '<td>' . esc_html( (string) $item['message'] ) . '</td></tr>';
		}
		echo '</tbody></table>';

		if ( ! $check['can_send'] ) {
			echo '<p class="notice notice-error" style="padding:8px 12px;">' . esc_html__( 'Der Kampagnenversand ist gesperrt, bis SPF und DKIM stimmen. Testmails bleiben moeglich.', 'kurato' ) . '</p>';
		}
	}

	/**
	 * Testversand an die eigene Adresse.
	 *
	 * @param int  $id       Kampagnen-ID.
	 * @param bool $can_send Darf der Nutzer senden?
	 * @return void
	 */
	private function render_test_section( int $id, bool $can_send ): void {
		if ( ! $can_send ) {
			return;
		}
		$to = $this->current_user_email();

		echo '<h2>' . esc_html__( 'Testversand', 'kurato' ) . '</h2>';
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_TEST ) . '" />';
		echo '<input type="hidden" name="campaign" value="' . esc_attr( (string) $id ) . '" />';
		wp_nonce_field( self::ACTION_TEST . '_' . $id );
		echo '<p><label>' . esc_html__( 'Empfaenger', 'kurato' ) . ' <input type="email" name="test_to" value="' . esc_attr( $to ) . '" class="regular-text" required /></label> ';
		echo '<button type="submit" class="button">' . esc_html__( 'Testmail senden', 'kurato' ) . '</button></p>';
		echo '<p class="description">' . esc_html__( 'Sendet eine Vorschau dieser Kampagne ueber den aktuell konfigurierten Versandweg.', 'kurato' ) . '</p>';
		echo '</form>';
	}

	/**
	 * Start / Pause / Fortsetzen / Abbrechen.
	 *
	 * @param int                  $id       Kampagnen-ID.
	 * @param array<string, mixed> $campaign Kampagne.
	 * @param bool                 $can_send Darf der Nutzer senden?
	 * @return void
	 */
	private function render_control_section( int $id, array $campaign, bool $can_send ): void {
		if ( ! $can_send ) {
			return;
		}
		$status = (string) $campaign['status'];

		echo '<h2>' . esc_html__( 'Versand', 'kurato' ) . '</h2>';

		if ( in_array( $status, array( 'draft', 'scheduled' ), true ) ) {
			echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" onsubmit="return confirm(\'' . esc_js( __( 'Versand jetzt starten?', 'kurato' ) ) . '\');">';
			echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_START ) . '" />';
			echo '<input type="hidden" name="campaign" value="' . esc_attr( (string) $id ) . '" />';
			wp_nonce_field( self::ACTION_START . '_' . $id );
			echo '<p><button type="submit" class="button button-primary">' . esc_html__( 'Kampagne starten', 'kurato' ) . '</button></p>';
			echo '</form>';
			return;
		}

		if ( 'sending' === $status ) {
			$this->control_buttons( $id, array(
				'pause' => __( 'Versand pausieren', 'kurato' ),
				'abort' => __( 'Versand abbrechen', 'kurato' ),
			) );
			return;
		}

		if ( 'paused' === $status ) {
			$this->control_buttons( $id, array(
				'resume' => __( 'Versand fortsetzen', 'kurato' ),
				'abort'  => __( 'Versand abbrechen', 'kurato' ),
			) );
			return;
		}

		if ( 'sent' === $status ) {
			echo '<p>' . esc_html__( 'Diese Kampagne wurde vollstaendig versendet.', 'kurato' ) . '</p>';
			return;
		}

		echo '<p>' . esc_html__( 'Diese Kampagne wurde abgebrochen.', 'kurato' ) . '</p>';
	}

	/**
	 * Rendert eine Reihe Status-Buttons (op => Label).
	 *
	 * @param int                   $id  Kampagnen-ID.
	 * @param array<string, string> $ops Operationen.
	 * @return void
	 */
	private function control_buttons( int $id, array $ops ): void {
		echo '<p>';
		foreach ( $ops as $op => $label ) {
			$confirm = 'abort' === $op ? ' onsubmit="return confirm(\'' . esc_js( __( 'Versand wirklich abbrechen? Das laesst sich nicht rueckgaengig machen.', 'kurato' ) ) . '\');"' : '';
			echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline-block;margin-right:8px;"' . $confirm . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- statischer, escapeter confirm-Handler.
			echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_STATUS ) . '" />';
			echo '<input type="hidden" name="campaign" value="' . esc_attr( (string) $id ) . '" />';
			echo '<input type="hidden" name="op" value="' . esc_attr( $op ) . '" />';
			wp_nonce_field( self::ACTION_STATUS . '_' . $id );
			echo '<button type="submit" class="button">' . esc_html( $label ) . '</button>';
			echo '</form>';
		}
		echo '</p>';
	}

	// --- Handler -----------------------------------------------------------

	/**
	 * Legt eine neue Kampagne an: CPT-Entwurf plus Zustandssatz, dann in den
	 * Block-Editor des Inhalts.
	 *
	 * @return void
	 */
	public function handle_create(): void {
		$this->guard_send();
		check_admin_referer( self::ACTION_CREATE );

		$post_id = wp_insert_post(
			array(
				'post_type'   => MailPostType::POST_TYPE,
				'post_status' => 'draft',
				'post_title'  => __( 'Newsletter-Entwurf', 'kurato' ),
			),
			true
		);

		if ( is_wp_error( $post_id ) || 0 === (int) $post_id ) {
			$this->redirect_list( 'create_failed' );
		}

		$this->campaigns->create( (int) $post_id );

		$edit_link = get_edit_post_link( (int) $post_id, 'url' );
		if ( ! $edit_link ) {
			$edit_link = admin_url( 'admin.php?page=' . self::SLUG );
		}
		wp_safe_redirect( $edit_link );
		exit;
	}

	/**
	 * Speichert die Empfaengerauswahl.
	 *
	 * @return void
	 */
	public function handle_save_audience(): void {
		$this->guard_send();
		$id = $this->posted_campaign_id();
		check_admin_referer( self::ACTION_AUDIENCE . '_' . $id );

		$campaign = $this->campaigns->get( $id );
		if ( null === $campaign || ! $this->is_editable( (string) $campaign['status'] ) ) {
			$this->redirect_detail( $id, 'locked' );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$raw_lists = isset( $_POST['lists'] ) ? wp_unslash( $_POST['lists'] ) : array();
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$raw_excl = isset( $_POST['exclude_lists'] ) ? wp_unslash( $_POST['exclude_lists'] ) : array();
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$raw_segments = isset( $_POST['segments'] ) ? wp_unslash( $_POST['segments'] ) : array();

		$audience = AudienceSelector::sanitize_audience( $raw_lists, $raw_excl, $raw_segments );
		$this->campaigns->update( $id, array( 'audience' => $audience ) );

		$this->redirect_detail( $id, 'audience_saved' );
	}

	/**
	 * Sendet eine Vorschau der Kampagne an eine (eigene) Adresse.
	 *
	 * @return void
	 */
	public function handle_test(): void {
		$this->guard_send();
		$id = $this->posted_campaign_id();
		check_admin_referer( self::ACTION_TEST . '_' . $id );

		$campaign = $this->campaigns->get( $id );
		if ( null === $campaign ) {
			$this->redirect_detail( $id, 'not_found' );
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$to = sanitize_email( wp_unslash( $_POST['test_to'] ?? '' ) );
		if ( '' === $to ) {
			$to = $this->current_user_email();
		}
		if ( '' === $to ) {
			$this->redirect_detail( $id, 'test_failed' );
		}

		$message = $this->build_preview_message( (int) $campaign['post_id'], $to );
		$ok      = ( new DriverFactory() )->current()->send( $message );

		$this->redirect_detail( $id, $ok ? 'test_sent' : 'test_failed' );
	}

	/**
	 * Startet den Versand: DNS-Block, dann Materialisierung.
	 *
	 * @return void
	 */
	public function handle_start(): void {
		$this->guard_send();
		$id = $this->posted_campaign_id();
		check_admin_referer( self::ACTION_START . '_' . $id );

		$campaign = $this->campaigns->get( $id );
		if ( null === $campaign ) {
			$this->redirect_detail( $id, 'not_found' );
		}
		if ( ! in_array( (string) $campaign['status'], array( 'draft', 'scheduled' ), true ) ) {
			$this->redirect_detail( $id, 'cannot_start' );
		}

		$settings   = $this->settings();
		$from       = (string) $settings->get( 'mail.sender.from_email', '' );
		if ( '' === $from || false === strpos( $from, '@' ) ) {
			$this->redirect_detail( $id, 'no_sender' );
		}
		$domain    = strtolower( substr( strrchr( $from, '@' ), 1 ) );
		$transport = (string) $settings->get( 'mail.transport', 'wp_mail' );

		$service = new DnsCheckService( (bool) $settings->get( 'dns.doh_enabled', false ) );
		$check   = $service->check( $domain, $transport );
		if ( empty( $check['can_send'] ) ) {
			$this->redirect_detail( $id, 'dns_blocked' );
		}

		$count = $this->materializer->materialize( $id );
		$this->redirect_detail( $id, $count > 0 ? 'started' : 'empty', array( 'count' => $count ) );
	}

	/**
	 * Pause / Fortsetzen / Abbrechen.
	 *
	 * @return void
	 */
	public function handle_status(): void {
		$this->guard_send();
		$id = $this->posted_campaign_id();
		check_admin_referer( self::ACTION_STATUS . '_' . $id );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$op       = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : '';
		$campaign = $this->campaigns->get( $id );
		if ( null === $campaign ) {
			$this->redirect_detail( $id, 'not_found' );
		}

		$status = (string) $campaign['status'];
		$msg    = 'status_invalid';

		if ( 'pause' === $op && 'sending' === $status ) {
			$this->campaigns->set_status( $id, 'paused' );
			$msg = 'paused';
		} elseif ( 'resume' === $op && 'paused' === $status ) {
			$this->campaigns->set_status( $id, 'sending' );
			$msg = 'resumed';
		} elseif ( 'abort' === $op && in_array( $status, array( 'sending', 'paused' ), true ) ) {
			$this->campaigns->set_status( $id, 'aborted' );
			$msg = 'aborted';
		}

		$this->redirect_detail( $id, $msg );
	}

	// --- Helfer ------------------------------------------------------------

	/**
	 * Baut die Vorschau-Nachricht einer Kampagne (Testversand).
	 *
	 * Rendert Inhalt und Betreff des CPT direkt ueber den EmailRenderer (wie der
	 * CampaignMailBuilder), aber mit einem synthetischen Empfaenger, damit die
	 * Vorschau unabhaengig vom Kampagnenstatus und ohne echten Kontakt funktioniert.
	 *
	 * @param int    $post_id Inhalts-Post.
	 * @param string $to      Ziel-Adresse.
	 * @return Message
	 */
	private function build_preview_message( int $post_id, string $to ): Message {
		$settings   = $this->settings();
		$inner_html = do_blocks( (string) get_post_field( 'post_content', $post_id ) );
		$subject    = (string) get_the_title( $post_id );
		if ( '' === trim( $subject ) ) {
			$subject = __( 'Newsletter-Vorschau', 'kurato' );
		}

		// Synthetischer Abmeldelink (Kontakt 0); der Renderer erzwingt sonst den Footer.
		$link_token      = new LinkToken();
		$unsubscribe_url = add_query_arg( 'token', $link_token->create( 0, 0, LinkToken::PURPOSE_UNSUBSCRIBE ), rest_url( 'kurato/v1/unsubscribe' ) );
		$preferences_url = PreferenceCenter::url( PreferenceCenter::token_for( $link_token, 0 ) );

		$context = array(
			'first_name'      => __( 'Vorschau', 'kurato' ),
			'display_name'    => __( 'Vorschau', 'kurato' ),
			'email'           => $to,
			'unsubscribe_url' => $unsubscribe_url,
			'preferences_url' => $preferences_url,
			'sender_name'     => (string) $settings->get( 'mail.sender.from_name', '' ),
			'sender_address'  => (string) $settings->get( 'mail.sender.address', '' ),
			'subject'         => $subject,
		);

		$body    = ( new EmailRenderer() )->render_html( $inner_html, $context );
		$message = new Message(
			$to,
			/* translators: %s: Betreff */
			sprintf( __( '[Test] %s', 'kurato' ), $subject ),
			$body
		);
		$message->is_html    = true;
		$message->from_name  = (string) $settings->get( 'mail.sender.from_name', '' );
		$message->from_email = (string) $settings->get( 'mail.sender.from_email', '' );
		if ( '' === $message->from_email ) {
			$message->from_email = (string) get_option( 'admin_email', '' );
		}

		return $message;
	}

	/**
	 * Fortschrittstext fuer die Uebersichtstabelle.
	 *
	 * @param int                  $id       Kampagnen-ID.
	 * @param array<string, mixed> $campaign Kampagne.
	 * @return string
	 */
	private function progress_text( int $id, array $campaign ): string {
		if ( 'draft' === (string) $campaign['status'] || 'scheduled' === (string) $campaign['status'] ) {
			return __( 'noch nicht gestartet', 'kurato' );
		}
		$counts = $this->queue->counts_for_campaign( $id );
		$total  = array_sum( $counts );
		if ( 0 === $total ) {
			$total = (int) $campaign['total_recipients'];
		}
		return sprintf(
			/* translators: 1: gesendet, 2: gesamt */
			__( '%1$s / %2$s gesendet', 'kurato' ),
			number_format_i18n( (int) $counts['sent'] ),
			number_format_i18n( (int) $total )
		);
	}

	/**
	 * Dekodiert die audience-Struktur einer Kampagne.
	 *
	 * @param string $json Roh-JSON.
	 * @return array{lists:array<int,int>,exclude_lists:array<int,int>,segments:array<int,int>}
	 */
	private function decode_audience( string $json ): array {
		$data = '' !== $json ? json_decode( $json, true ) : array();
		$data = is_array( $data ) ? $data : array();
		return AudienceSelector::sanitize_audience( $data['lists'] ?? array(), $data['exclude_lists'] ?? array(), $data['segments'] ?? array() );
	}

	/**
	 * Darf die Empfaengerauswahl noch geaendert werden?
	 *
	 * @param string $status Status.
	 * @return bool
	 */
	private function is_editable( string $status ): bool {
		return in_array( $status, array( 'draft', 'scheduled' ), true );
	}

	/**
	 * "Neue Kampagne"-Button (eigenes admin-post-Formular).
	 *
	 * @param string $class CSS-Klassen.
	 * @return string
	 */
	private function create_button( string $class ): string {
		$html  = '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="display:inline;">';
		$html .= '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_CREATE ) . '" />';
		$html .= wp_nonce_field( self::ACTION_CREATE, '_wpnonce', true, false );
		$html .= '<button type="submit" class="' . esc_attr( $class ) . '">' . esc_html__( 'Neue Kampagne', 'kurato' ) . '</button>';
		$html .= '</form>';
		return $html;
	}

	/**
	 * Status-Badge einer Kampagne.
	 *
	 * @param string $status Status.
	 * @return string
	 */
	private function status_badge( string $status ): string {
		$labels = array(
			'draft'     => __( 'Entwurf', 'kurato' ),
			'scheduled' => __( 'Geplant', 'kurato' ),
			'sending'   => __( 'Versand laeuft', 'kurato' ),
			'paused'    => __( 'Pausiert', 'kurato' ),
			'sent'      => __( 'Versendet', 'kurato' ),
			'aborted'   => __( 'Abgebrochen', 'kurato' ),
		);
		$classes = array(
			'draft'     => 'hs-shield--grey',
			'scheduled' => 'hs-shield--grey',
			'sending'   => 'hs-shield--green',
			'paused'    => 'hs-shield--amber',
			'sent'      => 'hs-shield--green',
			'aborted'   => 'hs-shield--amber',
		);
		$label = $labels[ $status ] ?? $status;
		$class = $classes[ $status ] ?? 'hs-shield--grey';
		return '<span class="hs-shield ' . esc_attr( $class ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * DNS-Statusbadge (wie in den Versand-Einstellungen).
	 *
	 * @param string $status Status.
	 * @return string
	 */
	private function dns_badge( string $status ): string {
		$labels = array(
			DnsCheckService::PASS    => __( 'In Ordnung', 'kurato' ),
			DnsCheckService::WARN    => __( 'Hinweis', 'kurato' ),
			DnsCheckService::FAIL    => __( 'Fehlt', 'kurato' ),
			DnsCheckService::UNKNOWN => __( 'Nicht pruefbar', 'kurato' ),
		);
		$classes = array(
			DnsCheckService::PASS    => 'hs-shield--green',
			DnsCheckService::WARN    => 'hs-shield--amber',
			DnsCheckService::FAIL    => 'hs-shield--amber',
			DnsCheckService::UNKNOWN => 'hs-shield--grey',
		);
		$label = $labels[ $status ] ?? $status;
		$class = $classes[ $status ] ?? 'hs-shield--grey';
		return '<span class="hs-shield ' . esc_attr( $class ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * DNS-Check mit 12-Stunden-Cache (teilt den Transient mit den Versand-Einstellungen).
	 *
	 * @param string $domain    Domain.
	 * @param string $transport Transport.
	 * @return array<string, mixed>
	 */
	private function cached_dns_check( string $domain, string $transport ): array {
		$cache = get_transient( self::DNS_TRANSIENT );
		if ( is_array( $cache ) && ( $cache['domain'] ?? '' ) === $domain && ( $cache['transport'] ?? '' ) === $transport ) {
			return $cache['result'];
		}
		$service = new DnsCheckService( (bool) $this->settings()->get( 'dns.doh_enabled', false ) );
		$result  = $service->check( $domain, $transport );
		set_transient( self::DNS_TRANSIENT, array( 'domain' => $domain, 'transport' => $transport, 'result' => $result ), 12 * HOUR_IN_SECONDS );
		return $result;
	}

	/**
	 * Formatiert ein mysql-UTC-Datum fuer die Anzeige.
	 *
	 * @param string $mysql UTC-Datum.
	 * @return string
	 */
	private function format_date( string $mysql ): string {
		if ( '' === $mysql ) {
			return '';
		}
		$ts = strtotime( $mysql . ' UTC' );
		if ( false === $ts ) {
			return $mysql;
		}
		return date_i18n( (string) get_option( 'date_format', 'Y-m-d' ), $ts );
	}

	/**
	 * E-Mail des aktuellen Nutzers.
	 *
	 * @return string
	 */
	private function current_user_email(): string {
		$user = wp_get_current_user();
		$mail = $user instanceof \WP_User ? (string) $user->user_email : '';
		if ( '' === $mail ) {
			$mail = (string) get_option( 'admin_email', '' );
		}
		return $mail;
	}

	/**
	 * Settings-Speicher.
	 *
	 * @return \Hafenstudios\Core\Settings\Settings
	 */
	private function settings() {
		return Core::instance()->settings( 'kurato' );
	}

	/**
	 * Capability-Wache fuer aendernde Aktionen.
	 *
	 * @return void
	 */
	private function guard_send(): void {
		if ( ! current_user_can( 'hs_send_mails' ) ) {
			wp_die( esc_html__( 'Sie haben keine Berechtigung, Kampagnen zu versenden.', 'kurato' ) );
		}
	}

	/**
	 * Liest die Kampagnen-ID aus dem POST.
	 *
	 * @return int
	 */
	private function posted_campaign_id(): int {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- ID wird vor der Nonce-Pruefung nur zur Nonce-Aktion gebildet; die eigentliche Nonce wird direkt danach geprueft.
		return isset( $_POST['campaign'] ) ? absint( wp_unslash( $_POST['campaign'] ) ) : 0;
	}

	/**
	 * Zeigt die Statusmeldung nach einem Redirect.
	 *
	 * @return void
	 */
	private function render_notice(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nur Anzeige nach Redirect.
		$msg = isset( $_GET['msg'] ) ? sanitize_key( wp_unslash( $_GET['msg'] ) ) : '';
		if ( '' === $msg ) {
			return;
		}
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nur Anzeige nach Redirect.
		$count = isset( $_GET['count'] ) ? absint( wp_unslash( $_GET['count'] ) ) : 0;

		if ( 'started' === $msg ) {
			printf(
				'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
				esc_html(
					sprintf(
						/* translators: %s: Anzahl Empfaenger */
						__( '%s Empfaenger, der Versand laeuft.', 'kurato' ),
						number_format_i18n( $count )
					)
				)
			);
			return;
		}

		$map = array(
			'audience_saved' => array( 'success', __( 'Empfaenger gespeichert.', 'kurato' ) ),
			'test_sent'      => array( 'success', __( 'Testmail wurde uebergeben. Bitte pruefen Sie Ihr Postfach.', 'kurato' ) ),
			'test_failed'    => array( 'error', __( 'Die Testmail konnte nicht gesendet werden. Bitte pruefen Sie die Versand-Einstellungen.', 'kurato' ) ),
			'paused'         => array( 'success', __( 'Der Versand ist pausiert.', 'kurato' ) ),
			'resumed'        => array( 'success', __( 'Der Versand wird fortgesetzt.', 'kurato' ) ),
			'aborted'        => array( 'success', __( 'Der Versand wurde abgebrochen.', 'kurato' ) ),
			'empty'          => array( 'warning', __( 'Es gibt keine bestaetigten Empfaenger fuer die gewaehlten Listen.', 'kurato' ) ),
			'dns_blocked'    => array( 'error', __( 'Versand gesperrt bis SPF und DKIM stimmen. Bitte pruefen Sie die Zustellbarkeit.', 'kurato' ) ),
			'no_sender'      => array( 'error', __( 'Es ist keine Absender-Adresse hinterlegt. Bitte tragen Sie sie in den Versand-Einstellungen ein.', 'kurato' ) ),
			'cannot_start'   => array( 'error', __( 'Diese Kampagne kann nicht mehr gestartet werden.', 'kurato' ) ),
			'locked'         => array( 'error', __( 'Die Empfaenger lassen sich nach dem Start nicht mehr aendern.', 'kurato' ) ),
			'status_invalid' => array( 'error', __( 'Diese Aktion ist im aktuellen Status nicht moeglich.', 'kurato' ) ),
			'not_found'      => array( 'error', __( 'Diese Kampagne wurde nicht gefunden.', 'kurato' ) ),
			'create_failed'  => array( 'error', __( 'Die Kampagne konnte nicht angelegt werden.', 'kurato' ) ),
		);
		if ( isset( $map[ $msg ] ) ) {
			printf( '<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>', esc_attr( $map[ $msg ][0] ), esc_html( $map[ $msg ][1] ) );
		}
	}

	/**
	 * Redirect auf die Uebersicht mit Statusmeldung.
	 *
	 * @param string $msg Statuscode.
	 * @return void
	 */
	private function redirect_list( string $msg ): void {
		wp_safe_redirect( add_query_arg( array( 'page' => self::SLUG, 'msg' => $msg ), admin_url( 'admin.php' ) ) );
		exit;
	}

	/**
	 * Redirect auf die Detailseite mit Statusmeldung.
	 *
	 * @param int                  $id    Kampagnen-ID.
	 * @param string               $msg   Statuscode.
	 * @param array<string, mixed> $extra Zusaetzliche Query-Parameter.
	 * @return void
	 */
	private function redirect_detail( int $id, string $msg, array $extra = array() ): void {
		$args = array_merge(
			array( 'page' => self::DETAIL_SLUG, 'campaign' => $id, 'msg' => $msg ),
			$extra
		);
		wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );
		exit;
	}
}
