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

namespace Hafenstudios\Kurato\Admin;

use Hafenstudios\Core\Core;
use Hafenstudios\Kurato\Booking\BookingInput;
use Hafenstudios\Kurato\Booking\BookingLimits;
use Hafenstudios\Kurato\Db\AvailabilityRepository;
use Hafenstudios\Kurato\Db\BookingTypeRepository;
use Hafenstudios\Kurato\Db\HostRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Buchungsseiten-Verwaltung (Etappe K5b, K5-KONZEPT Abschnitt 7).
 *
 * Serverseitig ueber admin-post (kein JS-Framework, Muster ListsPage/CampaignsPage).
 * Die Uebersicht listet die Ansprechpartner (Hosts); die versteckte Detailseite
 * fuehrt je Host durch Stammdaten, oeffentliche URL, Terminarten, Wochenraster und
 * Ausnahmen/Urlaub. Der Free-Umfang (ein Host, zwei Terminarten je Host) sitzt im
 * Filter `kurato_booking_limits` (BookingLimits); ist er erreicht, erscheint eine
 * ruhige Erklaerung plus Kauf-Karte ({@see ProArea}) statt des Anlege-Formulars.
 *
 * Die Grenze wirkt nur beim ANLEGEN: bestehende Ansprechpartner, Terminarten und
 * Buchungen bleiben vollstaendig sichtbar und buchbar, auch wenn eine Pro-Lizenz
 * ausgelaufen ist. Es wird nichts geloescht und nichts abgeschaltet.
 *
 * Rechte: Ansehen und Aendern ueber `hs_manage_contacts` (keine eigene Capability;
 * die Terminverwaltung ist Teil der Kontaktpflege). Jede Aktion ist per Nonce und
 * Capability abgesichert und endet in `wp_safe_redirect` mit Statusmeldung.
 */
final class HostsPage {

	public const SLUG        = 'kurato-hosts';
	public const DETAIL_SLUG = 'kurato-host';

	public const ACTION_SAVE_HOST    = 'kurato_save_host';
	public const ACTION_TOGGLE_HOST  = 'kurato_toggle_host';
	public const ACTION_DELETE_HOST  = 'kurato_delete_host';
	public const ACTION_SAVE_TYPE    = 'kurato_save_booking_type';
	public const ACTION_DELETE_TYPE  = 'kurato_delete_booking_type';
	public const ACTION_ADD_AVAIL    = 'kurato_add_availability';
	public const ACTION_DELETE_AVAIL = 'kurato_delete_availability';

	private const CAP = 'hs_manage_contacts';

	private HostRepository $hosts;

	private BookingTypeRepository $types;

	private AvailabilityRepository $availability;

	/**
	 * @param HostRepository         $hosts        Host-Repository.
	 * @param BookingTypeRepository  $types        Terminarten-Repository.
	 * @param AvailabilityRepository $availability Verfuegbarkeits-Repository.
	 */
	public function __construct( HostRepository $hosts, BookingTypeRepository $types, AvailabilityRepository $availability ) {
		$this->hosts        = $hosts;
		$this->types        = $types;
		$this->availability = $availability;
	}

	/**
	 * Registriert die admin-post-Handler.
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( 'admin_post_' . self::ACTION_SAVE_HOST, array( $this, 'handle_save_host' ) );
		add_action( 'admin_post_' . self::ACTION_TOGGLE_HOST, array( $this, 'handle_toggle_host' ) );
		add_action( 'admin_post_' . self::ACTION_DELETE_HOST, array( $this, 'handle_delete_host' ) );
		add_action( 'admin_post_' . self::ACTION_SAVE_TYPE, array( $this, 'handle_save_type' ) );
		add_action( 'admin_post_' . self::ACTION_DELETE_TYPE, array( $this, 'handle_delete_type' ) );
		add_action( 'admin_post_' . self::ACTION_ADD_AVAIL, array( $this, 'handle_add_availability' ) );
		add_action( 'admin_post_' . self::ACTION_DELETE_AVAIL, array( $this, 'handle_delete_availability' ) );
	}

	/**
	 * URL der Detailseite eines Hosts.
	 *
	 * @param int $host_id Host-ID.
	 * @return string
	 */
	public static function detail_url( int $host_id ): string {
		return admin_url( add_query_arg( array( 'page' => self::DETAIL_SLUG, 'host' => $host_id ), 'admin.php' ) );
	}

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

	/**
	 * Rendert die Host-Uebersicht.
	 *
	 * @return void
	 */
	public function render(): void {
		$this->guard();

		echo '<div class="wrap hs-kurato-hosts">';
		echo '<h1 class="wp-heading-inline">' . esc_html__( 'Buchungsseiten', 'kurato' ) . '</h1>';
		echo '<hr class="wp-header-end" />';

		$this->render_notice();

		$hosts = $this->hosts->all();
		if ( array() === $hosts ) {
			$this->render_empty_state();
			echo '</div>';
			return;
		}

		$this->render_host_table( $hosts );
		$this->render_create_host_form( count( $hosts ) );
		echo '</div>';
	}

	/**
	 * Leerer Zustand mit Hans und einem einzigen Aufruf zur Handlung.
	 *
	 * @return void
	 */
	private function render_empty_state(): 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 Buchungsseite. Legen Sie Ihren ersten Ansprechpartner an und geben Sie ihm eine Terminart.', 'kurato' ) . '</p>';
		echo '</div>';

		$this->render_create_host_form( 0 );
	}

	/**
	 * Host-Tabelle.
	 *
	 * @param array<int, array<string, mixed>> $hosts Hosts.
	 * @return void
	 */
	private function render_host_table( array $hosts ): void {
		echo '<table class="wp-list-table widefat fixed striped">';
		echo '<thead><tr>';
		echo '<th scope="col">' . esc_html__( 'Ansprechpartner', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Oeffentliche URL', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Terminarten', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Status', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Aktion', 'kurato' ) . '</th>';
		echo '</tr></thead><tbody>';

		foreach ( $hosts as $host ) {
			$id   = (int) $host['id'];
			$slug = (string) $host['slug'];

			echo '<tr>';
			echo '<td><strong><a href="' . esc_url( self::detail_url( $id ) ) . '">' . esc_html( (string) $host['label'] ) . '</a></strong>';
			if ( '' !== (string) $host['role'] ) {
				echo '<br /><span class="hs-muted">' . esc_html( (string) $host['role'] ) . '</span>';
			}
			echo '</td>';
			echo '<td><code>' . esc_html( $this->public_url( $slug ) ) . '</code></td>';
			echo '<td>' . esc_html( number_format_i18n( $this->types->count_for_host( $id ) ) ) . '</td>';
			echo '<td>' . $this->active_badge( (bool) $host['active'] ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Badge intern escaped.
			echo '<td>';
			echo '<a href="' . esc_url( self::detail_url( $id ) ) . '">' . esc_html__( 'Bearbeiten', 'kurato' ) . '</a> | ';
			echo $this->toggle_button( $host ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
			echo '</td>';
			echo '</tr>';
		}

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

	/**
	 * Formular zum Anlegen eines Hosts (respektiert das Free-Limit).
	 *
	 * @param int $current_count Zahl vorhandener Hosts.
	 * @return void
	 */
	private function render_create_host_form( int $current_count ): void {
		$limits = BookingLimits::resolve();

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

		if ( ! BookingLimits::can_add_host( $current_count, $limits ) ) {
			// Die Erklaerung bleibt immer stehen (keine Sackgasse); nur die
			// Kauf-Karte darunter laesst sich dauerhaft wegklicken.
			echo '<p class="hs-muted">' . esc_html__( 'Mehr als ein Ansprechpartner gehoert zu Kurato Pro. Ihre bestehenden Ansprechpartner und Termine bleiben unveraendert.', 'kurato' ) . '</p>';
			echo ProArea::card( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Karte intern escaped.
				ProArea::CONTEXT_HOSTS,
				__( 'Mehr Ansprechpartner mit Kurato Pro', 'kurato' ),
				__( 'Mit Kurato Pro legen Sie beliebig viele Ansprechpartner mit eigener Buchungsseite an.', 'kurato' )
			);
			return;
		}

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="hs-kurato-host-form">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_SAVE_HOST ) . '" />';
		wp_nonce_field( self::ACTION_SAVE_HOST );

		echo '<p><label>' . esc_html__( 'Name', 'kurato' ) . '<br /><input type="text" name="label" required class="regular-text" /></label></p>';
		echo '<p><label>' . esc_html__( 'Rolle (optional)', 'kurato' ) . '<br /><input type="text" name="role" class="regular-text" placeholder="' . esc_attr__( 'z. B. Beraterin', 'kurato' ) . '" /></label></p>';
		echo '<p><label>' . esc_html__( 'Adress-Kuerzel fuer die URL (optional, wird sonst aus dem Namen gebildet)', 'kurato' ) . '<br /><input type="text" name="slug" class="regular-text" /></label></p>';
		echo '<p><button type="submit" class="button button-primary">' . esc_html__( 'Ansprechpartner anlegen', 'kurato' ) . '</button></p>';
		echo '</form>';
	}

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

	/**
	 * Rendert die Detailseite eines Hosts.
	 *
	 * @return void
	 */
	public function render_detail(): void {
		$this->guard();

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nur Auswahl des angezeigten Hosts.
		$id   = isset( $_GET['host'] ) ? absint( wp_unslash( $_GET['host'] ) ) : 0;
		$host = $this->hosts->get( $id );

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

		if ( null === $host ) {
			echo '<h1>' . esc_html__( 'Ansprechpartner', 'kurato' ) . '</h1>';
			echo '<div class="notice notice-error"><p>' . esc_html__( 'Dieser Ansprechpartner 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;
		}

		echo '<h1 class="wp-heading-inline">' . esc_html( (string) $host['label'] ) . '</h1> ';
		echo $this->active_badge( (bool) $host['active'] ); // 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();

		$this->render_host_form( $host );
		$this->render_types_section( $host );
		$this->render_schedule_section( $host );

		echo '</div>';
	}

	/**
	 * Stammdaten-Formular des Hosts inklusive oeffentlicher URL.
	 *
	 * @param array<string, mixed> $host Host.
	 * @return void
	 */
	private function render_host_form( array $host ): void {
		$id   = (int) $host['id'];
		$slug = (string) $host['slug'];

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

		echo '<p><label>' . esc_html__( 'Oeffentliche URL', 'kurato' ) . '<br />';
		echo '<input type="text" class="large-text code" readonly value="' . esc_attr( $this->public_url( $slug ) ) . '" onclick="this.select();" /></label>';
		echo '<span class="hs-muted">' . esc_html__( 'Diese Adresse teilen Sie mit Interessenten. Sie wird mit der oeffentlichen Buchungsseite aktiv.', 'kurato' ) . '</span></p>';

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

		echo '<table class="form-table"><tbody>';
		echo '<tr><th scope="row"><label for="hs-host-label">' . esc_html__( 'Name', 'kurato' ) . '</label></th><td><input type="text" id="hs-host-label" name="label" required class="regular-text" value="' . esc_attr( (string) $host['label'] ) . '" /></td></tr>';
		echo '<tr><th scope="row"><label for="hs-host-role">' . esc_html__( 'Rolle', 'kurato' ) . '</label></th><td><input type="text" id="hs-host-role" name="role" class="regular-text" value="' . esc_attr( (string) $host['role'] ) . '" /></td></tr>';
		echo '<tr><th scope="row"><label for="hs-host-slug">' . esc_html__( 'Adress-Kuerzel (Slug)', 'kurato' ) . '</label></th><td><input type="text" id="hs-host-slug" name="slug" class="regular-text" value="' . esc_attr( $slug ) . '" /><p class="description">' . esc_html__( 'Teil der oeffentlichen URL. Muss eindeutig sein; bei Kollision wird eine Ziffer angehaengt.', 'kurato' ) . '</p></td></tr>';

		$photo_id = ! empty( $host['photo_id'] ) ? (int) $host['photo_id'] : 0;
		echo '<tr><th scope="row"><label for="hs-host-photo">' . esc_html__( 'Foto (Medien-ID)', 'kurato' ) . '</label></th><td>';
		echo '<input type="number" id="hs-host-photo" name="photo_id" class="small-text" min="0" value="' . esc_attr( (string) $photo_id ) . '" /> ';
		echo '<span class="description">' . esc_html__( 'ID eines Bildes aus der Mediathek (in der Mediathek unter Details ablesbar).', 'kurato' ) . '</span>';
		if ( $photo_id > 0 && function_exists( 'wp_get_attachment_image' ) ) {
			$img = wp_get_attachment_image( $photo_id, array( 96, 96 ) );
			if ( '' !== $img ) {
				echo '<br />' . $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image liefert escaptes HTML.
			}
		}
		echo '</td></tr>';

		echo '<tr><th scope="row">' . esc_html__( 'Sichtbarkeit', 'kurato' ) . '</th><td><label><input type="checkbox" name="active" value="1"' . checked( (bool) $host['active'], true, false ) . ' /> ' . esc_html__( 'Aktiv (oeffentliche Seite erreichbar)', 'kurato' ) . '</label></td></tr>';
		echo '</tbody></table>';

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

		// Loeschen (getrennt, mit deutlicher Rueckfrage).
		$del = wp_nonce_url(
			add_query_arg(
				array( 'action' => self::ACTION_DELETE_HOST, 'host' => $id ),
				admin_url( 'admin-post.php' )
			),
			self::ACTION_DELETE_HOST . '_' . $id
		);
		echo '<p><a href="' . esc_url( $del ) . '" class="submitdelete" onclick="return confirm(\'' . esc_js( __( 'Diesen Ansprechpartner mit allen Terminarten, Zeiten und Buchungen wirklich loeschen? Das laesst sich nicht rueckgaengig machen.', 'kurato' ) ) . '\');">' . esc_html__( 'Ansprechpartner loeschen', 'kurato' ) . '</a></p>';
	}

	/**
	 * Terminarten des Hosts: Liste und Anlege-Formular (respektiert das Limit).
	 *
	 * @param array<string, mixed> $host Host.
	 * @return void
	 */
	private function render_types_section( array $host ): void {
		$id    = (int) $host['id'];
		$types = $this->types->for_host( $id );

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

		if ( array() === $types ) {
			echo '<p>' . esc_html__( 'Noch keine Terminart. Legen Sie unten die erste an (etwa "Erstgespraech, 30 Minuten").', 'kurato' ) . '</p>';
		} else {
			echo '<table class="wp-list-table widefat fixed striped">';
			echo '<thead><tr>';
			echo '<th scope="col">' . esc_html__( 'Titel', 'kurato' ) . '</th>';
			echo '<th scope="col">' . esc_html__( 'Dauer', 'kurato' ) . '</th>';
			echo '<th scope="col">' . esc_html__( 'Ort', 'kurato' ) . '</th>';
			echo '<th scope="col">' . esc_html__( 'Puffer / Vorlauf / Frist', 'kurato' ) . '</th>';
			echo '<th scope="col">' . esc_html__( 'Status', 'kurato' ) . '</th>';
			echo '<th scope="col">' . esc_html__( 'Aktion', 'kurato' ) . '</th>';
			echo '</tr></thead><tbody>';
			foreach ( $types as $type ) {
				$this->render_type_row( $host, $type );
			}
			echo '</tbody></table>';
		}

		$this->render_type_form( $host, count( $types ) );
	}

	/**
	 * Eine Terminart-Zeile mit ausklappbarem Bearbeiten-Formular.
	 *
	 * @param array<string, mixed> $host Host.
	 * @param array<string, mixed> $type Terminart.
	 * @return void
	 */
	private function render_type_row( array $host, array $type ): void {
		$id   = (int) $type['id'];
		$mins = (int) $type['duration_min'];

		echo '<tr>';
		echo '<td><strong>' . esc_html( (string) $type['label'] ) . '</strong></td>';
		echo '<td>' . esc_html( sprintf( /* translators: %d: Minuten */ __( '%d Min.', 'kurato' ), $mins ) ) . '</td>';
		echo '<td>' . esc_html( $this->location_label( (string) $type['location_type'] ) );
		if ( '' !== (string) $type['location_detail'] ) {
			echo '<br /><span class="hs-muted">' . esc_html( (string) $type['location_detail'] ) . '</span>';
		}
		echo '</td>';
		echo '<td class="hs-muted">' . esc_html(
			sprintf(
				/* translators: 1: Puffer vor, 2: Puffer nach, 3: Vorlauf Stunden, 4: Frist Tage */
				__( 'Puffer %1$d/%2$d Min., Vorlauf %3$d h, Frist %4$d Tage', 'kurato' ),
				(int) $type['buffer_before_min'],
				(int) $type['buffer_after_min'],
				(int) $type['lead_time_hours'],
				(int) $type['horizon_days']
			)
		) . '</td>';
		echo '<td>' . $this->active_badge( (bool) $type['active'] ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Badge intern escaped.
		echo '<td>';
		$del = wp_nonce_url(
			add_query_arg(
				array( 'action' => self::ACTION_DELETE_TYPE, 'host' => (int) $host['id'], 'type' => $id ),
				admin_url( 'admin-post.php' )
			),
			self::ACTION_DELETE_TYPE . '_' . $id
		);
		echo '<a href="' . esc_url( $del ) . '" class="submitdelete" onclick="return confirm(\'' . esc_js( __( 'Diese Terminart wirklich loeschen?', 'kurato' ) ) . '\');">' . esc_html__( 'Loeschen', 'kurato' ) . '</a>';
		echo '</td>';
		echo '</tr>';

		// Bearbeiten-Zeile: per <details> ohne JS aufklappbar, damit die Tabelle kompakt bleibt.
		echo '<tr class="hs-type-edit"><td colspan="6">';
		echo '<details><summary>' . esc_html__( 'Bearbeiten', 'kurato' ) . '</summary>';
		$this->render_type_form( $host, -1, $type );
		echo '</details>';
		echo '</td></tr>';
	}

	/**
	 * Formular zum Anlegen/Bearbeiten einer Terminart.
	 *
	 * @param array<string, mixed>      $host          Host.
	 * @param int                       $current_count Zahl vorhandener Terminarten (fuer das Limit; -1 = Bearbeiten).
	 * @param array<string, mixed>|null $type          Zu bearbeitende Terminart oder null (Anlegen).
	 * @return void
	 */
	private function render_type_form( array $host, int $current_count, ?array $type = null ): void {
		$editing = null !== $type;

		if ( ! $editing ) {
			$limits = BookingLimits::resolve();
			echo '<h3>' . esc_html__( 'Neue Terminart', 'kurato' ) . '</h3>';
			if ( ! BookingLimits::can_add_type( $current_count, $limits ) ) {
				echo '<p class="hs-muted">' . esc_html__( 'Mehr als zwei Terminarten je Ansprechpartner gehoeren zu Kurato Pro. Ihre bestehenden Terminarten und Termine bleiben unveraendert.', 'kurato' ) . '</p>';
				echo ProArea::card( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Karte intern escaped.
					ProArea::CONTEXT_BOOKING_TYPES,
					__( 'Mehr Terminarten mit Kurato Pro', 'kurato' ),
					__( 'Mit Kurato Pro bieten Sie je Ansprechpartner beliebig viele Terminarten zur Buchung an.', 'kurato' )
				);
				return;
			}
		}

		$v = static function ( string $key, $fallback = '' ) use ( $type ) {
			return null !== $type ? $type[ $key ] : $fallback;
		};

		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="hs-kurato-type-form">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_SAVE_TYPE ) . '" />';
		echo '<input type="hidden" name="host" value="' . esc_attr( (string) (int) $host['id'] ) . '" />';
		if ( $editing ) {
			echo '<input type="hidden" name="type" value="' . esc_attr( (string) (int) $type['id'] ) . '" />';
		}
		wp_nonce_field( self::ACTION_SAVE_TYPE );

		echo '<p><label>' . esc_html__( 'Titel', 'kurato' ) . ' <input type="text" name="label" required class="regular-text" value="' . esc_attr( (string) $v( 'label' ) ) . '" /></label></p>';

		echo '<p><label>' . esc_html__( 'Dauer (Minuten)', 'kurato' ) . ' <input type="number" name="duration_min" min="1" max="' . esc_attr( (string) BookingInput::MAX_DURATION_MIN ) . '" class="small-text" value="' . esc_attr( (string) ( $editing ? (int) $v( 'duration_min' ) : 30 ) ) . '" /></label></p>';

		$loc = (string) $v( 'location_type', 'phone' );
		echo '<p><label>' . esc_html__( 'Ort', 'kurato' ) . ' <select name="location_type">';
		foreach ( array( 'phone', 'video', 'in_person' ) as $opt ) {
			echo '<option value="' . esc_attr( $opt ) . '"' . selected( $loc, $opt, false ) . '>' . esc_html( $this->location_label( $opt ) ) . '</option>';
		}
		echo '</select></label> ';
		echo '<label>' . esc_html__( 'Detail (Nummer, Link oder Adresse)', 'kurato' ) . ' <input type="text" name="location_detail" class="regular-text" value="' . esc_attr( (string) $v( 'location_detail' ) ) . '" /></label></p>';

		echo '<p>';
		echo '<label>' . esc_html__( 'Puffer vorher (Min.)', 'kurato' ) . ' <input type="number" name="buffer_before_min" min="0" max="' . esc_attr( (string) BookingInput::MAX_BUFFER_MIN ) . '" class="small-text" value="' . esc_attr( (string) ( $editing ? (int) $v( 'buffer_before_min' ) : 0 ) ) . '" /></label> ';
		echo '<label>' . esc_html__( 'Puffer nachher (Min.)', 'kurato' ) . ' <input type="number" name="buffer_after_min" min="0" max="' . esc_attr( (string) BookingInput::MAX_BUFFER_MIN ) . '" class="small-text" value="' . esc_attr( (string) ( $editing ? (int) $v( 'buffer_after_min' ) : 0 ) ) . '" /></label> ';
		echo '<label>' . esc_html__( 'Vorlauf (Stunden)', 'kurato' ) . ' <input type="number" name="lead_time_hours" min="0" max="' . esc_attr( (string) BookingInput::MAX_LEAD_HOURS ) . '" class="small-text" value="' . esc_attr( (string) ( $editing ? (int) $v( 'lead_time_hours' ) : 0 ) ) . '" /></label> ';
		echo '<label>' . esc_html__( 'Maximalfrist (Tage)', 'kurato' ) . ' <input type="number" name="horizon_days" min="1" max="' . esc_attr( (string) BookingInput::MAX_HORIZON_DAYS ) . '" class="small-text" value="' . esc_attr( (string) ( $editing ? (int) $v( 'horizon_days' ) : 60 ) ) . '" /></label>';
		echo '</p>';

		$active = $editing ? (bool) $v( 'active' ) : true;
		echo '<p><label><input type="checkbox" name="active" value="1"' . checked( $active, true, false ) . ' /> ' . esc_html__( 'Aktiv (buchbar)', 'kurato' ) . '</label></p>';

		echo '<p><button type="submit" class="button' . ( $editing ? '' : ' button-primary' ) . '">' . ( $editing ? esc_html__( 'Terminart speichern', 'kurato' ) : esc_html__( 'Terminart anlegen', 'kurato' ) ) . '</button></p>';
		echo '</form>';
	}

	/**
	 * Wochenraster und Ausnahmen/Urlaub des Hosts.
	 *
	 * @param array<string, mixed> $host Host.
	 * @return void
	 */
	private function render_schedule_section( array $host ): void {
		$id      = (int) $host['id'];
		$rows    = $this->availability->for_host( $id );
		$types   = $this->types->for_host( $id );
		$weekly  = array();
		$excepts = array();
		foreach ( $rows as $row ) {
			if ( 'weekly' === $row['kind'] ) {
				$weekly[] = $row;
			} elseif ( 'exception' === $row['kind'] ) {
				$excepts[] = $row;
			}
		}

		// Terminart-Namen fuer die Anzeige (booking_type_id NULL = alle Arten).
		$type_labels = array();
		foreach ( $types as $type ) {
			$type_labels[ (int) $type['id'] ] = (string) $type['label'];
		}

		echo '<h2>' . esc_html__( 'Wochenraster', 'kurato' ) . '</h2>';
		echo '<p class="description">' . esc_html__( 'Wann ist dieser Ansprechpartner erreichbar? Legen Sie je Wochentag ein oder mehrere Zeitfenster an (Site-Zeitzone).', 'kurato' ) . '</p>';
		$this->render_weekly_table( $weekly, $type_labels, $id );
		$this->render_weekly_form( $id, $types );

		echo '<h2>' . esc_html__( 'Ausnahmen und Urlaub', 'kurato' ) . '</h2>';
		echo '<p class="description">' . esc_html__( 'Zeitraeume, in denen keine Buchung moeglich ist. Ohne Uhrzeit gilt der ganze Tag, mit Uhrzeit nur dieses Fenster je Tag.', 'kurato' ) . '</p>';
		$this->render_exception_table( $excepts, $id );
		$this->render_exception_form( $id );
	}

	/**
	 * Tabelle der Wochenraster-Fenster, nach Wochentag (Montag zuerst).
	 *
	 * @param array<int, array<string, mixed>> $weekly      Wochenraster-Zeilen.
	 * @param array<int, string>               $type_labels Terminart-Namen.
	 * @param int                              $host_id     Host-ID.
	 * @return void
	 */
	private function render_weekly_table( array $weekly, array $type_labels, int $host_id ): void {
		if ( array() === $weekly ) {
			echo '<p>' . esc_html__( 'Noch keine Zeiten hinterlegt.', 'kurato' ) . '</p>';
			return;
		}

		// Nach Wochentag Montag..Sonntag ordnen (Speicher-Konvention 0=Sonntag).
		$order = array( 1, 2, 3, 4, 5, 6, 0 );
		usort(
			$weekly,
			static function ( $a, $b ) use ( $order ) {
				$pa = array_search( (int) $a['weekday'], $order, true );
				$pb = array_search( (int) $b['weekday'], $order, true );
				if ( $pa === $pb ) {
					return strcmp( (string) $a['start_time'], (string) $b['start_time'] );
				}
				return $pa <=> $pb;
			}
		);

		echo '<table class="wp-list-table widefat fixed striped" style="max-width:640px;">';
		echo '<thead><tr>';
		echo '<th scope="col">' . esc_html__( 'Wochentag', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Von', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Bis', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Gilt fuer', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Aktion', 'kurato' ) . '</th>';
		echo '</tr></thead><tbody>';
		foreach ( $weekly as $row ) {
			echo '<tr>';
			echo '<td>' . esc_html( $this->weekday_label( (int) $row['weekday'] ) ) . '</td>';
			echo '<td>' . esc_html( $this->short_time( (string) $row['start_time'] ) ) . '</td>';
			echo '<td>' . esc_html( $this->short_time( (string) $row['end_time'] ) ) . '</td>';
			$tid = $row['booking_type_id'];
			echo '<td>' . esc_html( null === $tid ? __( 'Alle Terminarten', 'kurato' ) : ( $type_labels[ (int) $tid ] ?? __( '(entfernte Terminart)', 'kurato' ) ) ) . '</td>';
			echo '<td>' . $this->delete_avail_link( (int) $row['id'], $host_id ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
			echo '</tr>';
		}
		echo '</tbody></table>';
	}

	/**
	 * Formular fuer ein neues Wochenraster-Fenster.
	 *
	 * @param int                              $host_id Host-ID.
	 * @param array<int, array<string, mixed>> $types   Terminarten (fuer die optionale Zuordnung).
	 * @return void
	 */
	private function render_weekly_form( int $host_id, array $types ): void {
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="hs-kurato-avail-form">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_ADD_AVAIL ) . '" />';
		echo '<input type="hidden" name="kind" value="weekly" />';
		echo '<input type="hidden" name="host" value="' . esc_attr( (string) $host_id ) . '" />';
		wp_nonce_field( self::ACTION_ADD_AVAIL );

		echo '<p>';
		echo '<label>' . esc_html__( 'Wochentag', 'kurato' ) . ' <select name="weekday">';
		foreach ( array( 1, 2, 3, 4, 5, 6, 0 ) as $wd ) {
			echo '<option value="' . esc_attr( (string) $wd ) . '">' . esc_html( $this->weekday_label( $wd ) ) . '</option>';
		}
		echo '</select></label> ';
		echo '<label>' . esc_html__( 'Von', 'kurato' ) . ' <input type="time" name="start_time" required /></label> ';
		echo '<label>' . esc_html__( 'Bis', 'kurato' ) . ' <input type="time" name="end_time" required /></label> ';
		if ( array() !== $types ) {
			echo '<label>' . esc_html__( 'Gilt fuer', 'kurato' ) . ' <select name="booking_type_id">';
			echo '<option value="0">' . esc_html__( 'Alle Terminarten', 'kurato' ) . '</option>';
			foreach ( $types as $type ) {
				echo '<option value="' . esc_attr( (string) (int) $type['id'] ) . '">' . esc_html( (string) $type['label'] ) . '</option>';
			}
			echo '</select></label> ';
		}
		echo '<button type="submit" class="button">' . esc_html__( 'Fenster hinzufuegen', 'kurato' ) . '</button>';
		echo '</p>';
		echo '</form>';
	}

	/**
	 * Tabelle der Ausnahmen/Urlaube.
	 *
	 * @param array<int, array<string, mixed>> $excepts Ausnahme-Zeilen.
	 * @param int                              $host_id Host-ID.
	 * @return void
	 */
	private function render_exception_table( array $excepts, int $host_id ): void {
		if ( array() === $excepts ) {
			echo '<p>' . esc_html__( 'Keine Ausnahmen hinterlegt.', 'kurato' ) . '</p>';
			return;
		}

		echo '<table class="wp-list-table widefat fixed striped" style="max-width:640px;">';
		echo '<thead><tr>';
		echo '<th scope="col">' . esc_html__( 'Von', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Bis', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Uhrzeit', 'kurato' ) . '</th>';
		echo '<th scope="col">' . esc_html__( 'Aktion', 'kurato' ) . '</th>';
		echo '</tr></thead><tbody>';
		foreach ( $excepts as $row ) {
			$start = (string) ( $row['start_time'] ?? '' );
			$end   = (string) ( $row['end_time'] ?? '' );
			echo '<tr>';
			echo '<td>' . esc_html( (string) $row['date_from'] ) . '</td>';
			echo '<td>' . esc_html( (string) $row['date_to'] ) . '</td>';
			echo '<td>' . esc_html( ( '' !== $start && '' !== $end ) ? $this->short_time( $start ) . ' - ' . $this->short_time( $end ) : __( 'Ganztaegig', 'kurato' ) ) . '</td>';
			echo '<td>' . $this->delete_avail_link( (int) $row['id'], $host_id ) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- intern escaped.
			echo '</tr>';
		}
		echo '</tbody></table>';
	}

	/**
	 * Formular fuer eine neue Ausnahme/Urlaub.
	 *
	 * @param int $host_id Host-ID.
	 * @return void
	 */
	private function render_exception_form( int $host_id ): void {
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="hs-kurato-avail-form">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::ACTION_ADD_AVAIL ) . '" />';
		echo '<input type="hidden" name="kind" value="exception" />';
		echo '<input type="hidden" name="host" value="' . esc_attr( (string) $host_id ) . '" />';
		wp_nonce_field( self::ACTION_ADD_AVAIL );

		echo '<p>';
		echo '<label>' . esc_html__( 'Von (Datum)', 'kurato' ) . ' <input type="date" name="date_from" required /></label> ';
		echo '<label>' . esc_html__( 'Bis (Datum)', 'kurato' ) . ' <input type="date" name="date_to" required /></label> ';
		echo '<label>' . esc_html__( 'Von (Uhrzeit, optional)', 'kurato' ) . ' <input type="time" name="start_time" /></label> ';
		echo '<label>' . esc_html__( 'Bis (Uhrzeit, optional)', 'kurato' ) . ' <input type="time" name="end_time" /></label> ';
		echo '<button type="submit" class="button">' . esc_html__( 'Ausnahme hinzufuegen', 'kurato' ) . '</button>';
		echo '</p>';
		echo '</form>';
	}

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

	/**
	 * Legt einen Host an oder aktualisiert ihn.
	 *
	 * @return void
	 */
	public function handle_save_host(): void {
		$this->guard();
		check_admin_referer( self::ACTION_SAVE_HOST );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$id       = isset( $_POST['host'] ) ? absint( wp_unslash( $_POST['host'] ) ) : 0;
		$label    = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '';
		$role     = isset( $_POST['role'] ) ? sanitize_text_field( wp_unslash( $_POST['role'] ) ) : '';
		$slug_raw = isset( $_POST['slug'] ) ? sanitize_title( wp_unslash( $_POST['slug'] ) ) : '';
		$photo_id = isset( $_POST['photo_id'] ) ? absint( wp_unslash( $_POST['photo_id'] ) ) : 0;
		$active   = ! empty( $_POST['active'] );
		// phpcs:enable

		if ( '' === trim( $label ) ) {
			$this->redirect_list( 'host_invalid' );
		}

		if ( $id > 0 ) {
			$host = $this->hosts->get( $id );
			if ( null === $host ) {
				$this->redirect_list( 'not_found' );
			}

			// Slug eindeutig gegen die anderen Hosts halten (BookingInput, testbar).
			$desired = '' !== $slug_raw ? $slug_raw : sanitize_title( $label );
			$slug    = BookingInput::unique_slug( $desired, $this->other_host_slugs( $id ), 'host' );

			$this->hosts->update(
				$id,
				array(
					'label'    => $label,
					'slug'     => $slug,
					'role'     => $role,
					'photo_id' => $photo_id,
					'active'   => $active,
				)
			);
			$this->redirect_detail( $id, 'host_saved' );
		}

		// Anlegen: Free-Limit pruefen.
		$limits = BookingLimits::resolve();
		if ( ! BookingLimits::can_add_host( $this->hosts->count_all(), $limits ) ) {
			$this->redirect_list( 'host_limit' );
		}

		$new_id = $this->hosts->create(
			array(
				'label'    => $label,
				'slug'     => $slug_raw, // Leer => Repository bildet ihn aus dem Namen.
				'role'     => $role,
				'photo_id' => $photo_id,
				'active'   => $active,
			)
		);
		if ( $new_id > 0 ) {
			$this->redirect_detail( $new_id, 'host_saved' );
		}
		$this->redirect_list( 'host_invalid' );
	}

	/**
	 * Schaltet einen Host aktiv/inaktiv.
	 *
	 * @return void
	 */
	public function handle_toggle_host(): void {
		$this->guard();
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- ID nur zur Nonce-Aktion; Nonce direkt danach geprueft.
		$id = isset( $_POST['host'] ) ? absint( wp_unslash( $_POST['host'] ) ) : 0;
		check_admin_referer( self::ACTION_TOGGLE_HOST . '_' . $id );

		$host = $this->hosts->get( $id );
		if ( null !== $host ) {
			$this->hosts->update( $id, array( 'active' => ! (bool) $host['active'] ) );
		}
		$this->redirect_list( 'host_saved' );
	}

	/**
	 * Loescht einen Host samt Terminarten, Zeiten und Buchungen.
	 *
	 * @return void
	 */
	public function handle_delete_host(): void {
		$this->guard();
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- ID nur zur Nonce-Aktion; Nonce direkt danach geprueft.
		$id = isset( $_GET['host'] ) ? absint( wp_unslash( $_GET['host'] ) ) : 0;
		check_admin_referer( self::ACTION_DELETE_HOST . '_' . $id );

		if ( $id > 0 ) {
			$this->hosts->delete( $id );
		}
		$this->redirect_list( 'host_deleted' );
	}

	/**
	 * Legt eine Terminart an oder aktualisiert sie.
	 *
	 * @return void
	 */
	public function handle_save_type(): void {
		$this->guard();
		check_admin_referer( self::ACTION_SAVE_TYPE );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$host_id = isset( $_POST['host'] ) ? absint( wp_unslash( $_POST['host'] ) ) : 0;
		$type_id = isset( $_POST['type'] ) ? absint( wp_unslash( $_POST['type'] ) ) : 0;
		$raw     = array(
			'label'             => isset( $_POST['label'] ) ? wp_unslash( $_POST['label'] ) : '',
			'duration_min'      => $_POST['duration_min'] ?? 0,
			'location_type'     => isset( $_POST['location_type'] ) ? wp_unslash( $_POST['location_type'] ) : 'phone',
			'location_detail'   => isset( $_POST['location_detail'] ) ? wp_unslash( $_POST['location_detail'] ) : '',
			'buffer_before_min' => $_POST['buffer_before_min'] ?? 0,
			'buffer_after_min'  => $_POST['buffer_after_min'] ?? 0,
			'lead_time_hours'   => $_POST['lead_time_hours'] ?? 0,
			'horizon_days'      => $_POST['horizon_days'] ?? 0,
			'active'            => ! empty( $_POST['active'] ),
		);
		// phpcs:enable

		$host = $this->hosts->get( $host_id );
		if ( null === $host ) {
			$this->redirect_list( 'not_found' );
		}

		$clean = BookingInput::sanitize_type( $raw );
		if ( '' === $clean['label'] ) {
			$this->redirect_detail( $host_id, 'type_invalid' );
		}

		if ( $type_id > 0 ) {
			$existing = $this->types->get( $type_id );
			if ( null === $existing || (int) $existing['host_id'] !== $host_id ) {
				$this->redirect_detail( $host_id, 'not_found' );
			}
			$this->types->update( $type_id, $clean );
			$this->redirect_detail( $host_id, 'type_saved' );
		}

		// Anlegen: Free-Limit je Host pruefen.
		$limits = BookingLimits::resolve();
		if ( ! BookingLimits::can_add_type( $this->types->count_for_host( $host_id ), $limits ) ) {
			$this->redirect_detail( $host_id, 'type_limit' );
		}

		$clean['host_id'] = $host_id;
		$this->types->create( $clean );
		$this->redirect_detail( $host_id, 'type_saved' );
	}

	/**
	 * Loescht eine Terminart.
	 *
	 * @return void
	 */
	public function handle_delete_type(): void {
		$this->guard();
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- IDs nur zur Nonce-Aktion; Nonce direkt danach geprueft.
		$host_id = isset( $_GET['host'] ) ? absint( wp_unslash( $_GET['host'] ) ) : 0;
		$type_id = isset( $_GET['type'] ) ? absint( wp_unslash( $_GET['type'] ) ) : 0;
		// phpcs:enable
		check_admin_referer( self::ACTION_DELETE_TYPE . '_' . $type_id );

		$type = $this->types->get( $type_id );
		if ( null !== $type && (int) $type['host_id'] === $host_id ) {
			$this->types->delete( $type_id );
		}
		$this->redirect_detail( $host_id, 'type_deleted' );
	}

	/**
	 * Fuegt ein Wochenraster-Fenster oder eine Ausnahme hinzu.
	 *
	 * @return void
	 */
	public function handle_add_availability(): void {
		$this->guard();
		check_admin_referer( self::ACTION_ADD_AVAIL );

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
		$host_id = isset( $_POST['host'] ) ? absint( wp_unslash( $_POST['host'] ) ) : 0;
		$kind    = isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( $_POST['kind'] ) ) : '';
		$start   = isset( $_POST['start_time'] ) ? sanitize_text_field( wp_unslash( $_POST['start_time'] ) ) : '';
		$end     = isset( $_POST['end_time'] ) ? sanitize_text_field( wp_unslash( $_POST['end_time'] ) ) : '';
		$type_id = isset( $_POST['booking_type_id'] ) ? absint( wp_unslash( $_POST['booking_type_id'] ) ) : 0;
		// phpcs:enable

		$host = $this->hosts->get( $host_id );
		if ( null === $host ) {
			$this->redirect_list( 'not_found' );
		}

		// Eine mitgegebene Terminart muss zu diesem Host gehoeren.
		$assigned = null;
		if ( $type_id > 0 ) {
			$type = $this->types->get( $type_id );
			if ( null !== $type && (int) $type['host_id'] === $host_id ) {
				$assigned = $type_id;
			}
		}

		if ( 'weekly' === $kind ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
			$weekday = isset( $_POST['weekday'] ) ? absint( wp_unslash( $_POST['weekday'] ) ) : -1;
			if ( ! BookingInput::valid_window( $start, $end ) ) {
				$this->redirect_detail( $host_id, 'window_invalid' );
			}
			$id = $this->availability->create_weekly( $host_id, $weekday, $start, $end, $assigned );
			$this->redirect_detail( $host_id, $id > 0 ? 'avail_saved' : 'window_invalid' );
		}

		if ( 'exception' === $kind ) {
			// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce oben geprueft.
			$from = isset( $_POST['date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['date_from'] ) ) : '';
			$to   = isset( $_POST['date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['date_to'] ) ) : '';
			// phpcs:enable
			// Teiltaegige Ausnahme: wenn beide Uhrzeiten gesetzt sind, muss das Fenster gueltig sein.
			if ( '' !== $start && '' !== $end && ! BookingInput::valid_window( $start, $end ) ) {
				$this->redirect_detail( $host_id, 'window_invalid' );
			}
			$id = $this->availability->create_exception( $host_id, $from, $to, $start, $end, $assigned );
			$this->redirect_detail( $host_id, $id > 0 ? 'avail_saved' : 'exception_invalid' );
		}

		$this->redirect_detail( $host_id, 'window_invalid' );
	}

	/**
	 * Loescht ein Wochenraster-Fenster oder eine Ausnahme.
	 *
	 * @return void
	 */
	public function handle_delete_availability(): void {
		$this->guard();
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- IDs nur zur Nonce-Aktion; Nonce direkt danach geprueft.
		$host_id = isset( $_GET['host'] ) ? absint( wp_unslash( $_GET['host'] ) ) : 0;
		$row_id  = isset( $_GET['row'] ) ? absint( wp_unslash( $_GET['row'] ) ) : 0;
		// phpcs:enable
		check_admin_referer( self::ACTION_DELETE_AVAIL . '_' . $row_id );

		// Nur loeschen, wenn die Zeile wirklich zu diesem Host gehoert.
		foreach ( $this->availability->for_host( $host_id ) as $row ) {
			if ( (int) $row['id'] === $row_id ) {
				$this->availability->delete( $row_id );
				break;
			}
		}
		$this->redirect_detail( $host_id, 'avail_deleted' );
	}

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

	/**
	 * Slugs aller Hosts ausser dem gegebenen (fuer die Eindeutigkeit bei Bearbeitung).
	 *
	 * @param int $exclude_id Auszuschliessende Host-ID.
	 * @return array<int, string>
	 */
	private function other_host_slugs( int $exclude_id ): array {
		$out = array();
		foreach ( $this->hosts->all() as $host ) {
			if ( (int) $host['id'] !== $exclude_id ) {
				$out[] = (string) $host['slug'];
			}
		}
		return $out;
	}

	/**
	 * Oeffentliche Buchungs-URL eines Hosts (geht mit K5c live).
	 *
	 * @param string $slug Host-Slug.
	 * @return string
	 */
	private function public_url( string $slug ): string {
		return home_url( '/termin/' . $slug );
	}

	/**
	 * Deutscher Wochentagsname zur Speicher-Konvention (0=Sonntag..6=Samstag).
	 *
	 * @param int $weekday Wochentag.
	 * @return string
	 */
	private function weekday_label( int $weekday ): string {
		$labels = array(
			0 => __( 'Sonntag', 'kurato' ),
			1 => __( 'Montag', 'kurato' ),
			2 => __( 'Dienstag', 'kurato' ),
			3 => __( 'Mittwoch', 'kurato' ),
			4 => __( 'Donnerstag', 'kurato' ),
			5 => __( 'Freitag', 'kurato' ),
			6 => __( 'Samstag', 'kurato' ),
		);
		return $labels[ $weekday ] ?? '';
	}

	/**
	 * Lesbarer Ortstyp.
	 *
	 * @param string $type Ortstyp.
	 * @return string
	 */
	private function location_label( string $type ): string {
		$labels = array(
			'phone'     => __( 'Telefon', 'kurato' ),
			'video'     => __( 'Video', 'kurato' ),
			'in_person' => __( 'Vor Ort', 'kurato' ),
		);
		return $labels[ $type ] ?? $type;
	}

	/**
	 * Kuerzt eine 'H:i:s'-Zeit auf 'H:i'.
	 *
	 * @param string $time Zeit.
	 * @return string
	 */
	private function short_time( string $time ): string {
		return substr( $time, 0, 5 );
	}

	/**
	 * Aktiv-/Inaktiv-Badge (hs-shield-Muster).
	 *
	 * @param bool $active Aktiv?
	 * @return string
	 */
	private function active_badge( bool $active ): string {
		$class = $active ? 'hs-shield--green' : 'hs-shield--grey';
		$label = $active ? __( 'Aktiv', 'kurato' ) : __( 'Inaktiv', 'kurato' );
		return '<span class="hs-shield ' . esc_attr( $class ) . '">' . esc_html( $label ) . '</span>';
	}

	/**
	 * Aktiv/Inaktiv-Schaltflaeche fuer die Uebersicht.
	 *
	 * @param array<string, mixed> $host Host.
	 * @return string
	 */
	private function toggle_button( array $host ): string {
		$id    = (int) $host['id'];
		$label = (bool) $host['active'] ? __( 'Deaktivieren', 'kurato' ) : __( 'Aktivieren', 'kurato' );

		$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_TOGGLE_HOST ) . '" />';
		$html .= '<input type="hidden" name="host" value="' . esc_attr( (string) $id ) . '" />';
		$html .= wp_nonce_field( self::ACTION_TOGGLE_HOST . '_' . $id, '_wpnonce', true, false );
		$html .= '<button type="submit" class="button-link">' . esc_html( $label ) . '</button>';
		$html .= '</form>';
		return $html;
	}

	/**
	 * Loesch-Link fuer eine Verfuegbarkeits-Zeile.
	 *
	 * @param int $row_id  Zeilen-ID.
	 * @param int $host_id Host-ID.
	 * @return string
	 */
	private function delete_avail_link( int $row_id, int $host_id ): string {
		$url = wp_nonce_url(
			add_query_arg(
				array( 'action' => self::ACTION_DELETE_AVAIL, 'host' => $host_id, 'row' => $row_id ),
				admin_url( 'admin-post.php' )
			),
			self::ACTION_DELETE_AVAIL . '_' . $row_id
		);
		return '<a href="' . esc_url( $url ) . '" class="submitdelete" onclick="return confirm(\'' . esc_js( __( 'Diesen Eintrag wirklich loeschen?', 'kurato' ) ) . '\');">' . esc_html__( 'Loeschen', 'kurato' ) . '</a>';
	}

	/**
	 * Capability-Wache.
	 *
	 * @return void
	 */
	private function guard(): void {
		if ( ! current_user_can( self::CAP ) ) {
			wp_die( esc_html__( 'Sie haben keine Berechtigung, die Terminverwaltung zu oeffnen.', 'kurato' ) );
		}
	}

	/**
	 * Statusmeldung nach 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;
		}
		$map = array(
			'host_saved'        => array( 'success', __( 'Ansprechpartner gespeichert.', 'kurato' ) ),
			'host_deleted'      => array( 'success', __( 'Ansprechpartner geloescht.', 'kurato' ) ),
			'host_invalid'      => array( 'error', __( 'Bitte geben Sie einen Namen an.', 'kurato' ) ),
			'host_limit'        => array( 'warning', __( 'Mehr als ein Ansprechpartner gehoert zu Kurato Pro. Ihre bestehenden Ansprechpartner und Termine bleiben unveraendert.', 'kurato' ) ),
			'type_saved'        => array( 'success', __( 'Terminart gespeichert.', 'kurato' ) ),
			'type_deleted'      => array( 'success', __( 'Terminart geloescht.', 'kurato' ) ),
			'type_invalid'      => array( 'error', __( 'Bitte geben Sie einen Titel fuer die Terminart an.', 'kurato' ) ),
			'type_limit'        => array( 'warning', __( 'Mehr als zwei Terminarten je Ansprechpartner gehoeren zu Kurato Pro. Ihre bestehenden Terminarten und Termine bleiben unveraendert.', 'kurato' ) ),
			'avail_saved'       => array( 'success', __( 'Zeitfenster hinzugefuegt.', 'kurato' ) ),
			'avail_deleted'     => array( 'success', __( 'Eintrag geloescht.', 'kurato' ) ),
			'window_invalid'    => array( 'error', __( 'Das Zeitfenster ist ungueltig. Die Endzeit muss nach der Startzeit liegen.', 'kurato' ) ),
			'exception_invalid' => array( 'error', __( 'Der Ausnahme-Zeitraum ist ungueltig. Bitte pruefen Sie die Datumsangaben.', 'kurato' ) ),
			'not_found'         => array( 'error', __( 'Der Eintrag wurde nicht gefunden.', '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 eines Hosts mit Statusmeldung.
	 *
	 * @param int    $host_id Host-ID.
	 * @param string $msg     Statuscode.
	 * @return void
	 */
	private function redirect_detail( int $host_id, string $msg ): void {
		wp_safe_redirect( add_query_arg( array( 'page' => self::DETAIL_SLUG, 'host' => $host_id, 'msg' => $msg ), admin_url( 'admin.php' ) ) );
		exit;
	}
}
