<?php
/**
 * Buchung Post Type
 *
 * @package KursBooking
 */

declare(strict_types=1);

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Kurs_Booking_Buchung
 *
 * Handles the 'kurs-buchung' custom post type for bookings.
 */
class Kurs_Booking_Buchung {

	/**
	 * Post type name
	 */
	public const POST_TYPE = 'kurs-buchung';

	/**
	 * Meta field prefix
	 */
	private const META_PREFIX = '_buchung_';

	/**
	 * Booking statuses
	 */
	public const STATUS_PENDING   = 'pending';
	public const STATUS_CONFIRMED = 'confirmed';
	public const STATUS_CANCELLED = 'cancelled';
	public const STATUS_ANFRAGE   = 'anfrage';

	/**
	 * Initialize hooks
	 */
	public static function init(): void {
		add_action( 'init', array( __CLASS__, 'register_post_type' ) );
		add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_boxes' ) );
		add_action( 'save_post_' . self::POST_TYPE, array( __CLASS__, 'save_meta' ), 10, 2 );
		add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( __CLASS__, 'add_admin_columns' ) );
		add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( __CLASS__, 'render_admin_columns' ), 10, 2 );
		add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( __CLASS__, 'sortable_columns' ) );
		add_action( 'admin_menu', array( __CLASS__, 'add_pending_count_bubble' ), 99 );

		// Sprint 11: Sync to custom database table (low priority to run after all meta is saved).
		add_action( 'save_post_' . self::POST_TYPE, array( __CLASS__, 'sync_on_save' ), 99, 2 );
		add_action( 'before_delete_post', array( __CLASS__, 'on_delete_booking' ) );
		add_action( 'trashed_post', array( __CLASS__, 'on_delete_booking' ) );
	}

	/**
	 * Sync booking to database after any save (REST API, admin, etc.)
	 *
	 * @param int     $post_id Post ID.
	 * @param WP_Post $post Post object.
	 */
	public static function sync_on_save( int $post_id, WP_Post $post ): void {
		// Skip auto-drafts and revisions.
		if ( 'auto-draft' === $post->post_status || wp_is_post_revision( $post_id ) ) {
			return;
		}

		self::sync_to_database( $post_id );
	}

	/**
	 * Handle booking deletion - remove from custom table
	 *
	 * @param int $post_id Post ID being deleted.
	 */
	public static function on_delete_booking( int $post_id ): void {
		if ( get_post_type( $post_id ) !== self::POST_TYPE ) {
			return;
		}

		if ( class_exists( 'Kurs_Booking_Database' ) ) {
			Kurs_Booking_Database::delete_booking( $post_id );

			// Invalidate caches.
			if ( class_exists( 'Kurs_Booking_Buchung_Repository' ) ) {
				$kurs_id = (int) get_post_meta( $post_id, '_buchung_kurs_id', true );
				if ( $kurs_id ) {
					Kurs_Booking_Buchung_Repository::invalidate_kurs_cache( $kurs_id );
				}
				Kurs_Booking_Buchung_Repository::invalidate_cache();
			}
		}
	}

	/**
	 * Add pending bookings count bubble to admin menu.
	 *
	 * Shows the number of pending (not yet confirmed) bookings next to "Buchungen" menu item.
	 */
	public static function add_pending_count_bubble(): void {
		global $submenu;

		// Get pending bookings count.
		$pending_count = self::count_pending_bookings();

		if ( $pending_count < 1 ) {
			return;
		}

		// The parent menu slug.
		$parent_slug = 'edit.php?post_type=' . Kurs_Booking_Kurs::POST_TYPE;

		if ( ! isset( $submenu[ $parent_slug ] ) ) {
			return;
		}

		// Find and update the Buchungen submenu item.
		foreach ( $submenu[ $parent_slug ] as $key => $item ) {
			if ( isset( $item[2] ) && 'edit.php?post_type=' . self::POST_TYPE === $item[2] ) {
                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
				$submenu[ $parent_slug ][ $key ][0] = sprintf(
					/* translators: 1: Menu item name, 2: Number of pending items */
					'%1$s <span class="awaiting-mod update-plugins count-%2$d"><span class="pending-count">%2$d</span></span>',
					esc_html__( 'Buchungen', 'kurs-booking' ),
					$pending_count
				);
				break;
			}
		}
	}

	/**
	 * Count pending (not yet confirmed) bookings.
	 *
	 * @return int Number of pending bookings.
	 */
	public static function count_pending_bookings(): int {
		global $wpdb;

		// Use direct query for performance.
		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(p.ID)
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                WHERE p.post_type = %s
                AND p.post_status = 'publish'
                AND pm.meta_key = '_buchung_status'
                AND pm.meta_value = %s",
				self::POST_TYPE,
				self::STATUS_PENDING
			)
		);

		return (int) $count;
	}

	/**
	 * Register the buchung post type
	 */
	public static function register_post_type(): void {
		$labels = array(
			'name'               => _x( 'Buchungen', 'Post type general name', 'kurs-booking' ),
			'singular_name'      => _x( 'Buchung', 'Post type singular name', 'kurs-booking' ),
			'menu_name'          => _x( 'Buchungen', 'Admin Menu text', 'kurs-booking' ),
			'add_new'            => __( 'Neue Buchung', 'kurs-booking' ),
			'add_new_item'       => __( 'Neue Buchung erstellen', 'kurs-booking' ),
			'edit_item'          => __( 'Buchung bearbeiten', 'kurs-booking' ),
			'new_item'           => __( 'Neue Buchung', 'kurs-booking' ),
			'view_item'          => __( 'Buchung ansehen', 'kurs-booking' ),
			'search_items'       => __( 'Buchungen suchen', 'kurs-booking' ),
			'not_found'          => __( 'Keine Buchungen gefunden', 'kurs-booking' ),
			'not_found_in_trash' => __( 'Keine Buchungen im Papierkorb', 'kurs-booking' ),
			'all_items'          => __( 'Alle Buchungen', 'kurs-booking' ),
		);

		$args = array(
			'labels'              => $labels,
			'public'              => false,
			'publicly_queryable'  => false,
			'show_ui'             => true,
			'show_in_menu'        => 'edit.php?post_type=' . Kurs_Booking_Kurs::POST_TYPE,
			'query_var'           => false,
			'rewrite'             => false,
			'capability_type'     => 'post',
			'has_archive'         => false,
			'hierarchical'        => false,
			'supports'            => array( 'title' ),
			'show_in_rest'        => false,
			'exclude_from_search' => true,
		);

		register_post_type( self::POST_TYPE, $args );
	}

	/**
	 * Add meta boxes to buchung post type
	 */
	public static function add_meta_boxes(): void {
		add_meta_box(
			'buchung_details',
			__( 'Buchungsdetails', 'kurs-booking' ),
			array( __CLASS__, 'render_details_meta_box' ),
			self::POST_TYPE,
			'normal',
			'high'
		);

		add_meta_box(
			'buchung_customer',
			__( 'Kundendaten', 'kurs-booking' ),
			array( __CLASS__, 'render_customer_meta_box' ),
			self::POST_TYPE,
			'normal',
			'default'
		);

		add_meta_box(
			'buchung_status',
			__( 'Status', 'kurs-booking' ),
			array( __CLASS__, 'render_status_meta_box' ),
			self::POST_TYPE,
			'side',
			'high'
		);

		add_meta_box(
			'buchung_sevdesk',
			__( 'sevDesk Integration', 'kurs-booking' ),
			array( __CLASS__, 'render_sevdesk_meta_box' ),
			self::POST_TYPE,
			'side',
			'default'
		);
	}

	/**
	 * Render booking details meta box (READ-ONLY)
	 *
	 * Buchungsdaten sind schreibgeschützt, da sie Grundlage für sevDesk-Rechnungen sind.
	 *
	 * @param WP_Post $post The post object.
	 */
	public static function render_details_meta_box( WP_Post $post ): void {
		wp_nonce_field( 'kurs_booking_save_buchung', 'kurs_booking_buchung_nonce' );

		$kurs_id       = (int) get_post_meta( $post->ID, self::META_PREFIX . 'kurs_id', true );
		$variante_id   = (int) get_post_meta( $post->ID, self::META_PREFIX . 'variante_id', true );
		$anzahl        = (int) get_post_meta( $post->ID, self::META_PREFIX . 'anzahl', true ) ?: 1;
		$total_price   = (float) get_post_meta( $post->ID, self::META_PREFIX . 'total_price', true );
		$deposit       = (float) get_post_meta( $post->ID, self::META_PREFIX . 'deposit_amount', true );
		$custom_fields = get_post_meta( $post->ID, self::META_PREFIX . 'custom_fields', true );
		$tickets_json  = get_post_meta( $post->ID, self::META_PREFIX . 'tickets', true );

		// Kurs-Daten.
		$kurs_title  = $kurs_id ? get_the_title( $kurs_id ) : '—';
		$kurs_date   = $kurs_id ? get_post_meta( $kurs_id, '_kurs_start_date', true ) : '';
		$kurs_date_f = $kurs_date ? date_i18n( 'd.m.Y', strtotime( $kurs_date ) ) : '';

		// Varianten-Name ermitteln.
		$variante_name = '—';
		if ( $kurs_id && $variante_id ) {
			$varianten = Kurs_Booking_Kurs::get_varianten( $kurs_id );
			foreach ( $varianten as $idx => $v ) {
				$v_id = $v['id'] ?? ( $idx + 1 );
				if ( (int) $v_id === $variante_id ) {
					$variante_name = $v['name'] ?? __( 'Ticket', 'kurs-booking' );
					break;
				}
			}
		}

		// Tickets aus JSON parsen.
		$tickets = array();
		if ( $tickets_json ) {
			$tickets = is_array( $tickets_json ) ? $tickets_json : ( json_decode( $tickets_json, true ) ?: array() );
		}

		// Check setting: show empty fields?
		$show_empty = (bool) get_option( 'kurs_booking_show_empty_fields', false );
		?>
		<div class="buchung-details-readonly">
			<!-- Preis-Highlight -->
			<div class="buchung-price-hero">
				<span class="buchung-price-label"><?php esc_html_e( 'Gesamtbetrag', 'kurs-booking' ); ?></span>
				<span class="buchung-price-value"><?php echo esc_html( number_format( $total_price, 2, ',', '.' ) ); ?> €</span>
				<?php if ( $deposit > 0 ) : ?>
					<span class="buchung-deposit-info">
						<?php
						printf(
							/* translators: %s: deposit amount */
							esc_html__( 'davon Anzahlung: %s €', 'kurs-booking' ),
							esc_html( number_format( $deposit, 2, ',', '.' ) )
						);
						?>
					</span>
				<?php endif; ?>
			</div>

			<!-- Kurs-Info Card -->
			<div class="buchung-info-card">
				<div class="buchung-info-card-header">
					<span class="dashicons dashicons-calendar-alt"></span>
					<?php esc_html_e( 'Veranstaltung', 'kurs-booking' ); ?>
				</div>
				<div class="buchung-info-card-body">
					<div class="buchung-info-item">
						<span class="buchung-info-label"><?php esc_html_e( 'Kurs', 'kurs-booking' ); ?></span>
						<span class="buchung-info-value">
							<?php if ( $kurs_id ) : ?>
								<a href="<?php echo esc_url( get_edit_post_link( $kurs_id ) ); ?>"><?php echo esc_html( $kurs_title ); ?></a>
							<?php else : ?>
								<?php echo esc_html( $kurs_title ); ?>
							<?php endif; ?>
						</span>
					</div>
					<?php if ( $kurs_date_f || $show_empty ) : ?>
					<div class="buchung-info-item">
						<span class="buchung-info-label"><?php esc_html_e( 'Datum', 'kurs-booking' ); ?></span>
						<span class="buchung-info-value"><?php echo esc_html( $kurs_date_f ?: '—' ); ?></span>
					</div>
					<?php endif; ?>
					<div class="buchung-info-item">
						<span class="buchung-info-label"><?php esc_html_e( 'Ticket-Typ', 'kurs-booking' ); ?></span>
						<span class="buchung-info-value"><?php echo esc_html( $variante_name ); ?></span>
					</div>
					<div class="buchung-info-item">
						<span class="buchung-info-label"><?php esc_html_e( 'Anzahl', 'kurs-booking' ); ?></span>
						<span class="buchung-info-value buchung-info-highlight"><?php echo esc_html( (string) $anzahl ); ?> <?php esc_html_e( 'Teilnehmer', 'kurs-booking' ); ?></span>
					</div>
				</div>
			</div>

			<?php
			// Handle both JSON string and array formats.
			if ( is_array( $custom_fields ) ) {
				$fields = $custom_fields;
			} elseif ( is_string( $custom_fields ) && ! empty( $custom_fields ) ) {
				$fields = json_decode( $custom_fields, true ) ?: array();
			} else {
				$fields = array();
			}
			if ( ( is_array( $fields ) && ! empty( $fields ) ) || $show_empty ) :
				?>
				<div class="buchung-info-card">
					<div class="buchung-info-card-header">
						<span class="dashicons dashicons-list-view"></span>
						<?php esc_html_e( 'Zusätzliche Angaben', 'kurs-booking' ); ?>
					</div>
					<div class="buchung-info-card-body">
						<?php if ( is_array( $fields ) && ! empty( $fields ) ) : ?>
							<?php foreach ( $fields as $label => $value ) : ?>
								<div class="buchung-info-item">
									<span class="buchung-info-label"><?php echo esc_html( $label ); ?></span>
									<span class="buchung-info-value"><?php echo esc_html( $value ?: '—' ); ?></span>
								</div>
							<?php endforeach; ?>
						<?php else : ?>
							<div class="buchung-info-item">
								<span class="buchung-info-value empty-field"><?php esc_html_e( 'Keine zusätzlichen Angaben', 'kurs-booking' ); ?></span>
							</div>
						<?php endif; ?>
					</div>
				</div>
			<?php endif; ?>

			<!-- Hinweis -->
			<div class="buchung-readonly-notice">
				<span class="dashicons dashicons-lock"></span>
				<?php esc_html_e( 'Buchungsdaten sind schreibgeschützt (Rechnungsgrundlage).', 'kurs-booking' ); ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Render customer data meta box (READ-ONLY)
	 *
	 * Kundendaten sind schreibgeschützt, da sie Grundlage für sevDesk-Rechnungen sind.
	 *
	 * @param WP_Post $post The post object.
	 */
	public static function render_customer_meta_box( WP_Post $post ): void {
		$name         = get_post_meta( $post->ID, self::META_PREFIX . 'name', true );
		$email        = get_post_meta( $post->ID, self::META_PREFIX . 'email', true );
		$phone        = get_post_meta( $post->ID, self::META_PREFIX . 'phone', true );
		$message      = get_post_meta( $post->ID, self::META_PREFIX . 'message', true );
		$wunschtermin = get_post_meta( $post->ID, self::META_PREFIX . 'wunschtermin', true );

		// Check setting: show empty fields?
		$show_empty = (bool) get_option( 'kurs_booking_show_empty_fields', false );
		?>
		<div class="buchung-customer-readonly">
			<!-- Kunden-Header mit Avatar -->
			<div class="buchung-customer-header">
				<div class="buchung-customer-avatar">
					<?php echo esc_html( mb_strtoupper( mb_substr( $name ?: '?', 0, 1 ) ) ); ?>
				</div>
				<div class="buchung-customer-name">
					<strong><?php echo esc_html( $name ?: '—' ); ?></strong>
				</div>
			</div>

			<!-- Kontaktdaten -->
			<div class="buchung-customer-contacts">
				<?php if ( $email || $show_empty ) : ?>
				<div class="buchung-customer-contact">
					<span class="dashicons dashicons-email"></span>
					<?php if ( $email ) : ?>
						<a href="mailto:<?php echo esc_attr( $email ); ?>"><?php echo esc_html( $email ); ?></a>
					<?php else : ?>
						<span class="empty-field">—</span>
					<?php endif; ?>
				</div>
				<?php endif; ?>

				<?php if ( $phone || $show_empty ) : ?>
				<div class="buchung-customer-contact">
					<span class="dashicons dashicons-phone"></span>
					<?php if ( $phone ) : ?>
						<a href="tel:<?php echo esc_attr( preg_replace( '/[^0-9+]/', '', $phone ) ); ?>"><?php echo esc_html( $phone ); ?></a>
					<?php else : ?>
						<span class="empty-field">—</span>
					<?php endif; ?>
				</div>
				<?php endif; ?>

				<?php if ( $wunschtermin || $show_empty ) : ?>
				<div class="buchung-customer-contact">
					<span class="dashicons dashicons-calendar"></span>
					<span><?php echo esc_html( $wunschtermin ?: '—' ); ?></span>
				</div>
				<?php endif; ?>
			</div>

			<?php if ( $message || $show_empty ) : ?>
			<div class="buchung-customer-message">
				<div class="buchung-customer-message-label">
					<span class="dashicons dashicons-format-chat"></span>
					<?php esc_html_e( 'Nachricht', 'kurs-booking' ); ?>
				</div>
				<div class="buchung-customer-message-text">
					<?php echo $message ? nl2br( esc_html( $message ) ) : '<span class="empty-field">—</span>'; ?>
				</div>
			</div>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Render status meta box
	 *
	 * Dies ist die EINZIGE editierbare Metabox für Buchungen.
	 *
	 * @param WP_Post $post The post object.
	 */
	public static function render_status_meta_box( WP_Post $post ): void {
		$status         = get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: self::STATUS_PENDING;
		$confirm_token  = get_post_meta( $post->ID, self::META_PREFIX . 'confirm_token', true );
		$confirmed_at   = get_post_meta( $post->ID, self::META_PREFIX . 'confirmed_at', true );
		$created_at     = get_post_meta( $post->ID, self::META_PREFIX . 'created_at', true );
		$last_email_at  = get_post_meta( $post->ID, '_last_email_sent_at', true );
		$cancelled_at   = get_post_meta( $post->ID, '_cancel_confirmed_at', true );

		$statuses = array(
			self::STATUS_PENDING   => array(
				'label' => __( 'Ausstehend', 'kurs-booking' ),
				'icon'  => 'clock',
				'color' => 'warning',
			),
			self::STATUS_CONFIRMED => array(
				'label' => __( 'Bestätigt', 'kurs-booking' ),
				'icon'  => 'yes-alt',
				'color' => 'success',
			),
			self::STATUS_CANCELLED => array(
				'label' => __( 'Storniert', 'kurs-booking' ),
				'icon'  => 'dismiss',
				'color' => 'danger',
			),
			self::STATUS_ANFRAGE   => array(
				'label' => __( 'Anfrage', 'kurs-booking' ),
				'icon'  => 'email',
				'color' => 'info',
			),
		);

		$current_status = $statuses[ $status ] ?? $statuses[ self::STATUS_PENDING ];

		// Admin nonce for AJAX actions.
		$admin_nonce = wp_create_nonce( 'kurs_booking_admin' );
		?>
		<div class="buchung-status-box">
			<!-- Aktueller Status Badge -->
			<div class="buchung-current-status buchung-status-<?php echo esc_attr( $current_status['color'] ); ?>">
				<span class="dashicons dashicons-<?php echo esc_attr( $current_status['icon'] ); ?>"></span>
				<span><?php echo esc_html( $current_status['label'] ); ?></span>
			</div>

			<!-- Status ändern -->
			<div class="buchung-status-select">
				<label for="buchung_status"><?php esc_html_e( 'Status ändern', 'kurs-booking' ); ?></label>
				<select name="buchung_status" id="buchung_status">
					<?php foreach ( $statuses as $value => $data ) : ?>
						<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $status, $value ); ?>>
							<?php echo esc_html( $data['label'] ); ?>
						</option>
					<?php endforeach; ?>
				</select>
			</div>

			<!-- Timeline -->
			<div class="buchung-timeline">
				<?php if ( $created_at ) : ?>
				<div class="buchung-timeline-item">
					<span class="buchung-timeline-icon">
						<span class="dashicons dashicons-calendar-alt"></span>
					</span>
					<div class="buchung-timeline-content">
						<span class="buchung-timeline-label"><?php esc_html_e( 'Erstellt', 'kurs-booking' ); ?></span>
						<span class="buchung-timeline-date"><?php echo esc_html( date_i18n( 'd.m.Y, H:i', strtotime( $created_at ) ) ); ?> <?php esc_html_e( 'Uhr', 'kurs-booking' ); ?></span>
					</div>
				</div>
				<?php endif; ?>

				<?php if ( $confirmed_at ) : ?>
				<div class="buchung-timeline-item buchung-timeline-success">
					<span class="buchung-timeline-icon">
						<span class="dashicons dashicons-yes"></span>
					</span>
					<div class="buchung-timeline-content">
						<span class="buchung-timeline-label"><?php esc_html_e( 'Bestätigt', 'kurs-booking' ); ?></span>
						<span class="buchung-timeline-date"><?php echo esc_html( date_i18n( 'd.m.Y, H:i', strtotime( $confirmed_at ) ) ); ?> <?php esc_html_e( 'Uhr', 'kurs-booking' ); ?></span>
					</div>
				</div>
				<?php endif; ?>

				<?php if ( $cancelled_at ) : ?>
				<div class="buchung-timeline-item buchung-timeline-danger">
					<span class="buchung-timeline-icon">
						<span class="dashicons dashicons-dismiss"></span>
					</span>
					<div class="buchung-timeline-content">
						<span class="buchung-timeline-label"><?php esc_html_e( 'Storniert', 'kurs-booking' ); ?></span>
						<span class="buchung-timeline-date"><?php echo esc_html( date_i18n( 'd.m.Y, H:i', strtotime( $cancelled_at ) ) ); ?> <?php esc_html_e( 'Uhr', 'kurs-booking' ); ?></span>
					</div>
				</div>
				<?php endif; ?>
			</div>

			<?php if ( $confirm_token && self::STATUS_PENDING === $status ) : ?>
			<div class="buchung-token-info">
				<span class="dashicons dashicons-admin-network"></span>
				<small><?php esc_html_e( 'Warte auf Kundenbestätigung per E-Mail', 'kurs-booking' ); ?></small>
			</div>
			<?php endif; ?>

			<!-- Cancellation Info (if cancelled) -->
			<?php if ( self::STATUS_CANCELLED === $status ) : ?>
				<?php $cancel_data = self::get_cancellation_data( $post->ID ); ?>
				<?php if ( $cancel_data ) : ?>
				<div class="buchung-cancel-info" style="background: #fff3cd; border-radius: 4px; padding: 10px; margin-top: 10px;">
					<p style="margin: 5px 0;"><strong><?php esc_html_e( 'Stornogebühr:', 'kurs-booking' ); ?></strong> <?php echo esc_html( number_format( $cancel_data['fee_amount'], 2, ',', '.' ) ); ?> € (<?php echo esc_html( $cancel_data['fee_percent'] ); ?>%)</p>
					<p style="margin: 5px 0;"><strong><?php esc_html_e( 'Rückerstattung:', 'kurs-booking' ); ?></strong> <?php echo esc_html( number_format( $cancel_data['refund_amount'], 2, ',', '.' ) ); ?> €</p>
					<p style="margin: 5px 0; font-size: 11px; color: #666;"><?php echo esc_html( $cancel_data['fee_reason'] ); ?></p>
					<?php if ( $cancel_data['notes'] ) : ?>
					<p style="margin: 5px 0; font-size: 11px;"><strong><?php esc_html_e( 'Notizen:', 'kurs-booking' ); ?></strong> <?php echo esc_html( $cancel_data['notes'] ); ?></p>
					<?php endif; ?>
				</div>
				<?php endif; ?>
			<?php endif; ?>

			<!-- Action Buttons -->
			<div class="buchung-action-buttons" style="margin-top: 15px; display: flex; flex-direction: column; gap: 8px;">
				<!-- E-Mail erneut senden -->
				<button type="button" class="button kurs-booking-resend-email" data-buchung-id="<?php echo esc_attr( (string) $post->ID ); ?>" data-nonce="<?php echo esc_attr( $admin_nonce ); ?>">
					<span class="dashicons dashicons-email-alt" style="vertical-align: middle;"></span>
					<?php esc_html_e( 'E-Mail erneut senden', 'kurs-booking' ); ?>
				</button>
				<?php if ( $last_email_at ) : ?>
				<small style="color: #666; margin-left: 5px;">
					<?php
					printf(
						/* translators: %s: date/time */
						esc_html__( 'Zuletzt: %s', 'kurs-booking' ),
						esc_html( date_i18n( 'd.m.Y, H:i', strtotime( $last_email_at ) ) )
					);
					?>
				</small>
				<?php endif; ?>

				<!-- Stornieren Button (nur bei bestätigten Buchungen) -->
				<?php if ( self::STATUS_CONFIRMED === $status ) : ?>
					<?php $fee_data = self::calculate_cancellation_fee( $post->ID ); ?>
					<button type="button" class="button button-link-delete kurs-booking-admin-cancel"
							data-buchung-id="<?php echo esc_attr( (string) $post->ID ); ?>"
							data-nonce="<?php echo esc_attr( $admin_nonce ); ?>"
							data-fee-percent="<?php echo esc_attr( (string) $fee_data['cancel_fee_percent'] ); ?>"
							data-can-cancel="<?php echo esc_attr( $fee_data['can_cancel'] ? '1' : '0' ); ?>"
							style="color: #b32d2e;">
						<span class="dashicons dashicons-no" style="vertical-align: middle;"></span>
						<?php esc_html_e( 'Buchung stornieren', 'kurs-booking' ); ?>
					</button>
					<small style="color: #666;">
						<?php echo esc_html( $fee_data['reason'] ); ?>
					</small>
				<?php endif; ?>
			</div>
		</div>

		<!-- Storno-Dialog -->
		<?php
		$allow_replacement = get_option( 'kurs_booking_cancel_allow_replacement', '1' );
		$deposit_retained  = get_option( 'kurs_booking_cancel_deposit_retained', '1' );
		$kurs_id           = (int) self::get_meta( $post->ID, 'kurs_id', 0 );
		$deposit_enabled   = get_post_meta( $kurs_id, '_kurs_deposit_enabled', true );
		$deposit_percent   = (int) get_post_meta( $kurs_id, '_kurs_deposit_percent', true );
		$has_deposit       = '1' === $deposit_retained && '1' === $deposit_enabled && $deposit_percent > 0;
		?>
		<div id="kurs-booking-cancel-dialog" style="display: none;"
			 data-allow-replacement="<?php echo esc_attr( $allow_replacement ); ?>"
			 data-has-deposit="<?php echo esc_attr( $has_deposit ? '1' : '0' ); ?>"
			 data-deposit-percent="<?php echo esc_attr( (string) $deposit_percent ); ?>">
			<div class="cancel-dialog-content" style="padding: 15px;">
				<p><strong><?php esc_html_e( 'Buchung wirklich stornieren?', 'kurs-booking' ); ?></strong></p>
				<p class="cancel-fee-info"></p>

				<?php if ( $has_deposit ) : ?>
				<div class="cancel-deposit-notice" style="background: #fff3cd; padding: 10px; border-radius: 4px; margin: 10px 0;">
					<span class="dashicons dashicons-warning" style="color: #856404;"></span>
					<?php
					printf(
						/* translators: %d: deposit percentage */
						esc_html__( 'Hinweis: Die Anzahlung (%d%%) wird bei Stornierung einbehalten.', 'kurs-booking' ),
						(int) $deposit_percent
					);
					?>
				</div>
				<?php endif; ?>

				<?php if ( '1' === $allow_replacement ) : ?>
				<div style="margin: 15px 0; padding: 10px; background: #d4edda; border-radius: 4px;">
					<label for="cancel-has-replacement" style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
						<input type="checkbox" id="cancel-has-replacement">
						<span>
							<strong><?php esc_html_e( 'Ersatzteilnehmer wird gestellt', 'kurs-booking' ); ?></strong>
							<br>
							<small style="color: #155724;">
								<?php
								if ( $has_deposit ) {
									esc_html_e( 'Keine Stornogebühr, nur Anzahlung wird einbehalten.', 'kurs-booking' );
								} else {
									esc_html_e( 'Keine Stornogebühr, volle Rückerstattung.', 'kurs-booking' );
								}
								?>
							</small>
						</span>
					</label>
				</div>
				<?php endif; ?>

				<div style="margin: 15px 0;">
					<label for="cancel-fee-override">
						<input type="checkbox" id="cancel-fee-override">
						<?php esc_html_e( 'Stornogebühr manuell überschreiben', 'kurs-booking' ); ?>
					</label>
					<div id="cancel-fee-override-fields" style="display: none; margin-top: 10px;">
						<label for="cancel-fee-percent"><?php esc_html_e( 'Stornogebühr (%)', 'kurs-booking' ); ?></label>
						<input type="number" id="cancel-fee-percent" min="0" max="100" value="0" style="width: 80px;">
					</div>
				</div>
				<div style="margin: 15px 0;">
					<label for="cancel-notes"><?php esc_html_e( 'Notizen (optional)', 'kurs-booking' ); ?></label>
					<textarea id="cancel-notes" rows="2" style="width: 100%;"></textarea>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Render sevDesk integration meta box
	 *
	 * @param WP_Post $post The post object.
	 */
	public static function render_sevdesk_meta_box( WP_Post $post ): void {
		$contact_id     = get_post_meta( $post->ID, '_sevdesk_contact_id', true );
		$invoice_id     = get_post_meta( $post->ID, '_sevdesk_invoice_id', true );
		$invoice_number = get_post_meta( $post->ID, '_sevdesk_invoice_number', true );
		$sevdesk_error  = get_post_meta( $post->ID, '_sevdesk_error', true );
		$error_time     = get_post_meta( $post->ID, '_sevdesk_error_time', true );
		$status         = get_post_meta( $post->ID, self::META_PREFIX . 'status', true );
		$is_configured  = Kurs_Booking_SevDesk::is_configured();

		// sevDesk base URL for links.
		$sevdesk_base_url = 'https://my.sevdesk.de';
		?>
		<div class="buchung-sevdesk-box">
			<?php if ( ! $is_configured ) : ?>
				<p class="sevdesk-notice sevdesk-notice-info">
					<span class="dashicons dashicons-info"></span>
					<?php esc_html_e( 'sevDesk ist nicht konfiguriert.', 'kurs-booking' ); ?>
					<a href="<?php echo esc_url( admin_url( 'admin.php?page=kurs-booking-settings&tab=sevdesk' ) ); ?>">
						<?php esc_html_e( 'Einstellungen', 'kurs-booking' ); ?>
					</a>
				</p>
			<?php elseif ( $invoice_id ) : ?>
				<!-- Rechnung existiert -->
				<div class="sevdesk-status sevdesk-status-success">
					<span class="dashicons dashicons-yes-alt"></span>
					<?php esc_html_e( 'Rechnung erstellt', 'kurs-booking' ); ?>
				</div>

				<?php if ( $invoice_number ) : ?>
				<div class="sevdesk-info-row">
					<span class="sevdesk-label"><?php esc_html_e( 'Rechnungs-Nr.', 'kurs-booking' ); ?></span>
					<span class="sevdesk-value">
						<a href="<?php echo esc_url( $sevdesk_base_url . '/#/fi/edit/type/RE/id/' . $invoice_id ); ?>" target="_blank" rel="noopener">
							<strong><?php echo esc_html( $invoice_number ); ?></strong>
							<span class="dashicons dashicons-external"></span>
						</a>
					</span>
				</div>
				<?php endif; ?>

				<?php if ( $contact_id ) : ?>
				<div class="sevdesk-info-row">
					<span class="sevdesk-label"><?php esc_html_e( 'Kontakt', 'kurs-booking' ); ?></span>
					<span class="sevdesk-value">
						<a href="<?php echo esc_url( $sevdesk_base_url . '/#/crm/detail/id/' . $contact_id ); ?>" target="_blank" rel="noopener">
							#<?php echo esc_html( $contact_id ); ?>
							<span class="dashicons dashicons-external"></span>
						</a>
					</span>
				</div>
				<?php endif; ?>

			<?php elseif ( $sevdesk_error ) : ?>
				<!-- Fehler bei Rechnungserstellung -->
				<div class="sevdesk-status sevdesk-status-error">
					<span class="dashicons dashicons-warning"></span>
					<?php esc_html_e( 'Fehler bei Rechnungserstellung', 'kurs-booking' ); ?>
				</div>
				<div class="sevdesk-error-message">
					<code><?php echo esc_html( $sevdesk_error ); ?></code>
					<?php if ( $error_time ) : ?>
						<small><?php echo esc_html( date_i18n( 'd.m.Y H:i', $error_time ) ); ?></small>
					<?php endif; ?>
				</div>

				<?php if ( $contact_id ) : ?>
				<div class="sevdesk-info-row">
					<span class="sevdesk-label"><?php esc_html_e( 'Kontakt', 'kurs-booking' ); ?></span>
					<span class="sevdesk-value">
						<a href="<?php echo esc_url( $sevdesk_base_url . '/#/crm/detail/id/' . $contact_id ); ?>" target="_blank" rel="noopener">
							#<?php echo esc_html( $contact_id ); ?>
						</a>
					</span>
				</div>
				<?php endif; ?>

				<!-- Erneut versuchen Button -->
				<?php if ( self::STATUS_CONFIRMED === $status ) : ?>
				<p style="margin-top: 10px;">
					<button type="button" class="button kurs-booking-create-invoice" data-buchung-id="<?php echo esc_attr( (string) $post->ID ); ?>">
						<span class="dashicons dashicons-update"></span>
						<?php esc_html_e( 'Erneut versuchen', 'kurs-booking' ); ?>
					</button>
				</p>
				<?php endif; ?>

			<?php else : ?>
				<!-- Keine sevDesk-Verknüpfung -->
				<p class="sevdesk-notice sevdesk-notice-neutral">
					<?php esc_html_e( 'Noch keine sevDesk-Verknüpfung.', 'kurs-booking' ); ?>
				</p>

				<?php if ( self::STATUS_CONFIRMED === $status ) : ?>
				<p style="margin-top: 10px;">
					<button type="button" class="button button-primary kurs-booking-create-invoice" data-buchung-id="<?php echo esc_attr( (string) $post->ID ); ?>">
						<span class="dashicons dashicons-media-spreadsheet"></span>
						<?php esc_html_e( 'Rechnung erstellen', 'kurs-booking' ); ?>
					</button>
				</p>
				<?php elseif ( self::STATUS_PENDING === $status ) : ?>
				<p class="description">
					<?php esc_html_e( 'Buchung muss erst bestätigt werden.', 'kurs-booking' ); ?>
				</p>
				<?php endif; ?>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Save buchung meta data
	 *
	 * @param int     $post_id The post ID.
	 * @param WP_Post $post    The post object.
	 */
	public static function save_meta( int $post_id, WP_Post $post ): void {
		// Verify nonce.
		if ( ! isset( $_POST['kurs_booking_buchung_nonce'] ) ||
			 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kurs_booking_buchung_nonce'] ) ), 'kurs_booking_save_buchung' ) ) {
			return;
		}

		// Check autosave.
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		// Check permissions.
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		// Save fields.
		$text_fields = array( 'name', 'phone', 'message', 'status' );
		$int_fields  = array( 'kurs_id', 'variante_id', 'anzahl' );
		$float_fields = array( 'total_price', 'deposit_amount' );

		foreach ( $text_fields as $field ) {
			$key   = 'buchung_' . $field;
			$value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
			update_post_meta( $post_id, self::META_PREFIX . $field, $value );
		}

		// Email field with validation.
		if ( isset( $_POST['buchung_email'] ) ) {
			$email = sanitize_email( wp_unslash( $_POST['buchung_email'] ) );
			update_post_meta( $post_id, self::META_PREFIX . 'email', $email );
		}

		foreach ( $int_fields as $field ) {
			$key   = 'buchung_' . $field;
			$value = isset( $_POST[ $key ] ) ? absint( $_POST[ $key ] ) : 0;
			update_post_meta( $post_id, self::META_PREFIX . $field, $value );
		}

		foreach ( $float_fields as $field ) {
			$key   = 'buchung_' . $field;
			$value = isset( $_POST[ $key ] ) ? floatval( $_POST[ $key ] ) : 0.0;
			update_post_meta( $post_id, self::META_PREFIX . $field, $value );
		}

		// Update post title based on customer name and kurs.
		$name    = get_post_meta( $post_id, self::META_PREFIX . 'name', true );
		$kurs_id = get_post_meta( $post_id, self::META_PREFIX . 'kurs_id', true );
		$kurs_title = $kurs_id ? get_the_title( $kurs_id ) : '';

		if ( $name || $kurs_title ) {
			$new_title = sprintf( 'BK-%d: %s - %s', $post_id, $name, $kurs_title );
			wp_update_post(
				array(
					'ID'         => $post_id,
					'post_title' => $new_title,
				)
			);
		}

		// Sprint 11: Sync to custom database table.
		self::sync_to_database( $post_id );
	}

	/**
	 * Sync booking to custom database table
	 *
	 * @param int $post_id Booking post ID.
	 */
	public static function sync_to_database( int $post_id ): void {
		if ( class_exists( 'Kurs_Booking_Database' ) && Kurs_Booking_Database::table_exists() ) {
			Kurs_Booking_Database::sync_booking_to_table( $post_id );

			// Invalidate relevant caches.
			if ( class_exists( 'Kurs_Booking_Buchung_Repository' ) ) {
				$kurs_id = (int) get_post_meta( $post_id, '_buchung_kurs_id', true );
				if ( $kurs_id ) {
					Kurs_Booking_Buchung_Repository::invalidate_kurs_cache( $kurs_id );
				}
				Kurs_Booking_Buchung_Repository::invalidate_cache();
			}
		}
	}

	/**
	 * Add custom admin columns.
	 *
	 * @param array<string, string> $columns Existing columns.
	 * @return array<string, string> Modified columns.
	 */
	public static function add_admin_columns( array $columns ): array {
		$new_columns = array();
		$new_columns['cb'] = $columns['cb'];
		$new_columns['title'] = $columns['title'];
		$new_columns['kurs'] = __( 'Kurs', 'kurs-booking' );
		$new_columns['customer'] = __( 'Kunde', 'kurs-booking' );
		$new_columns['total'] = __( 'Betrag', 'kurs-booking' );
		$new_columns['buchung_status'] = __( 'Status', 'kurs-booking' );
		$new_columns['date'] = $columns['date'];

		return $new_columns;
	}

	/**
	 * Render custom admin columns
	 *
	 * @param string $column  Column name.
	 * @param int    $post_id Post ID.
	 */
	public static function render_admin_columns( string $column, int $post_id ): void {
		switch ( $column ) {
			case 'kurs':
				$kurs_id = get_post_meta( $post_id, self::META_PREFIX . 'kurs_id', true );
				if ( $kurs_id ) {
					$kurs = get_post( $kurs_id );
					if ( $kurs ) {
						echo '<a href="' . esc_url( get_edit_post_link( $kurs_id ) ) . '">';
						echo esc_html( $kurs->post_title );
						echo '</a>';

						$start_date = get_post_meta( $kurs_id, '_kurs_start_date', true );
						if ( $start_date ) {
							echo '<br><small>' . esc_html( date_i18n( 'd.m.Y', strtotime( $start_date ) ) ) . '</small>';
						}
					}
				} else {
					echo '<em>' . esc_html__( 'Kein Kurs', 'kurs-booking' ) . '</em>';
				}
				break;

			case 'customer':
				$name  = get_post_meta( $post_id, self::META_PREFIX . 'name', true );
				$email = get_post_meta( $post_id, self::META_PREFIX . 'email', true );
				echo esc_html( $name );
				if ( $email ) {
					echo '<br><a href="mailto:' . esc_attr( $email ) . '">' . esc_html( $email ) . '</a>';
				}
				break;

			case 'total':
				$total = (float) get_post_meta( $post_id, self::META_PREFIX . 'total_price', true );
				echo esc_html( number_format( $total, 2, ',', '.' ) . ' EUR' );

				$deposit = (float) get_post_meta( $post_id, self::META_PREFIX . 'deposit_amount', true );
				if ( $deposit > 0 ) {
					echo '<br><small>' . esc_html__( 'Anzahlung:', 'kurs-booking' ) . ' ';
					echo esc_html( number_format( $deposit, 2, ',', '.' ) . ' EUR' ) . '</small>';
				}
				break;

			case 'buchung_status':
				$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true ) ?: self::STATUS_PENDING;
				$status_labels = array(
					self::STATUS_PENDING   => array( 'label' => __( 'Ausstehend', 'kurs-booking' ), 'color' => '#f0ad4e' ),
					self::STATUS_CONFIRMED => array( 'label' => __( 'Bestätigt', 'kurs-booking' ), 'color' => '#5cb85c' ),
					self::STATUS_CANCELLED => array( 'label' => __( 'Storniert', 'kurs-booking' ), 'color' => '#d9534f' ),
					self::STATUS_ANFRAGE   => array( 'label' => __( 'Anfrage', 'kurs-booking' ), 'color' => '#5bc0de' ),
				);

				$status_info = $status_labels[ $status ] ?? $status_labels[ self::STATUS_PENDING ];
				printf(
					'<span style="background: %s; color: #fff; padding: 3px 8px; border-radius: 3px; font-size: 11px;">%s</span>',
					esc_attr( $status_info['color'] ),
					esc_html( $status_info['label'] )
				);
				break;
		}
	}

	/**
	 * Define sortable columns.
	 *
	 * @param array<string, string> $columns Existing sortable columns.
	 * @return array<string, string> Modified sortable columns.
	 */
	public static function sortable_columns( array $columns ): array {
		$columns['buchung_status'] = 'buchung_status';
		return $columns;
	}

	/**
	 * Count confirmed bookings for a kurs
	 *
	 * @param int $kurs_id The kurs post ID.
	 * @return int Number of confirmed bookings.
	 */
	public static function count_confirmed_bookings( int $kurs_id ): int {
		global $wpdb;

		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COALESCE(SUM(pm2.meta_value), 0)
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm1 ON p.ID = pm1.post_id
                INNER JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id
                INNER JOIN {$wpdb->postmeta} pm3 ON p.ID = pm3.post_id
                WHERE p.post_type = %s
                AND p.post_status = 'publish'
                AND pm1.meta_key = %s AND pm1.meta_value = %d
                AND pm2.meta_key = %s
                AND pm3.meta_key = %s AND pm3.meta_value = %s",
				self::POST_TYPE,
				self::META_PREFIX . 'kurs_id',
				$kurs_id,
				self::META_PREFIX . 'anzahl',
				self::META_PREFIX . 'status',
				self::STATUS_CONFIRMED
			)
		);

		return (int) $count;
	}

	/**
	 * Get booking by confirmation token
	 *
	 * @param string $token Confirmation token.
	 * @return int|null Booking post ID or null if not found.
	 */
	public static function get_by_token( string $token ): ?int {
		global $wpdb;

		if ( empty( $token ) || strlen( $token ) < 32 ) {
			return null;
		}

		$post_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT p.ID
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                WHERE p.post_type = %s
                AND p.post_status = 'publish'
                AND pm.meta_key = %s
                AND pm.meta_value = %s
                LIMIT 1",
				self::POST_TYPE,
				self::META_PREFIX . 'confirm_token',
				$token
			)
		);

		return $post_id ? (int) $post_id : null;
	}

	/**
	 * Confirm a booking (Double-Opt-In)
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return bool True if confirmed successfully.
	 */
	public static function confirm( int $buchung_id ): bool {
		$current_status = self::get_meta( $buchung_id, 'status', '' );

		if ( self::STATUS_CONFIRMED === $current_status ) {
			return false; // Already confirmed.
		}

		// Update status.
		update_post_meta( $buchung_id, self::META_PREFIX . 'status', self::STATUS_CONFIRMED );
		update_post_meta( $buchung_id, self::META_PREFIX . 'confirmed_at', current_time( 'mysql' ) );

		// Clear confirmation token.
		delete_post_meta( $buchung_id, self::META_PREFIX . 'confirm_token' );

		/**
		 * Fires after a booking is confirmed.
		 *
		 * @param int $buchung_id The booking post ID.
		 */
		do_action( 'kurs_booking_confirmed', $buchung_id );

		return true;
	}

	/**
	 * Count confirmed bookings for a specific variant
	 *
	 * @param int $kurs_id    The kurs post ID.
	 * @param int $variant_id The variant ID.
	 * @return int Number of confirmed bookings for this variant.
	 */
	public static function count_variant_bookings( int $kurs_id, int $variant_id ): int {
		global $wpdb;

		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COALESCE(SUM(pm_anzahl.meta_value), 0)
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm_kurs ON p.ID = pm_kurs.post_id
                INNER JOIN {$wpdb->postmeta} pm_variante ON p.ID = pm_variante.post_id
                INNER JOIN {$wpdb->postmeta} pm_status ON p.ID = pm_status.post_id
                INNER JOIN {$wpdb->postmeta} pm_anzahl ON p.ID = pm_anzahl.post_id
                WHERE p.post_type = %s
                AND p.post_status = 'publish'
                AND pm_kurs.meta_key = %s AND pm_kurs.meta_value = %d
                AND pm_variante.meta_key = %s AND pm_variante.meta_value = %d
                AND pm_status.meta_key = %s AND pm_status.meta_value = %s
                AND pm_anzahl.meta_key = %s",
				self::POST_TYPE,
				self::META_PREFIX . 'kurs_id',
				$kurs_id,
				self::META_PREFIX . 'variante_id',
				$variant_id,
				self::META_PREFIX . 'status',
				self::STATUS_CONFIRMED,
				self::META_PREFIX . 'anzahl'
			)
		);

		return (int) $count;
	}

	/**
	 * Create a new booking.
	 *
	 * @param array<string, mixed> $data Booking data.
	 * @return int|WP_Error Post ID on success, WP_Error on failure.
	 */
	public static function create( array $data ) {
		// Validate required fields.
		$required = array( 'kurs_id', 'name', 'email' );
		foreach ( $required as $field ) {
			if ( empty( $data[ $field ] ) ) {
				return new WP_Error(
					'missing_field',
					/* translators: %s: field name */
					sprintf( __( 'Pflichtfeld fehlt: %s', 'kurs-booking' ), $field )
				);
			}
		}

		// Validate email.
		if ( ! is_email( $data['email'] ) ) {
			return new WP_Error( 'invalid_email', __( 'Ungültige E-Mail-Adresse', 'kurs-booking' ) );
		}

		// Validate kurs exists.
		$kurs = get_post( $data['kurs_id'] );
		if ( ! $kurs || Kurs_Booking_Kurs::POST_TYPE !== $kurs->post_type ) {
			return new WP_Error( 'invalid_kurs', __( 'Ungültiger Kurs', 'kurs-booking' ) );
		}

		// Create booking post.
		$post_id = wp_insert_post(
			array(
				'post_type'   => self::POST_TYPE,
				'post_status' => 'publish',
				'post_title'  => sprintf( 'BK-TEMP: %s', sanitize_text_field( $data['name'] ) ),
			)
		);

		if ( is_wp_error( $post_id ) ) {
			return $post_id;
		}

		// Generate confirmation token.
		$confirm_token = bin2hex( random_bytes( 32 ) );

		// Process tickets array (variant_id => quantity).
		$tickets      = isset( $data['tickets'] ) && is_array( $data['tickets'] ) ? $data['tickets'] : array();
		$ticket_days  = isset( $data['ticket_days'] ) && is_array( $data['ticket_days'] ) ? $data['ticket_days'] : array();
		$extra_fields = isset( $data['extra_fields'] ) && is_array( $data['extra_fields'] ) ? $data['extra_fields'] : array();
		$total_qty    = 0;
		$total_price  = 0;
		$variante_id  = 1; // Default to first variant.

		// Get kurs variants for price calculation.
		$varianten = Kurs_Booking_Kurs::get_varianten( $data['kurs_id'] );

		// Sprint 4.10: Get course days for per_day calculation (used as max/default).
		$kurs_days = Kurs_Booking_Kurs::get_course_days( $data['kurs_id'] );

		foreach ( $tickets as $variant_id => $quantity ) {
			$quantity = absint( $quantity );
			if ( $quantity < 1 ) {
				continue;
			}

			$total_qty += $quantity;

			// Find variant price.
			foreach ( $varianten as $index => $v ) {
				$v_id = $v['id'] ?? ( $index + 1 );
				if ( (int) $v_id === (int) $variant_id ) {
					$price   = floatval( $v['preis'] ?? 0 );
					$per_day = ! empty( $v['per_day'] );

					// Sprint 4.10: Apply per_day calculation with user-selected days.
					if ( $per_day ) {
						// Get user-selected days for this variant, default to full course days.
						$selected_days = isset( $ticket_days[ $variant_id ] ) && $ticket_days[ $variant_id ] > 0
							? min( absint( $ticket_days[ $variant_id ] ), $kurs_days )
							: $kurs_days;
						$effective_price = $price * $selected_days;
					} else {
						$effective_price = $price;
					}

					// Sprint 4.11: Apply extra field multiplier (quantity fields with multiply_price).
					$extra_multiplier = 1.0;
					if ( class_exists( 'Kurs_Booking_Custom_Fields' ) ) {
						// Ensure variant has 'id' for extra_fields lookup.
						$v_with_id       = array_merge( $v, array( 'id' => $v_id ) );
						$extra_multiplier = Kurs_Booking_Custom_Fields::calculate_extra_multiplier( $v_with_id, $data );
					}

					$total_price += $effective_price * $quantity * $extra_multiplier;

					$variante_id = (int) $variant_id; // Use last variant with qty > 0.
					break;
				}
			}
		}

		// Fallback for legacy single-variant booking.
		if ( $total_qty < 1 && isset( $data['anzahl'] ) ) {
			$total_qty   = absint( $data['anzahl'] );
			$variante_id = absint( $data['variante_id'] ?? 1 );
			$total_price = floatval( $data['total_price'] ?? 0 );
		}

		// Sprint 2.6: Calculate deposit with fallback to global settings.
		$deposit_enabled = (bool) Kurs_Booking_Kurs::get_meta( $data['kurs_id'], 'deposit_enabled', false );
		$deposit_percent = (int) Kurs_Booking_Kurs::get_meta( $data['kurs_id'], 'deposit_percent', 0 );

		// Fallback to global default deposit if kurs-specific is 0.
		if ( $deposit_enabled && 0 === $deposit_percent ) {
			$price_settings  = Kurs_Booking_Settings::get_price_settings();
			$deposit_percent = $price_settings['default_deposit_percent'];
		}

		$deposit_amount = $deposit_enabled ? ( $total_price * $deposit_percent / 100 ) : 0;

		// Save meta data.
		$meta_data = array(
			'kurs_id'        => absint( $data['kurs_id'] ),
			'variante_id'    => $variante_id,
			'tickets'        => wp_json_encode( $tickets ), // Store full ticket selection.
			'ticket_days'    => wp_json_encode( $ticket_days ), // Sprint 4.10: Days per variant for per_day pricing.
			'name'           => sanitize_text_field( $data['name'] ),
			'email'          => sanitize_email( $data['email'] ),
			'phone'          => sanitize_text_field( $data['phone'] ?? '' ),
			'message'        => sanitize_textarea_field( $data['message'] ?? '' ),
			'anzahl'         => $total_qty,
			'total_price'    => $total_price,
			'deposit_amount' => $deposit_amount,
			'status'         => self::STATUS_PENDING,
			'confirm_token'  => $confirm_token,
			'created_at'     => current_time( 'mysql' ),
		);

		if ( ! empty( $data['wunschtermin'] ) ) {
			$meta_data['wunschtermin'] = sanitize_text_field( $data['wunschtermin'] );
		}

		if ( ! empty( $data['custom_fields'] ) && is_array( $data['custom_fields'] ) ) {
			$meta_data['custom_fields'] = wp_json_encode( $data['custom_fields'] );
		}

		foreach ( $meta_data as $key => $value ) {
			update_post_meta( $post_id, self::META_PREFIX . $key, $value );
		}

		// Sprint 4.6: Store withdrawal consent for digital products (§356 Abs. 5 BGB).
		if ( ! empty( $data['withdrawal_consent'] ) ) {
			update_post_meta( $post_id, '_withdrawal_consent', '1' );
			update_post_meta( $post_id, '_withdrawal_consent_date', current_time( 'mysql' ) );
		}

		// Sprint 4.11: Save extra fields data (merged custom fields + variant extra fields).
		if ( class_exists( 'Kurs_Booking_Custom_Fields' ) ) {
			Kurs_Booking_Custom_Fields::save_to_booking( $post_id, $data );
		}

		// Sprint 2.6: Use Settings for booking number generation.
		$buchungs_nummer = Kurs_Booking_Settings::get_next_booking_number();
		wp_update_post(
			array(
				'ID'         => $post_id,
				'post_title' => $buchungs_nummer . ': ' . sanitize_text_field( $data['name'] ),
			)
		);

		/**
		 * Fires after a booking is created.
		 *
		 * @param int   $post_id The booking post ID.
		 * @param array $data    The booking data.
		 */
		do_action( 'kurs_booking_created', $post_id, $data );

		return $post_id;
	}

	/**
	 * Confirm a booking via token
	 *
	 * @param string $token The confirmation token.
	 * @return int|WP_Error Post ID on success, WP_Error on failure.
	 */
	public static function confirm_by_token( string $token ) {
		global $wpdb;

		// Find booking by token.
		$post_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_id FROM {$wpdb->postmeta}
                WHERE meta_key = %s AND meta_value = %s",
				self::META_PREFIX . 'confirm_token',
				$token
			)
		);

		if ( ! $post_id ) {
			return new WP_Error( 'invalid_token', __( 'Ungültiger Bestätigungs-Token', 'kurs-booking' ) );
		}

		// Check if already confirmed.
		$status = get_post_meta( $post_id, self::META_PREFIX . 'status', true );
		if ( self::STATUS_CONFIRMED === $status ) {
			return new WP_Error( 'already_confirmed', __( 'Buchung wurde bereits bestätigt', 'kurs-booking' ) );
		}

		// Sprint 2.6: Check token expiry using Settings.
		$booking_settings = Kurs_Booking_Settings::get_booking_settings();
		$validity_hours   = $booking_settings['confirm_validity_hours'];
		$created_at       = get_post_meta( $post_id, self::META_PREFIX . 'created_at', true );

		if ( $created_at && strtotime( $created_at ) < strtotime( "-{$validity_hours} hours" ) ) {
			return new WP_Error( 'token_expired', __( 'Bestätigungs-Link ist abgelaufen', 'kurs-booking' ) );
		}

		// Confirm booking.
		update_post_meta( $post_id, self::META_PREFIX . 'status', self::STATUS_CONFIRMED );
		update_post_meta( $post_id, self::META_PREFIX . 'confirmed_at', current_time( 'mysql' ) );

		/**
		 * Fires after a booking is confirmed.
		 *
		 * @param int $post_id The booking post ID.
		 */
		do_action( 'kurs_booking_confirmed', $post_id );

		return (int) $post_id;
	}

	/**
	 * Get meta value for a buchung
	 *
	 * @param int    $post_id The post ID.
	 * @param string $key     The meta key (without prefix).
	 * @param mixed  $default The default value.
	 * @return mixed
	 */
	public static function get_meta( int $post_id, string $key, mixed $default = '' ): mixed {
		$value = get_post_meta( $post_id, self::META_PREFIX . $key, true );
		return '' !== $value ? $value : $default;
	}

	/**
	 * Calculate cancellation fee based on days until course start.
	 *
	 * Uses settings from Tab 4 (Stornierung):
	 * - kurs_booking_cancel_free_days: Free cancellation until X days before
	 * - kurs_booking_cancel_partial_days: Partial fee until X days before
	 * - kurs_booking_cancel_partial_percent: Fee percentage for partial cancellation
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return array{
	 *   days_until_start: int,
	 *   cancel_fee_percent: int,
	 *   cancel_fee_amount: float,
	 *   refund_amount: float,
	 *   reason: string,
	 *   can_cancel: bool
	 * }
	 */
	public static function calculate_cancellation_fee( int $buchung_id, bool $has_replacement = false ): array {
		$kurs_id     = (int) self::get_meta( $buchung_id, 'kurs_id', 0 );
		$total_price = (float) self::get_meta( $buchung_id, 'total_price', 0 );

		// Digital products (Video-Kurs) cannot be cancelled.
		if ( Kurs_Booking_Kurs::is_digital_product( $kurs_id ) ) {
			return array(
				'days_until_start'   => 0,
				'cancel_fee_percent' => 100,
				'cancel_fee_amount'  => $total_price,
				'refund_amount'      => 0.00,
				'deposit_retained'   => 0.00,
				'reason'             => __( 'Digitales Produkt - keine Stornierung moeglich', 'kurs-booking' ),
				'can_cancel'         => false,
				'has_replacement'    => false,
			);
		}

		// Check deposit settings.
		$deposit_retained_setting = get_option( 'kurs_booking_cancel_deposit_retained', '1' );
		$deposit_enabled          = get_post_meta( $kurs_id, '_kurs_deposit_enabled', true );
		$deposit_percent          = (int) get_post_meta( $kurs_id, '_kurs_deposit_percent', true );

		// Calculate deposit amount (if applicable).
		$deposit_amount = 0.00;
		if ( '1' === $deposit_retained_setting && '1' === $deposit_enabled && $deposit_percent > 0 ) {
			$deposit_amount = round( $total_price * ( $deposit_percent / 100 ), 2 );
		}

		// Amount available for refund (excluding retained deposit).
		$refundable_amount = $total_price - $deposit_amount;

		$kurs_date = get_post_meta( $kurs_id, '_kurs_start_date', true );

		// Calculate days until course start.
		$start_timestamp  = strtotime( $kurs_date );
		$today_timestamp  = strtotime( 'today' );
		$days_until_start = (int) floor( ( $start_timestamp - $today_timestamp ) / DAY_IN_SECONDS );

		// Load cancellation settings.
		$free_days         = (int) get_option( 'kurs_booking_cancel_free_days', 28 );
		$partial_days      = (int) get_option( 'kurs_booking_cancel_partial_days', 14 );
		$partial_pct       = (int) get_option( 'kurs_booking_cancel_partial_percent', 50 );
		$allow_replacement = get_option( 'kurs_booking_cancel_allow_replacement', '1' );

		// Base result array.
		$result = array(
			'days_until_start' => $days_until_start,
			'deposit_retained' => $deposit_amount,
			'has_replacement'  => $has_replacement,
			'can_cancel'       => true,
		);

		// Course already started - no cancellation possible.
		if ( $days_until_start < 0 ) {
			return array_merge(
				$result,
				array(
					'cancel_fee_percent' => 100,
					'cancel_fee_amount'  => $total_price,
					'refund_amount'      => 0.00,
					'reason'             => __( 'Stornierung nicht mehr möglich (Kurs bereits gestartet)', 'kurs-booking' ),
					'can_cancel'         => false,
				)
			);
		}

		// Replacement participant - no cancellation fee (only deposit retained if applicable).
		if ( $has_replacement && '1' === $allow_replacement ) {
			return array_merge(
				$result,
				array(
					'cancel_fee_percent' => 0,
					'cancel_fee_amount'  => $deposit_amount,
					'refund_amount'      => $refundable_amount,
					'reason'             => $deposit_amount > 0
						? sprintf(
							/* translators: %s: deposit amount */
							__( 'Ersatzteilnehmer gestellt (Anzahlung %s wird einbehalten)', 'kurs-booking' ),
							number_format_i18n( $deposit_amount, 2 ) . ' ' . get_option( 'kurs_booking_currency', 'EUR' )
						)
						: __( 'Ersatzteilnehmer gestellt (volle Rückerstattung)', 'kurs-booking' ),
				)
			);
		}

		// Check cancellation conditions.
		if ( $days_until_start >= $free_days ) {
			// Free cancellation (deposit still retained).
			return array_merge(
				$result,
				array(
					'cancel_fee_percent' => 0,
					'cancel_fee_amount'  => $deposit_amount,
					'refund_amount'      => $refundable_amount,
					'reason'             => $deposit_amount > 0
						? sprintf(
							/* translators: 1: days, 2: deposit amount */
							__( 'Kostenfreie Stornierung (mehr als %1$d Tage vor Kursbeginn). Anzahlung %2$s wird einbehalten.', 'kurs-booking' ),
							$free_days,
							number_format_i18n( $deposit_amount, 2 ) . ' ' . get_option( 'kurs_booking_currency', 'EUR' )
						)
						: sprintf(
							/* translators: %d: number of days */
							__( 'Kostenfreie Stornierung (mehr als %d Tage vor Kursbeginn)', 'kurs-booking' ),
							$free_days
						),
				)
			);
		} elseif ( $days_until_start >= $partial_days ) {
			// Partial cancellation fee on refundable amount.
			$fee_on_refundable = round( $refundable_amount * ( $partial_pct / 100 ), 2 );
			$total_fee         = $deposit_amount + $fee_on_refundable;
			$refund            = round( $total_price - $total_fee, 2 );

			return array_merge(
				$result,
				array(
					'cancel_fee_percent' => $partial_pct,
					'cancel_fee_amount'  => $total_fee,
					'refund_amount'      => max( 0, $refund ),
					'reason'             => $deposit_amount > 0
						? sprintf(
							/* translators: 1: fee percentage, 2: partial days, 3: free days minus 1 */
							__( 'Stornogebühr %1$d%% + Anzahlung einbehalten (%2$d-%3$d Tage vor Kursbeginn)', 'kurs-booking' ),
							$partial_pct,
							$partial_days,
							$free_days - 1
						)
						: sprintf(
							/* translators: 1: fee percentage, 2: partial days, 3: free days minus 1 */
							__( 'Stornogebühr %1$d%% (%2$d-%3$d Tage vor Kursbeginn)', 'kurs-booking' ),
							$partial_pct,
							$partial_days,
							$free_days - 1
						),
				)
			);
		} else {
			// No refund.
			return array_merge(
				$result,
				array(
					'cancel_fee_percent' => 100,
					'cancel_fee_amount'  => $total_price,
					'refund_amount'      => 0.00,
					'reason'             => sprintf(
						/* translators: %d: number of days */
						__( 'Keine Rückerstattung (weniger als %d Tage vor Kursbeginn)', 'kurs-booking' ),
						$partial_days
					),
				)
			);
		}
	}

	/**
	 * Cancel a booking.
	 *
	 * @param int         $buchung_id      Booking post ID.
	 * @param string      $reason          Cancellation reason.
	 * @param int|null    $fee_override    Optional override for fee percentage.
	 * @param string|null $notes           Optional admin notes.
	 * @param bool        $has_replacement Whether a replacement participant is provided.
	 * @return bool|WP_Error True on success, WP_Error on failure.
	 */
	public static function cancel( int $buchung_id, string $reason = 'customer_request', ?int $fee_override = null, ?string $notes = null, bool $has_replacement = false ): bool|WP_Error {
		$current_status = self::get_meta( $buchung_id, 'status', '' );

		// Only confirmed bookings can be cancelled.
		if ( self::STATUS_CONFIRMED !== $current_status ) {
			return new WP_Error(
				'invalid_status',
				__( 'Nur bestätigte Buchungen können storniert werden.', 'kurs-booking' )
			);
		}

		// Calculate fee (with replacement consideration).
		$fee_data = self::calculate_cancellation_fee( $buchung_id, $has_replacement );

		// Check if cancellation is allowed.
		if ( ! $fee_data['can_cancel'] && null === $fee_override ) {
			return new WP_Error(
				'cannot_cancel',
				$fee_data['reason']
			);
		}

		// Apply fee override if provided (admin override).
		if ( null !== $fee_override ) {
			$total_price    = (float) self::get_meta( $buchung_id, 'total_price', 0 );
			$fee_override   = max( 0, min( 100, $fee_override ) ); // Clamp 0-100.
			$fee_amount     = round( $total_price * ( $fee_override / 100 ), 2 );
			$refund_amount  = round( $total_price - $fee_amount, 2 );
			$fee_percent    = $fee_override;
			$fee_reason     = __( 'Admin-Override', 'kurs-booking' );
		} else {
			$fee_amount    = $fee_data['cancel_fee_amount'];
			$refund_amount = $fee_data['refund_amount'];
			$fee_percent   = $fee_data['cancel_fee_percent'];
			$fee_reason    = $fee_data['reason'];
		}

		// Update status.
		update_post_meta( $buchung_id, self::META_PREFIX . 'status', self::STATUS_CANCELLED );

		// Save cancellation meta.
		update_post_meta( $buchung_id, '_cancel_requested_at', current_time( 'mysql' ) );
		update_post_meta( $buchung_id, '_cancel_confirmed_at', current_time( 'mysql' ) );
		update_post_meta( $buchung_id, '_cancel_fee_percent', $fee_percent );
		update_post_meta( $buchung_id, '_cancel_fee_amount', $fee_amount );
		update_post_meta( $buchung_id, '_cancel_refund_amount', $refund_amount );
		update_post_meta( $buchung_id, '_cancel_reason', sanitize_text_field( $reason ) );
		update_post_meta( $buchung_id, '_cancel_fee_reason', $fee_reason );
		update_post_meta( $buchung_id, '_cancel_has_replacement', $has_replacement ? '1' : '0' );

		// Save deposit retained amount if applicable.
		if ( isset( $fee_data['deposit_retained'] ) && $fee_data['deposit_retained'] > 0 ) {
			update_post_meta( $buchung_id, '_cancel_deposit_retained', $fee_data['deposit_retained'] );
		}

		if ( $notes ) {
			update_post_meta( $buchung_id, '_cancel_notes', sanitize_textarea_field( $notes ) );
		}

		/**
		 * Fires after a booking is cancelled.
		 *
		 * @param int    $buchung_id      The booking post ID.
		 * @param float  $refund_amount   Amount to refund.
		 * @param float  $fee_amount      Cancellation fee amount.
		 * @param string $reason          Cancellation reason.
		 * @param bool   $has_replacement Whether a replacement participant was provided.
		 */
		do_action( 'kurs_booking_cancelled', $buchung_id, $refund_amount, $fee_amount, $reason, $has_replacement );

		return true;
	}

	/**
	 * Get booking by cancellation token.
	 *
	 * @param string $token Cancellation token.
	 * @return int|null Booking post ID or null if not found.
	 */
	public static function get_by_cancel_token( string $token ): ?int {
		global $wpdb;

		if ( empty( $token ) || strlen( $token ) < 32 ) {
			return null;
		}

		$post_id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT p.ID
                FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                WHERE p.post_type = %s
                AND p.post_status = 'publish'
                AND pm.meta_key = %s
                AND pm.meta_value = %s
                LIMIT 1",
				self::POST_TYPE,
				'_cancel_token',
				$token
			)
		);

		return $post_id ? (int) $post_id : null;
	}

	/**
	 * Generate cancellation token for a booking.
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return string The cancellation token.
	 */
	public static function generate_cancel_token( int $buchung_id ): string {
		$token = bin2hex( random_bytes( 32 ) );
		update_post_meta( $buchung_id, '_cancel_token', $token );
		return $token;
	}

	/**
	 * Get cancellation data for a booking.
	 *
	 * @param int $buchung_id Booking post ID.
	 * @return array<string, mixed>|null Cancellation data or null if not cancelled.
	 */
	public static function get_cancellation_data( int $buchung_id ): ?array {
		$status = self::get_meta( $buchung_id, 'status', '' );

		if ( self::STATUS_CANCELLED !== $status ) {
			return null;
		}

		return array(
			'requested_at'      => get_post_meta( $buchung_id, '_cancel_requested_at', true ),
			'confirmed_at'      => get_post_meta( $buchung_id, '_cancel_confirmed_at', true ),
			'fee_percent'       => (int) get_post_meta( $buchung_id, '_cancel_fee_percent', true ),
			'fee_amount'        => (float) get_post_meta( $buchung_id, '_cancel_fee_amount', true ),
			'refund_amount'     => (float) get_post_meta( $buchung_id, '_cancel_refund_amount', true ),
			'reason'            => get_post_meta( $buchung_id, '_cancel_reason', true ),
			'fee_reason'        => get_post_meta( $buchung_id, '_cancel_fee_reason', true ),
			'notes'             => get_post_meta( $buchung_id, '_cancel_notes', true ),
			'has_replacement'   => '1' === get_post_meta( $buchung_id, '_cancel_has_replacement', true ),
			'deposit_retained'  => (float) get_post_meta( $buchung_id, '_cancel_deposit_retained', true ),
		);
	}
}
