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

namespace Hafenstudios\Leadlotse\Frontend;

use Hafenstudios\Core\Events\EventBus;
use Hafenstudios\Leadlotse\Events\EventCapacity;
use Hafenstudios\Leadlotse\Fields\ConditionalLogic;
use Hafenstudios\Leadlotse\Fields\FieldRegistry;
use Hafenstudios\Leadlotse\Fields\Types\HoneypotField;
use Hafenstudios\Leadlotse\Forms\FormPostType;
use Hafenstudios\Leadlotse\Identity\IdentityService;
use Hafenstudios\Leadlotse\Submit\SpamGuard;
use Hafenstudios\Leadlotse\WhatsApp\ClickToChat;
use Hafenstudios\Leadlotse\WhatsApp\OptinCollector;

defined( 'ABSPATH' ) || exit;

/**
 * Frontend-Renderer (Etappe 3, Punkt 1): erzeugt aus der validierten
 * Formular-Definition (`_leadlotse_definition`, D4) valides, barrierefreies HTML.
 *
 * Grundsaetze:
 * - Progressive enhancement: das Markup funktioniert komplett ohne JavaScript
 *   ueber den admin-post.php-Fallback (D8, FallbackHandler). JS haengt sich an
 *   dieselbe Struktur und sendet ueber REST `leadlotse/v1/submit`.
 * - Barrierefrei: jedes Feld hat ein Label, Hilfetexte werden per
 *   aria-describedby verknuepft, Fehler stehen inline am Feld (role="alert").
 * - Escaping out: alle dynamischen Werte werden beim Ausgeben escaped.
 * - Honeypot unsichtbar (CSS, nicht display:none allein) mit tabindex=-1 und
 *   autocomplete=off; die Auswertung laeuft in der Spam-Kette (D7).
 * - Conditional Logic: die Regeln werden als data-Attribut mitgegeben; das
 *   Ein-/Ausblenden passiert clientseitig, die verbindliche Auswertung
 *   serverseitig beim Submit.
 */
final class FormRenderer {

	private FieldRegistry $registry;

	private ConditionalLogic $conditions;

	private SpamGuard $spam;

	private ?IdentityService $identity;

	private ?EventCapacity $capacity;

	/**
	 * @param FieldRegistry        $registry   Feldtyp-Registry.
	 * @param ConditionalLogic     $conditions Conditional-Logic-Service.
	 * @param SpamGuard            $spam       Spam-Kette (fuer den Zeitstempel-Token).
	 * @param IdentityService|null $identity   Ausfuellhilfe (optional; ohne bleibt der Block aus).
	 * @param EventCapacity|null   $capacity   Kapazitaetslogik (L-R1; ohne keine Ausgebucht-Anzeige).
	 */
	public function __construct( FieldRegistry $registry, ConditionalLogic $conditions, SpamGuard $spam, ?IdentityService $identity = null, ?EventCapacity $capacity = null ) {
		$this->registry   = $registry;
		$this->conditions = $conditions;
		$this->spam       = $spam;
		$this->identity   = $identity;
		$this->capacity   = $capacity;
	}

	/**
	 * Rendert ein veroeffentlichtes Formular anhand seiner ID.
	 *
	 * Beruecksichtigt automatisch das Ergebnis eines No-JS-Submits, das der
	 * FallbackHandler ueber die Query-Parameter ll_success/ll_error zurueckgibt.
	 *
	 * @param int $form_id Formular-ID.
	 * @return string HTML oder ein Hinweis, wenn das Formular fehlt.
	 */
	public function render_by_id( int $form_id ): string {
		$post       = get_post( $form_id );
		$definition = FormPostType::get_definition( $form_id );

		if ( ! $post || FormPostType::POST_TYPE !== $post->post_type || null === $definition ) {
			return $this->notice( __( 'Dieses Formular ist nicht verfuegbar. Bitte pruefen Sie die Formular-ID oder legen Sie das Formular im Bereich Leadlotse neu an.', 'leadlotse' ) );
		}

		if ( 'publish' !== $post->post_status && ! current_user_can( 'hs_manage_forms' ) ) {
			return $this->notice( __( 'Dieses Formular ist noch nicht veroeffentlicht.', 'leadlotse' ) );
		}

		// Ausgebucht (L-R1): statt des Formulars ein klarer Hinweis. Die
		// verbindliche Pruefung laeuft zusaetzlich serverseitig beim Submit.
		if ( null !== $this->capacity && $this->capacity->is_full( $form_id, $definition ) ) {
			return $this->notice( __( 'Diese Veranstaltung ist ausgebucht. Es sind leider keine Plaetze mehr frei.', 'leadlotse' ) );
		}

		$args = $this->result_from_request( $form_id, $definition );
		$args = $this->apply_identity_prefill( $definition, $args );

		return $this->render( $form_id, $definition, $args );
	}

	/**
	 * Uebernimmt frisch verifizierte Daten aus einem Ausfuellhilfe-Ruecksprung.
	 *
	 * Liest das Einmal-Handoff-Token aus `ll_identity`, belegt das erste E-Mail-Feld
	 * sowie vorhandene Namensfelder vor, zeigt eine Erfolgs-Notice und reicht eine
	 * signierte Assertion an das Formular durch. `ll_id_error` erzeugt eine dezente
	 * Fehler-Notice; ein Abbruch (`ll_id_cancel`) bleibt bewusst ohne Meldung.
	 *
	 * @param array<string, mixed> $definition Formular-Definition.
	 * @param array<string, mixed> $args       Bisherige Render-Argumente.
	 * @return array<string, mixed>
	 */
	private function apply_identity_prefill( array $definition, array $args ): array {
		if ( null === $this->identity ) {
			return $args;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nur Vorbelegung nach Redirect, verbindliche Pruefung laeuft ueber signierte Assertion beim Submit.
		if ( isset( $_GET['ll_id_error'] ) && ! isset( $args['result'] ) ) {
			$args['result'] = array(
				'type'    => 'error',
				'message' => __( 'Die Uebernahme hat nicht geklappt. Bitte fuellen Sie das Formular von Hand aus.', 'leadlotse' ),
			);
			return $args;
		}

		if ( ! isset( $_GET['ll_identity'] ) ) {
			return $args;
		}
		$token = preg_replace( '/[^A-Za-z0-9]/', '', (string) wp_unslash( $_GET['ll_identity'] ) );
		// phpcs:enable
		if ( '' === $token || strlen( $token ) > 64 ) {
			return $args;
		}

		$data = $this->identity->consume_handoff( $token );
		if ( null === $data ) {
			return $args;
		}

		$fields = array_values( (array) ( $definition['fields'] ?? array() ) );
		$values = (array) ( $args['values'] ?? array() );
		$email  = (string) ( $data['email'] ?? '' );
		$name   = (string) ( $data['name'] ?? '' );

		$email_field = $this->first_field_id_of_type( $fields, 'email' );
		if ( '' !== $email_field && '' !== $email ) {
			$values[ $email_field ] = $email;
		}

		if ( '' !== $name ) {
			$parts = preg_split( '/\s+/', $name, 2 );
			$first = (string) ( $parts[0] ?? '' );
			$last  = (string) ( $parts[1] ?? '' );
			if ( $this->has_field_id( $fields, 'name' ) ) {
				$values['name'] = $name;
			}
			if ( '' !== $first && $this->has_field_id( $fields, 'first_name' ) ) {
				$values['first_name'] = $first;
			}
			if ( '' !== $last && $this->has_field_id( $fields, 'last_name' ) ) {
				$values['last_name'] = $last;
			}
		}

		$args['values'] = $values;

		$provider_key   = (string) ( $data['provider'] ?? '' );
		$provider_label = $this->provider_label( $provider_key );
		if ( ! isset( $args['result'] ) && '' !== $email ) {
			$args['result'] = array(
				'type'    => 'success',
				'message' => sprintf(
					/* translators: 1: Anbietername, 2: E-Mail-Adresse */
					__( 'Uebernommen von %1$s: %2$s. Sie koennen alle Angaben noch aendern.', 'leadlotse' ),
					$provider_label,
					$email
				),
			);
		}

		if ( '' !== $email ) {
			$args['identity_assertion'] = $this->identity->create_assertion( $email, $provider_key );
		}

		return $args;
	}

	/**
	 * Anzeigename eines Anbieters (aus dem konfigurierten Provider, sonst Titelform).
	 *
	 * @param string $key Anbieter-Schluessel.
	 * @return string
	 */
	private function provider_label( string $key ): string {
		if ( null !== $this->identity ) {
			$provider = $this->identity->provider( $key );
			if ( null !== $provider ) {
				return $provider->label();
			}
		}
		return '' === $key ? '' : ucfirst( $key );
	}

	/**
	 * Erste Feld-ID eines bestimmten Typs.
	 *
	 * @param array<int, mixed> $fields Felder.
	 * @param string            $type   Feldtyp.
	 * @return string
	 */
	private function first_field_id_of_type( array $fields, string $type ): string {
		foreach ( $fields as $field ) {
			if ( is_array( $field ) && $type === (string) ( $field['type'] ?? '' ) ) {
				return (string) ( $field['id'] ?? '' );
			}
		}
		return '';
	}

	/**
	 * Existiert ein Feld mit dieser ID?
	 *
	 * @param array<int, mixed> $fields Felder.
	 * @param string            $id     Feld-ID.
	 * @return bool
	 */
	private function has_field_id( array $fields, string $id ): bool {
		foreach ( $fields as $field ) {
			if ( is_array( $field ) && $id === (string) ( $field['id'] ?? '' ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Rendert ein Formular aus einer Definition.
	 *
	 * @param int                  $form_id    Formular-ID.
	 * @param array<string, mixed> $definition Validierte Definition.
	 * @param array<string, mixed> $args       Optional: 'errors' (feld_id => Meldung),
	 *                                          'values' (feld_id => Wert), 'result'
	 *                                          (['type'=>'success'|'error','message'=>...]).
	 * @return string HTML.
	 */
	public function render( int $form_id, array $definition, array $args = array() ): string {
		$fields   = array_values( (array) ( $definition['fields'] ?? array() ) );
		$settings = (array) ( $definition['settings'] ?? array() );
		$errors   = (array) ( $args['errors'] ?? array() );
		$values   = (array) ( $args['values'] ?? array() );
		$result   = is_array( $args['result'] ?? null ) ? $args['result'] : array();

		$action = function_exists( 'admin_url' ) ? admin_url( 'admin-post.php' ) : '';
		$submit = function_exists( 'rest_url' ) ? rest_url( 'leadlotse/v1/submit' ) : '';
		$token  = $this->spam->create_timestamp_token();
		$label  = (string) ( $settings['submit_label'] ?? '' );
		if ( '' === $label ) {
			$label = __( 'Absenden', 'leadlotse' );
		}

		$html  = '<form class="leadlotse-form" method="post" action="' . esc_url( $action ) . '"';
		$html .= ' data-leadlotse-form="1" data-form-id="' . esc_attr( (string) $form_id ) . '"';
		$html .= ' data-rest="' . esc_url( $submit ) . '">';

		// Pflichtfelder fuer die Submit-Pipeline (D8).
		$html .= '<input type="hidden" name="action" value="' . esc_attr( \Hafenstudios\Leadlotse\Submit\FallbackHandler::ACTION ) . '" />';
		$html .= '<input type="hidden" name="form_id" value="' . esc_attr( (string) $form_id ) . '" />';
		$html .= '<input type="hidden" name="' . esc_attr( SpamGuard::TIMESTAMP_FIELD ) . '" value="' . esc_attr( $token ) . '" />';

		// Signierte Assertion einer zuvor verifizierten E-Mail (Ausfuellhilfe).
		$assertion = (string) ( $args['identity_assertion'] ?? '' );
		if ( '' !== $assertion ) {
			$html .= '<input type="hidden" name="fields[_id_assert]" value="' . esc_attr( $assertion ) . '" />';
		}

		/**
		 * Erlaubt einem Dritt-Plugin (Wellenbrecher), Hidden-Felder in das
		 * Leadlotse-Formular zu injizieren und dabei ein Skript einzureihen
		 * (Anti-Spam-Signale + signals.js auf Seiten mit Leadlotse-Formular).
		 * Rueckgabe ist HTML, das direkt in das Formular uebernommen wird; der
		 * Konsument ist fuer sicheres Markup verantwortlich (wie bei jedem
		 * HTML-liefernden Filter).
		 *
		 * @param string               $html       Bisheriges Injektions-HTML (leer).
		 * @param int                  $form_id    Formular-ID.
		 * @param array<string, mixed> $definition Formular-Definition.
		 */
		$injected = EventBus::filter( 'leadlotse_form_fields', '', $form_id, $definition );
		if ( is_string( $injected ) && '' !== $injected ) {
			$html .= $injected;
		}

		// Status-Bereich: serverseitig vorbefuellt (No-JS), von JS wiederverwendet.
		$html .= $this->status_region( $result );

		// Ausfuellhilfe "Weiter mit Google/Apple" (nur wenn eingerichtet).
		$html .= $this->identity_block( $form_id, $fields, $settings );

		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			$type = (string) ( $field['type'] ?? '' );
			if ( 'honeypot' === $type ) {
				// Honeypot wird immer automatisch ergaenzt, nie aus der Definition.
				continue;
			}
			$html .= $this->render_field( $field, $form_id, $errors, $values );
		}

		// Honeypot automatisch anhaengen (BAUPLAN 12, D7).
		$html .= $this->render_honeypot();

		$html .= '<div class="leadlotse-field leadlotse-field--submit">';
		$html .= '<button type="submit" class="leadlotse-submit">' . esc_html( $label ) . '</button>';
		$html .= '</div>';

		// WhatsApp-Alternative (Click-to-Chat), noch innerhalb des Formulars.
		$html .= $this->whatsapp_block( $settings );

		$html .= '</form>';

		return $html;
	}

	/**
	 * Rendert den WhatsApp-Click-to-Chat-Block, wenn er aktiviert ist und die
	 * hinterlegte Nummer eine gueltige wa.me-URL ergibt. Der DSGVO-Hinweis steht
	 * immer sichtbar vor dem Klick (kein Zwei-Klick-JS, No-JS-Paritaet).
	 *
	 * @param array<string, mixed> $settings Formular-Einstellungen.
	 * @return string
	 */
	private function whatsapp_block( array $settings ): string {
		$whatsapp = is_array( $settings['whatsapp'] ?? null ) ? $settings['whatsapp'] : array();
		if ( empty( $whatsapp['enabled'] ) ) {
			return '';
		}

		$number  = (string) ( $whatsapp['number'] ?? '' );
		$prefill = (string) ( $whatsapp['prefill_text'] ?? '' );
		$url     = ClickToChat::chat_url( $number, $prefill );
		if ( '' === $url ) {
			return '';
		}

		/**
		 * Erweiterungspunkt fuer eine spaetere Business-API-Anbindung: erlaubt es,
		 * die Click-to-Chat-URL zu ersetzen. Ein Leerstring unterdrueckt den Block.
		 *
		 * @param string $url     Click-to-Chat-URL.
		 * @param string $number  Rohnummer aus den Einstellungen.
		 * @param string $prefill Vorbefuellter Text.
		 */
		$url = (string) EventBus::filter( 'leadlotse_whatsapp_url', $url, $number, $prefill );
		if ( '' === $url ) {
			return '';
		}

		$label = (string) ( $whatsapp['button_label'] ?? '' );
		if ( '' === $label ) {
			$label = __( 'Lieber per WhatsApp schreiben', 'leadlotse' );
		}

		$hint = (string) ( $whatsapp['hint_text'] ?? '' );
		if ( '' === $hint ) {
			$hint = __( 'WhatsApp ist ein Dienst der Meta Platforms Ireland Limited. Wenn Sie den Chat oeffnen, werden Daten an WhatsApp uebertragen, auch in Laender ausserhalb der EU. Es gilt die Datenschutzerklaerung von WhatsApp.', 'leadlotse' );
		}

		$out  = '<div class="leadlotse-whatsapp">';
		$out .= '<span class="leadlotse-whatsapp-or">' . esc_html__( 'oder', 'leadlotse' ) . '</span>';
		$out .= '<a class="leadlotse-whatsapp-link" href="' . esc_url( $url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $label ) . '</a>';
		$out .= '<p class="leadlotse-whatsapp-hint">' . esc_html( $hint ) . '</p>';
		$out .= '</div>';

		return $out;
	}

	/**
	 * Rendert den Ausfuellhilfe-Block ("Weiter mit Google/Apple").
	 *
	 * Nur sichtbar, wenn ein IdentityService vorhanden ist, die Formular-Einstellung
	 * settings.identity.enabled aktiv ist (Default an), mindestens ein Anbieter
	 * vollstaendig eingerichtet ist und das Formular ein E-Mail-Feld enthaelt. Es
	 * werden nur Textknoepfe erzeugt, keine Marken-Logos oder externen Ressourcen.
	 *
	 * @param int                  $form_id  Formular-ID.
	 * @param array<int, mixed>    $fields   Felder.
	 * @param array<string, mixed> $settings Formular-Einstellungen.
	 * @return string
	 */
	private function identity_block( int $form_id, array $fields, array $settings ): string {
		if ( null === $this->identity ) {
			return '';
		}

		$identity_settings = is_array( $settings['identity'] ?? null ) ? $settings['identity'] : array();
		if ( isset( $identity_settings['enabled'] ) && empty( $identity_settings['enabled'] ) ) {
			return '';
		}

		if ( '' === $this->first_field_id_of_type( $fields, 'email' ) ) {
			return '';
		}

		$providers = $this->identity->providers();
		if ( array() === $providers ) {
			return '';
		}

		$return_url = $this->current_url();
		$start_base = function_exists( 'rest_url' ) ? rest_url( 'leadlotse/v1/identity/start' ) : '';

		$out = '<div class="leadlotse-identity">';
		foreach ( $providers as $key => $provider ) {
			$start = add_query_arg(
				array(
					'provider' => $key,
					'form'     => $form_id,
					'return'   => $return_url,
				),
				$start_base
			);
			$out  .= '<a class="leadlotse-identity-btn leadlotse-identity-btn--' . esc_attr( sanitize_html_class( (string) $key ) ) . '" href="' . esc_url( $start ) . '">'
				. esc_html( sprintf( /* translators: %s: Anbietername */ __( 'Weiter mit %s', 'leadlotse' ), $provider->label() ) )
				. '</a>';
		}
		$out .= '<p class="leadlotse-identity-hint">' . esc_html( IdentityService::hint_text() ) . '</p>';
		$out .= '<span class="leadlotse-identity-or">' . esc_html__( 'oder klassisch ausfuellen', 'leadlotse' ) . '</span>';
		$out .= '</div>';

		return $out;
	}

	/**
	 * Aktuelle Seiten-URL fuer den Ruecksprung nach der Ausfuellhilfe.
	 *
	 * @return string
	 */
	private function current_url(): string {
		if ( function_exists( 'get_permalink' ) ) {
			$permalink = get_permalink();
			if ( is_string( $permalink ) && '' !== $permalink ) {
				return $permalink;
			}
		}
		if ( function_exists( 'home_url' ) ) {
			$uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw saeubert.
			return '' !== $uri ? home_url( $uri ) : home_url( '/' );
		}
		return '';
	}

	/**
	 * Rendert ein einzelnes Feld inklusive Label, Hilfetext und Fehlerplatz.
	 *
	 * @param array<string, mixed> $field   Felddefinition.
	 * @param int                  $form_id Formular-ID.
	 * @param array<string, mixed> $errors  Fehler je Feld-ID.
	 * @param array<string, mixed> $values  Vorbelegte Werte je Feld-ID.
	 * @return string
	 */
	private function render_field( array $field, int $form_id, array $errors, array $values ): string {
		$type_key = (string) ( $field['type'] ?? '' );
		$type     = $this->registry->get( $type_key );
		if ( null === $type ) {
			return '';
		}

		$data       = $type->render_data( $field );
		$id         = (string) ( $data['id'] ?? '' );
		if ( '' === $id ) {
			return '';
		}
		$dom_id     = 'll-' . $form_id . '-' . $id;
		$required   = ! empty( $data['required'] );
		$conditions = is_array( $data['conditions'] ?? null ) ? $data['conditions'] : array();
		$has_cond   = ! empty( $conditions['enabled'] );
		$error      = isset( $errors[ $id ] ) ? (string) $errors[ $id ] : '';
		$value      = array_key_exists( $id, $values ) ? (string) $values[ $id ] : (string) ( $data['default'] ?? '' );

		// Hidden-Feld: kein sichtbares Markup, nur der Eingabewert (Attribution).
		if ( 'hidden' === $type_key ) {
			return '<input type="hidden" name="' . esc_attr( 'fields[' . $id . ']' ) . '"'
				. ' id="' . esc_attr( $dom_id ) . '"'
				. ' value="' . esc_attr( $value ) . '"'
				. ' data-capture="' . esc_attr( (string) ( $data['capture'] ?? '' ) ) . '" />';
		}

		// Beschreibungs-IDs fuer aria-describedby (Hilfetext + Fehler).
		$describedby = array();
		$help        = (string) ( $data['help'] ?? '' );
		if ( '' !== $help ) {
			$describedby[] = $dom_id . '-help';
		}
		$describedby[] = $dom_id . '-error';

		$wrapper_classes = array( 'leadlotse-field', 'leadlotse-field--' . sanitize_html_class( $type_key ) );
		if ( '' !== $error ) {
			$wrapper_classes[] = 'leadlotse-field--has-error';
		}

		$wrapper  = '<div class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '"';
		$wrapper .= ' data-field-id="' . esc_attr( $id ) . '"';
		if ( $has_cond ) {
			$wrapper .= ' data-conditions="' . esc_attr( (string) wp_json_encode( $conditions ) ) . '"';
		}
		$wrapper .= '>';

		// WhatsApp-Opt-in-Checkbox (nur phone): Vorbelegung aus dem technischen Wert.
		$wa_checked = '1' === (string) ( $values[ '_wa_' . $id ] ?? '' );
		$control    = $this->render_control( $type_key, $data, $dom_id, $id, $value, $required, $has_cond, $describedby, $wa_checked );

		// Bei consent/checkbox steht das Label rechts vom Control (im Control gebaut).
		if ( in_array( $type_key, array( 'consent', 'checkbox' ), true ) ) {
			$inner = $control;
		} else {
			$inner  = $this->label( $dom_id, (string) ( $data['label'] ?? '' ), $required, 'select_radio' === $this->layout( $type_key, $data ) );
			$inner .= $control;
		}

		$html  = $wrapper . $inner;
		$html .= $this->help_html( $dom_id, $help );
		$html .= $this->error_html( $dom_id, $error );
		$html .= '</div>';

		return $html;
	}

	/**
	 * Baut das eigentliche Eingabe-Control je Feldtyp.
	 *
	 * @param string               $type_key    Feldtyp.
	 * @param array<string, mixed> $data        Render-Daten.
	 * @param string               $dom_id      HTML-id des Controls.
	 * @param string               $field_id    Feld-ID (Name).
	 * @param string               $value       Vorbelegter Wert.
	 * @param bool                 $required    Pflichtfeld.
	 * @param bool                 $has_cond    Hat Conditional Logic (dann kein hartes required-Attribut).
	 * @param array<int, string>   $describedby aria-describedby-IDs.
	 * @param bool                 $wa_checked  WhatsApp-Opt-in vorbelegt (nur phone).
	 * @return string
	 */
	private function render_control( string $type_key, array $data, string $dom_id, string $field_id, string $value, bool $required, bool $has_cond, array $describedby, bool $wa_checked = false ): string {
		$name      = 'fields[' . $field_id . ']';
		$aria      = ' aria-describedby="' . esc_attr( implode( ' ', $describedby ) ) . '"';
		$req_attr  = $required ? ' aria-required="true"' : '';
		// Hartes required nur ohne Conditional Logic (sonst blockiert es No-JS-Submits verborgener Felder).
		$req_attr .= ( $required && ! $has_cond ) ? ' required' : '';
		$place     = (string) ( $data['placeholder'] ?? '' );
		$place_attr = '' !== $place ? ' placeholder="' . esc_attr( $place ) . '"' : '';

		switch ( $type_key ) {
			case 'textarea':
				return '<textarea class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"'
					. $place_attr . $aria . $req_attr . ' rows="5">' . esc_textarea( $value ) . '</textarea>';

			case 'select':
				return $this->render_select( $data, $dom_id, $name, $value, $required, $has_cond, $aria );

			case 'checkbox':
				return $this->render_checkbox( $dom_id, $name, (string) ( $data['label'] ?? '' ), $value, $required, $has_cond, $aria );

			case 'consent':
				return $this->render_consent( $dom_id, $name, $data, $value, $has_cond, $aria );

			case 'date':
				return '<input type="date" class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"'
					. ' value="' . esc_attr( $value ) . '"' . $aria . $req_attr . ' />';

			case 'email':
				return '<input type="email" class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"'
					. ' value="' . esc_attr( $value ) . '" autocomplete="email"' . $place_attr . $aria . $req_attr . ' />';

			case 'number':
				$min      = absint( $data['min'] ?? 0 );
				$max      = absint( $data['max'] ?? 0 );
				$max_attr = $max > 0 ? ' max="' . esc_attr( (string) $max ) . '"' : '';
				return '<input type="number" class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"'
					. ' value="' . esc_attr( $value ) . '" inputmode="numeric" step="1" min="' . esc_attr( (string) $min ) . '"' . $max_attr
					. $place_attr . $aria . $req_attr . ' />';

			case 'phone':
				$phone = '<input type="tel" class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"'
					. ' value="' . esc_attr( $value ) . '" autocomplete="tel"' . $place_attr . $aria . $req_attr . ' />';
				if ( ! empty( $data['whatsapp_optin'] ) ) {
					$phone .= $this->whatsapp_optin_control( $data, $dom_id, $field_id, $wa_checked );
				}
				return $phone;

			case 'text':
			default:
				return '<input type="text" class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"'
					. ' value="' . esc_attr( $value ) . '"' . $place_attr . $aria . $req_attr . ' />';
		}
	}

	/**
	 * WhatsApp-Opt-in-Checkbox unter einem Telefon-Feld. Nie Pflicht. Der Wert
	 * landet als technischer Eingabewert `_wa_{feldid}` in der Submit-Pipeline.
	 *
	 * @param array<string, mixed> $data     Render-Daten (whatsapp_optin_text).
	 * @param string               $dom_id   HTML-id des Telefon-Controls.
	 * @param string               $field_id Feld-ID.
	 * @param bool                 $checked  Vorbelegung.
	 * @return string
	 */
	private function whatsapp_optin_control( array $data, string $dom_id, string $field_id, bool $checked ): string {
		$text = (string) ( $data['whatsapp_optin_text'] ?? '' );
		if ( '' === $text ) {
			$text = OptinCollector::default_optin_text();
		}
		$wa_id        = $dom_id . '-wa';
		$wa_name      = 'fields[_wa_' . $field_id . ']';
		$checked_attr = $checked ? ' checked' : '';

		$out  = '<label class="leadlotse-choice leadlotse-choice--wa-optin" for="' . esc_attr( $wa_id ) . '">';
		$out .= '<input type="checkbox" id="' . esc_attr( $wa_id ) . '" name="' . esc_attr( $wa_name ) . '" value="1"' . $checked_attr . ' /> ';
		$out .= '<span>' . esc_html( $text ) . '</span></label>';
		return $out;
	}

	/**
	 * Auswahlfeld als Dropdown oder Radiogruppe.
	 *
	 * @param array<string, mixed> $data     Render-Daten.
	 * @param string               $dom_id   HTML-id.
	 * @param string               $name     Feldname.
	 * @param string               $value    Vorauswahl.
	 * @param bool                 $required Pflichtfeld.
	 * @param bool                 $has_cond Conditional Logic aktiv.
	 * @param string               $aria     aria-describedby-Attribut.
	 * @return string
	 */
	private function render_select( array $data, string $dom_id, string $name, string $value, bool $required, bool $has_cond, string $aria ): string {
		$options = is_array( $data['options'] ?? null ) ? $data['options'] : array();
		$display = 'radio' === (string) ( $data['display'] ?? 'dropdown' ) ? 'radio' : 'dropdown';

		if ( 'radio' === $display ) {
			// Radiogruppe: die Beschreibung haengt am Fieldset (siehe render_field/legend).
			$out = '<div class="leadlotse-radio-group" role="radiogroup"' . $aria . '>';
			$i   = 0;
			foreach ( $options as $option ) {
				$opt_value = (string) ( $option['value'] ?? '' );
				$opt_label = (string) ( $option['label'] ?? $opt_value );
				$opt_id    = $dom_id . '-' . $i;
				$checked   = $opt_value === $value ? ' checked' : '';
				$req_attr  = ( $required && ! $has_cond ) ? ' required' : '';
				$out      .= '<label class="leadlotse-choice" for="' . esc_attr( $opt_id ) . '">';
				$out      .= '<input type="radio" id="' . esc_attr( $opt_id ) . '" name="' . esc_attr( $name ) . '"'
					. ' value="' . esc_attr( $opt_value ) . '"' . $checked . $req_attr . ' /> ';
				$out      .= '<span>' . esc_html( $opt_label ) . '</span></label>';
				++$i;
			}
			$out .= '</div>';
			return $out;
		}

		$req_attr = ( $required && ! $has_cond ) ? ' required' : '';
		$out      = '<select class="leadlotse-input" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '"' . $aria . ( $required ? ' aria-required="true"' : '' ) . $req_attr . '>';
		$out     .= '<option value="">' . esc_html__( 'Bitte waehlen', 'leadlotse' ) . '</option>';
		foreach ( $options as $option ) {
			$opt_value = (string) ( $option['value'] ?? '' );
			$opt_label = (string) ( $option['label'] ?? $opt_value );
			$selected  = $opt_value === $value ? ' selected' : '';
			$out      .= '<option value="' . esc_attr( $opt_value ) . '"' . $selected . '>' . esc_html( $opt_label ) . '</option>';
		}
		$out .= '</select>';
		return $out;
	}

	/**
	 * Einfache Checkbox (Label rechts).
	 *
	 * @param string $dom_id   HTML-id.
	 * @param string $name     Feldname.
	 * @param string $label    Beschriftung.
	 * @param string $value    Aktueller Wert ("1" = gesetzt).
	 * @param bool   $required Pflicht.
	 * @param bool   $has_cond Conditional Logic aktiv.
	 * @param string $aria     aria-describedby-Attribut.
	 * @return string
	 */
	private function render_checkbox( string $dom_id, string $name, string $label, string $value, bool $required, bool $has_cond, string $aria ): string {
		$checked  = '1' === $value ? ' checked' : '';
		$req_attr = $required ? ' aria-required="true"' : '';
		$req_attr .= ( $required && ! $has_cond ) ? ' required' : '';
		$out  = '<label class="leadlotse-choice" for="' . esc_attr( $dom_id ) . '">';
		$out .= '<input type="checkbox" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '" value="1"' . $checked . $aria . $req_attr . ' /> ';
		$out .= '<span>' . esc_html( $label );
		if ( $required ) {
			$out .= ' <span class="leadlotse-required" aria-hidden="true">*</span>';
		}
		$out .= '</span></label>';
		return $out;
	}

	/**
	 * Einwilligungs-Checkbox (immer Pflicht, DSGVO-Text als Label, D6).
	 *
	 * @param string               $dom_id   HTML-id.
	 * @param string               $name     Feldname.
	 * @param array<string, mixed> $data     Render-Daten (consent_text, text_version).
	 * @param string               $value    Aktueller Wert.
	 * @param bool                 $has_cond Conditional Logic aktiv.
	 * @param string               $aria     aria-describedby-Attribut.
	 * @return string
	 */
	private function render_consent( string $dom_id, string $name, array $data, string $value, bool $has_cond, string $aria ): string {
		$text    = (string) ( $data['consent_text'] ?? '' );
		if ( '' === $text ) {
			$text = (string) ( $data['label'] ?? '' );
		}
		$checked  = '1' === $value ? ' checked' : '';
		$req_attr = ' aria-required="true"' . ( $has_cond ? '' : ' required' );
		$out  = '<label class="leadlotse-choice leadlotse-choice--consent" for="' . esc_attr( $dom_id ) . '">';
		$out .= '<input type="checkbox" id="' . esc_attr( $dom_id ) . '" name="' . esc_attr( $name ) . '" value="1"' . $checked . $aria . $req_attr . ' /> ';
		// Consent-Text darf einfache Links enthalten (Datenschutzseite).
		$out .= '<span>' . wp_kses( $text, $this->allowed_consent_html() ) . ' <span class="leadlotse-required" aria-hidden="true">*</span></span>';
		$out .= '</label>';
		return $out;
	}

	/**
	 * Rendert das unsichtbare Honeypot-Feld (CSS-verborgen, tabindex=-1, autocomplete=off).
	 *
	 * @return string
	 */
	private function render_honeypot(): string {
		$name = HoneypotField::FIELD_ID;
		$out  = '<div class="leadlotse-hp" aria-hidden="true">';
		$out .= '<label for="' . esc_attr( $name ) . '">' . esc_html__( 'Dieses Feld bitte leer lassen', 'leadlotse' ) . '</label>';
		$out .= '<input type="text" id="' . esc_attr( $name ) . '" name="' . esc_attr( $name ) . '" value="" tabindex="-1" autocomplete="off" />';
		$out .= '</div>';
		return $out;
	}

	/**
	 * Label-Element.
	 *
	 * @param string $for      Ziel-id.
	 * @param string $text     Beschriftung.
	 * @param bool   $required Pflichtfeld.
	 * @param bool   $as_group Fuer Radiogruppen: als Gruppen-Beschriftung ausgeben.
	 * @return string
	 */
	private function label( string $for, string $text, bool $required, bool $as_group ): string {
		$star = $required ? ' <span class="leadlotse-required" aria-hidden="true">*</span>' : '';
		if ( $as_group ) {
			return '<span class="leadlotse-label" id="' . esc_attr( $for . '-label' ) . '">' . esc_html( $text ) . $star . '</span>';
		}
		return '<label class="leadlotse-label" for="' . esc_attr( $for ) . '">' . esc_html( $text ) . $star . '</label>';
	}

	/**
	 * Hilfetext (per aria-describedby verknuepft).
	 *
	 * @param string $dom_id HTML-id des Controls.
	 * @param string $help   Hilfetext.
	 * @return string
	 */
	private function help_html( string $dom_id, string $help ): string {
		if ( '' === $help ) {
			return '';
		}
		return '<span class="leadlotse-help" id="' . esc_attr( $dom_id . '-help' ) . '">' . esc_html( $help ) . '</span>';
	}

	/**
	 * Fehlerbereich am Feld. Immer vorhanden (fuer JS), mit role="alert" wenn befuellt.
	 *
	 * @param string $dom_id HTML-id des Controls.
	 * @param string $error  Fehlermeldung.
	 * @return string
	 */
	private function error_html( string $dom_id, string $error ): string {
		$attr = '' !== $error ? ' role="alert"' : '';
		return '<span class="leadlotse-error" id="' . esc_attr( $dom_id . '-error' ) . '"' . $attr . '>' . esc_html( $error ) . '</span>';
	}

	/**
	 * Status-Bereich (Erfolg/Fehler), vom JS wiederverwendet.
	 *
	 * @param array<string, mixed> $result Ergebnis (type, message).
	 * @return string
	 */
	private function status_region( array $result ): string {
		$type    = (string) ( $result['type'] ?? '' );
		$message = (string) ( $result['message'] ?? '' );
		$classes = 'leadlotse-status';
		$role    = 'status';
		$hidden  = ' hidden';

		if ( 'success' === $type ) {
			$classes .= ' leadlotse-status--success';
			$hidden   = '';
		} elseif ( 'error' === $type ) {
			$classes .= ' leadlotse-status--error';
			$role     = 'alert';
			$hidden   = '';
		}

		return '<div class="' . esc_attr( $classes ) . '" role="' . esc_attr( $role ) . '" aria-live="polite" data-leadlotse-status="1"' . $hidden . '>'
			. esc_html( $message ) . '</div>';
	}

	/**
	 * Bestimmt das Label-Layout eines Feldtyps.
	 *
	 * @param string               $type_key Feldtyp.
	 * @param array<string, mixed> $data     Render-Daten.
	 * @return string
	 */
	private function layout( string $type_key, array $data ): string {
		if ( 'select' === $type_key && 'radio' === (string) ( $data['display'] ?? 'dropdown' ) ) {
			return 'select_radio';
		}
		return 'default';
	}

	/**
	 * Liest das Submit-Ergebnis eines No-JS-Durchlaufs aus den Query-Parametern
	 * des FallbackHandlers (ll_success / ll_error, ll_form). Bei Erfolg wird die im
	 * Formular gepflegte Danke-Nachricht (settings.success_message) angezeigt, damit
	 * der No-JS-Weg dieselbe Meldung liefert wie der JS-Weg (nicht der Standardtext).
	 *
	 * @param int                  $form_id    Formular-ID.
	 * @param array<string, mixed> $definition Formular-Definition (fuer success_message).
	 * @return array<string, mixed>
	 */
	private function result_from_request( int $form_id, array $definition = array() ): array {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nur Anzeige eines Ergebnisses nach Redirect, keine Datenaenderung.
		$target = isset( $_GET['ll_form'] ) ? absint( wp_unslash( $_GET['ll_form'] ) ) : 0;
		if ( $target !== $form_id ) {
			return array();
		}

		if ( isset( $_GET['ll_success'] ) ) {
			$custom  = (string) ( $definition['settings']['success_message'] ?? '' );
			$message = '' !== $custom
				? $custom
				: __( 'Vielen Dank. Ihre Anfrage wurde gesendet. Wir melden uns so schnell wie moeglich bei Ihnen.', 'leadlotse' );
			return array(
				'result' => array(
					'type'    => 'success',
					'message' => $message,
				),
			);
		}

		if ( isset( $_GET['ll_error'] ) ) {
			$code = sanitize_key( wp_unslash( $_GET['ll_error'] ) );
			return array(
				'result' => array(
					'type'    => 'error',
					'message' => $this->message_for_error_code( $code ),
				),
			);
		}
		// phpcs:enable

		return array();
	}

	/**
	 * Uebersetzt einen Fehlercode des FallbackHandlers in eine Nutzermeldung
	 * (Ursache + naechster Schritt, UX-Grundsatz 7).
	 *
	 * @param string $code Fehlercode.
	 * @return string
	 */
	private function message_for_error_code( string $code ): string {
		switch ( $code ) {
			case 'leadlotse_validation_failed':
				return __( 'Bitte pruefen Sie Ihre Eingaben und senden Sie das Formular erneut.', 'leadlotse' );
			case 'leadlotse_time_trap':
				return __( 'Das Formular wurde zu schnell abgesendet. Bitte warten Sie einen Moment und senden Sie es erneut.', 'leadlotse' );
			case 'leadlotse_rate_limited':
				return __( 'Es wurden zu viele Anfragen in kurzer Zeit gesendet. Bitte versuchen Sie es in einigen Minuten erneut.', 'leadlotse' );
			case 'leadlotse_save_failed':
				return __( 'Die Anfrage konnte nicht gespeichert werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Betreiber der Website.', 'leadlotse' );
			case 'leadlotse_form_not_found':
				return __( 'Dieses Formular ist nicht mehr verfuegbar. Bitte laden Sie die Seite neu.', 'leadlotse' );
			case 'leadlotse_event_full':
				return __( 'Diese Veranstaltung ist ausgebucht. Es sind leider keine Plaetze mehr frei.', 'leadlotse' );
			default:
				return __( 'Die Anfrage konnte nicht gesendet werden. Bitte versuchen Sie es erneut.', 'leadlotse' );
		}
	}

	/**
	 * Erlaubte HTML-Tags im Einwilligungstext (nur Links und einfache Auszeichnung).
	 *
	 * @return array<string, array<string, bool>>
	 */
	private function allowed_consent_html(): array {
		return array(
			'a'      => array(
				'href'   => true,
				'target' => true,
				'rel'    => true,
			),
			'strong' => array(),
			'em'     => array(),
		);
	}

	/**
	 * Neutraler Hinweis, wenn ein Formular nicht gerendert werden kann.
	 *
	 * @param string $message Meldung.
	 * @return string
	 */
	private function notice( string $message ): string {
		return '<div class="leadlotse-notice">' . esc_html( $message ) . '</div>';
	}
}
