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

namespace Hafenstudios\Leadlotse\Submit;

use Hafenstudios\Core\Core;
use Hafenstudios\Core\Events\EventBus;
use Hafenstudios\Leadlotse\Db\EntryRepository;
use Hafenstudios\Leadlotse\Doi\DoiService;
use Hafenstudios\Leadlotse\Events\EventCapacity;
use Hafenstudios\Leadlotse\Fields\ConditionalLogic;
use Hafenstudios\Leadlotse\Fields\FieldRegistry;
use Hafenstudios\Leadlotse\Forms\FormPostType;
use Hafenstudios\Leadlotse\Identity\IdentityService;
use Hafenstudios\Leadlotse\WhatsApp\OptinCollector;

defined( 'ABSPATH' ) || exit;

/**
 * Submit-Pipeline (D7, D6, D9), von REST-Endpoint und admin-post.php-Fallback
 * gemeinsam genutzt:
 *
 * 1. Formular laden
 * 2. Honeypot (still verwerfen, Erfolg vortaeuschen)
 * 3. Zeitfalle
 * 4. Rate Limiting pro IP+Formular
 * 5. Conditional Logic auswerten (verborgene Felder ignorieren)
 * 6. Sanitizing und Validierung je Feld
 * 7. Consent-Aufzeichnung ueber Core ConsentLog (D6)
 * 8. Entry speichern (inkl. DOI-Token beim Zweck Newsletter, D9)
 * 9. Event hafenstudios/lead_created
 * 10. Bestaetigungs-, Benachrichtigungs- bzw. DOI-Mail ueber den Core-Mailer
 */
final class SubmitHandler {

	/**
	 * Signalwert eines Spam-Check-Konsumenten fuer "still verwerfen, Erfolg
	 * vortaeuschen" (Quarantaene, W-L1). Der Konsument (Wellenbrecher) hat den
	 * Payload dann bereits gesichert; der Besucher sieht die normale
	 * Erfolgsantwort. WP_Error bleibt "sichtbar ablehnen", true bleibt "durch".
	 */
	public const SPAM_SILENT_DROP = 'leadlotse_spam_silent_drop';

	/**
	 * Capability, die der Restore-Weg (W-L1) verlangt. Der Bypass des
	 * Spam-Schutzes ist nur aus einem Admin-Kontext mit dieser Cap wirksam.
	 */
	public const RESTORE_CAP = 'hs_manage_spam';

	private FieldRegistry $registry;

	private ConditionalLogic $conditions;

	private SpamGuard $spam;

	private EntryRepository $entries;

	private DoiService $doi;

	private MailService $mails;

	private ?IdentityService $identity;

	/**
	 * @param FieldRegistry        $registry   Feldtypen.
	 * @param ConditionalLogic     $conditions Conditional Logic.
	 * @param SpamGuard            $spam       Spam-Kette.
	 * @param EntryRepository      $entries    Entry-Speicher.
	 * @param DoiService           $doi        DOI-Flow.
	 * @param MailService          $mails      Mails.
	 * @param IdentityService|null $identity   Ausfuellhilfe (optional; prueft die Verifikations-Assertion).
	 */
	public function __construct(
		FieldRegistry $registry,
		ConditionalLogic $conditions,
		SpamGuard $spam,
		EntryRepository $entries,
		DoiService $doi,
		MailService $mails,
		?IdentityService $identity = null
	) {
		$this->registry   = $registry;
		$this->conditions = $conditions;
		$this->spam       = $spam;
		$this->entries    = $entries;
		$this->doi        = $doi;
		$this->mails      = $mails;
		$this->identity   = $identity;
	}

	/**
	 * Verarbeitet einen Submit.
	 *
	 * @param int                  $form_id Formular-ID.
	 * @param array<string, mixed> $input   Eingereichte Rohdaten (unslashed).
	 * @param array<string, mixed> $context Kontext: source_url, utm_source, utm_medium, utm_campaign.
	 * @return array<string, mixed>|\WP_Error Erfolgsdaten oder Fehler.
	 */
	public function handle( int $form_id, array $input, array $context = array(), bool $bypass_spam = false ) {
		$post       = get_post( $form_id );
		$definition = FormPostType::get_definition( $form_id );

		if ( ! $post || 'publish' !== $post->post_status || null === $definition ) {
			return new \WP_Error(
				'leadlotse_form_not_found',
				__( 'Dieses Formular ist nicht verfuegbar. Bitte laden Sie die Seite neu oder wenden Sie sich an den Betreiber der Website.', 'leadlotse' ),
				array( 'status' => 404 )
			);
		}

		$settings        = (array) ( $definition['settings'] ?? array() );
		$plugin_settings = Core::instance()->settings( 'leadlotse' );

		// Der Spam-Bypass (Restore-Weg, W-L1) ist NUR mit Admin-Cap wirksam;
		// ein oeffentlicher Aufruf mit gesetztem Flag kann ihn nie erzwingen.
		if ( $bypass_spam && ! current_user_can( self::RESTORE_CAP ) ) {
			$bypass_spam = false;
		}

		if ( ! $bypass_spam ) {
			// Schritt 1: Honeypot. Still verwerfen, Erfolg vortaeuschen (D7).
			if ( $this->spam->is_honeypot_filled( $input ) ) {
				return $this->success_payload( 0, $settings );
			}

			// Schritt 2: Zeitfalle.
			$min_seconds = absint( $settings['spam']['min_seconds'] ?? 3 );
			$trap_error  = $this->spam->check_time_trap( (string) ( $input[ SpamGuard::TIMESTAMP_FIELD ] ?? '' ), $min_seconds );
			if ( null !== $trap_error ) {
				return new \WP_Error( 'leadlotse_time_trap', $trap_error, array( 'status' => 400 ) );
			}

			// Schritt 3: Rate Limiting.
			$rate_error = $this->spam->check_rate_limit(
				$form_id,
				absint( $plugin_settings->get( 'spam.rate_limit.max', 5 ) ),
				absint( $plugin_settings->get( 'spam.rate_limit.window', 600 ) )
			);
			if ( null !== $rate_error ) {
				return new \WP_Error( 'leadlotse_rate_limited', $rate_error, array( 'status' => 429 ) );
			}

			/**
			 * Erweiterungspunkt fuer zusaetzliche Spam-Pruefungen (z.B. Turnstile,
			 * Wellenbrecher). Rueckgabe: WP_Error bricht sichtbar ab, die Konstante
			 * SubmitHandler::SPAM_SILENT_DROP verwirft still (Quarantaene), sonst
			 * laeuft der Submit weiter.
			 *
			 * Seit W-L1 wird ZUSAETZLICH ein Kontext-Array uebergeben (Server-
			 * Snapshot-Rohdaten, die injizierten Signalfelder und die aufbereiteten
			 * Textfeld-/E-Mail-Werte). Konsumenten mit der alten 3-Argument-Signatur
			 * bleiben unveraendert funktionsfaehig.
			 *
			 * @param true|\WP_Error|string $result  Bisheriges Ergebnis.
			 * @param array<string, mixed>  $input   Rohdaten.
			 * @param int                   $form_id Formular-ID.
			 * @param array<string, mixed>  $context Spam-Kontext (server, signals, fields, submit).
			 */
			$extra = EventBus::filter( 'leadlotse_spam_check', true, $input, $form_id, $this->spam_context( $input, (array) $definition, $context ) );
			if ( is_wp_error( $extra ) ) {
				return $extra;
			}
			if ( self::SPAM_SILENT_DROP === $extra ) {
				// Quarantaene: der Konsument hat den Payload gesichert; dem
				// Besucher die normale Erfolgsantwort zeigen (stilles Abfangen).
				return $this->success_payload( 0, $settings );
			}
		}

		// Schritt 4: Sanitizing, Conditional Logic, Validierung.
		$fields    = (array) ( $definition['fields'] ?? array() );
		$sanitized = array();
		foreach ( $fields as $field ) {
			$type = $this->registry->get( (string) ( $field['type'] ?? '' ) );
			if ( null === $type ) {
				continue;
			}
			$id               = (string) $field['id'];
			$sanitized[ $id ] = $type->sanitize( $input[ $id ] ?? '', $field );
		}

		$values = array();
		$errors = array();
		foreach ( $fields as $field ) {
			$type = $this->registry->get( (string) ( $field['type'] ?? '' ) );
			if ( null === $type ) {
				continue;
			}
			$id = (string) $field['id'];

			// Verborgene Felder: nicht validieren, nicht speichern.
			if ( ! $this->conditions->is_visible( $field, $sanitized ) ) {
				continue;
			}

			$error = $type->validate( $sanitized[ $id ], $field );
			if ( null !== $error ) {
				$errors[ $id ] = $error;
				continue;
			}
			$values[ $id ] = $sanitized[ $id ];
		}

		if ( array() !== $errors ) {
			return new \WP_Error(
				'leadlotse_validation_failed',
				__( 'Bitte pruefen Sie die markierten Felder und senden Sie das Formular erneut.', 'leadlotse' ),
				array(
					'status' => 400,
					'fields' => $errors,
				)
			);
		}

		// Schritt 4b: Kapazitaet der Veranstaltung (L-R1). Verbindliche
		// serverseitige Pruefung inklusive der beantragten Begleitpersonen;
		// die Ausgebucht-Anzeige im Renderer ist nur Komfort.
		$capacity_error = ( new EventCapacity( $this->entries ) )->check_submit( $form_id, (array) $definition, $values );
		if ( null !== $capacity_error ) {
			return $capacity_error;
		}

		$email   = $this->find_email( $fields, $values );
		$purpose = (string) ( $definition['purpose'] ?? 'contact' );

		// Schritt 5: Consent-Aufzeichnung ueber den Core (D6).
		$consent_id = 0;
		foreach ( $fields as $field ) {
			if ( 'consent' === (string) ( $field['type'] ?? '' ) && isset( $values[ (string) $field['id'] ] ) ) {
				$consent_id = Core::instance()->consent()->record(
					array(
						'source'        => 'leadlotse:form:' . $form_id,
						'subject_email' => $email,
						'purpose'       => $purpose,
						'consent_text'  => (string) ( $field['consent_text'] ?? '' ),
						'text_version'  => (string) ( $field['text_version'] ?? '1' ),
						'reference'     => 'form:' . $form_id,
					)
				);
				break;
			}
		}

		// Schritt 5b: WhatsApp-Opt-ins erfassen (Click-to-Chat, additive Einwilligung).
		// Jedes Opt-in wird als technischer Wert am Entry gespeichert und gerichtsfest
		// im Consent-Log der Flotte protokolliert.
		$whatsapp_optin = false;
		foreach ( OptinCollector::collect( $fields, $input, $this->conditions, $sanitized ) as $optin ) {
			$whatsapp_optin                       = true;
			$values[ '_wa_' . $optin['field_id'] ] = '1';
			Core::instance()->consent()->record(
				array(
					'source'        => 'leadlotse:form:' . $form_id,
					'subject_email' => $email,
					'purpose'       => 'whatsapp',
					'consent_text'  => $optin['text'],
					'text_version'  => '1',
					'reference'     => 'form:' . $form_id,
				)
			);
		}

		// Schritt 5c: Verifizierte E-Mail aus der Ausfuellhilfe pruefen (Assertion).
		// Nur wenn die signierte Assertion zur eingereichten E-Mail passt, gilt sie
		// als bestaetigt; der Anbieter-Schluessel wird technisch am Entry vermerkt.
		$email_verified = false;
		$assert         = (string) ( $input['_id_assert'] ?? '' );
		if ( '' !== $assert && '' !== $email && null !== $this->identity ) {
			$provider = $this->identity->verify_assertion( $assert, $email );
			if ( '' !== $provider ) {
				$values['_identity'] = $provider;
				$email_verified      = true;
			}
		}

		// Schritt 6: DOI vorbereiten (Zweck Newsletter oder Formular-Flag, D9).
		$doi_enabled = 'newsletter' === $purpose || ! empty( $settings['doi']['enabled'] );
		$doi_token   = '';
		if ( $doi_enabled && '' !== $email ) {
			// Token wird nach dem Insert erzeugt (braucht die Entry-ID); hier nur Flag.
			$doi_token = 'pending';
		}

		// Schritt 7: Entry speichern.
		$entry_id = $this->entries->insert(
			array(
				'form_id'      => $form_id,
				'email'        => $email,
				'status'       => 'new',
				'purpose'      => $purpose,
				'field_values' => $values,
				'source_url'   => (string) ( $context['source_url'] ?? '' ),
				'utm_source'   => (string) ( $context['utm_source'] ?? ( $values['utm_source'] ?? '' ) ),
				'utm_medium'   => (string) ( $context['utm_medium'] ?? ( $values['utm_medium'] ?? '' ) ),
				'utm_campaign' => (string) ( $context['utm_campaign'] ?? ( $values['utm_campaign'] ?? '' ) ),
				'consent_id'   => $consent_id,
			)
		);

		if ( 0 === $entry_id ) {
			return new \WP_Error(
				'leadlotse_save_failed',
				__( 'Die Anfrage konnte nicht gespeichert werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Betreiber der Website.', 'leadlotse' ),
				array( 'status' => 500 )
			);
		}

		$confirm_url = '';
		if ( '' !== $doi_token ) {
			$expiry_hours = absint( $plugin_settings->get( 'doi.expiry_hours', 48 ) );
			$token        = $this->doi->create_token( $entry_id, $expiry_hours );
			$this->store_doi_token_hash( $entry_id, $this->doi->token_hash( $token ) );
			$confirm_url = add_query_arg(
				array( 'token' => rawurlencode( $token ) ),
				rest_url( 'leadlotse/v1/confirm' )
			);
		}

		$entry = $this->entries->get( $entry_id );

		// Schritt 8: Flotten-Event (Kurato dockt hier in Phase 2 an).
		EventBus::emit(
			'lead_created',
			array(
				'entry_id'   => $entry_id,
				'form_id'    => $form_id,
				'form_title' => (string) $post->post_title,
				'purpose'    => $purpose,
				'email'      => $email,
				// Kontakt-Stammdaten fuer die Kurato-Uebernahme (BAUPLAN 7).
				'contact'    => $this->extract_contact_fields( $fields, $values, $whatsapp_optin, $email_verified ),
				'source'     => array(
					'url'          => (string) ( $context['source_url'] ?? '' ),
					'utm_source'   => (string) ( $entry['utm_source'] ?? '' ),
					'utm_medium'   => (string) ( $entry['utm_medium'] ?? '' ),
					'utm_campaign' => (string) ( $entry['utm_campaign'] ?? '' ),
				),
				'consent_id' => $consent_id,
				'doi'        => $doi_enabled,
			)
		);

		// Schritt 9: Mails.
		if ( is_array( $entry ) ) {
			$this->mails->send_notification( $entry, $definition, (string) $post->post_title );
			if ( '' !== $confirm_url ) {
				$this->mails->send_doi_mail( $entry, $confirm_url );
			} else {
				$this->mails->send_confirmation( $entry, $definition, (string) $post->post_title );
			}
		}

		return $this->success_payload( $entry_id, $settings, $doi_enabled );
	}

	/**
	 * Extrahiert die Kontakt-Standardfelder (Name, Telefon) aus den Formularwerten
	 * fuer die Kurato-Uebernahme. Datensparsam: nur diese vier Felder wandern mit.
	 *
	 * @param array<int, array<string, mixed>> $fields         Felddefinitionen.
	 * @param array<string, mixed>             $values         Validierte Werte.
	 * @param bool                             $whatsapp_optin Mindestens ein WhatsApp-Opt-in vorhanden.
	 * @param bool                             $email_verified E-Mail ueber die Ausfuellhilfe bestaetigt.
	 * @return array{display_name:string,first_name:string,last_name:string,phone:string,whatsapp_optin:bool,email_verified:bool}
	 */
	private function extract_contact_fields( array $fields, array $values, bool $whatsapp_optin = false, bool $email_verified = false ): array {
		$out = array(
			'display_name'   => isset( $values['name'] ) ? (string) $values['name'] : '',
			'first_name'     => isset( $values['first_name'] ) ? (string) $values['first_name'] : '',
			'last_name'      => isset( $values['last_name'] ) ? (string) $values['last_name'] : '',
			'phone'          => '',
			'whatsapp_optin' => $whatsapp_optin,
			'email_verified' => $email_verified,
		);
		foreach ( $fields as $field ) {
			if ( 'phone' === (string) ( $field['type'] ?? '' ) ) {
				$id = (string) ( $field['id'] ?? '' );
				if ( '' !== (string) ( $values[ $id ] ?? '' ) ) {
					$out['phone'] = (string) $values[ $id ];
					break;
				}
			}
		}
		return $out;
	}

	/**
	 * Designierter Restore-Weg (W-L1 c): schickt einen zuvor quarantinierten
	 * Payload mit Spam-Bypass erneut durch die Pipeline. NUR aus einem Admin-
	 * Kontext mit Capability aufrufbar (der Aufruf kommt von Wellenbrechers
	 * Quarantaene-Seite ueber den Filter hafenstudios/leadlotse_restore_submit);
	 * kein oeffentlicher Endpoint. Der Lead entsteht dabei ganz normal inklusive
	 * DOI/Kurato-Events.
	 *
	 * @param mixed                $result  Filter-Default (ignoriert).
	 * @param int                  $form_id Formular-ID.
	 * @param array<string, mixed> $input   Roh-Submit aus dem Payload.
	 * @param array<string, mixed> $context Submit-Kontext (source_url, utm_*).
	 * @return array<string, mixed>|\WP_Error
	 */
	public function restore( $result, int $form_id, array $input = array(), array $context = array() ) {
		if ( ! current_user_can( self::RESTORE_CAP ) ) {
			return new \WP_Error(
				'leadlotse_restore_forbidden',
				__( 'Fuer die Wiederherstellung fehlt Ihnen die Berechtigung.', 'leadlotse' ),
				array( 'status' => 403 )
			);
		}
		return $this->handle( $form_id, $input, $context, true );
	}

	/**
	 * Baut den Kontext-Array, den der Spam-Filter seit W-L1 zusaetzlich erhaelt:
	 * Server-Snapshot-Rohdaten, die injizierten Signalfelder und die von
	 * Leadlotse aufbereiteten Textfeld-/E-Mail-Werte (Leadlotse kennt die
	 * Feldtypen; der Konsument bleibt so von der Definition entkoppelt).
	 *
	 * @param array<string, mixed> $input      Roh-Submit.
	 * @param array<string, mixed> $definition Formular-Definition.
	 * @param array<string, mixed> $context    Submit-Kontext (source_url, utm_*).
	 * @return array<string, mixed>
	 */
	private function spam_context( array $input, array $definition, array $context ): array {
		$fields = (array) ( $definition['fields'] ?? array() );

		$email       = '';
		$author_name = '';
		$texts       = array();
		foreach ( $fields as $field ) {
			if ( ! is_array( $field ) ) {
				continue;
			}
			$type = (string) ( $field['type'] ?? '' );
			$id   = (string) ( $field['id'] ?? '' );
			if ( '' === $id ) {
				continue;
			}
			$value = $this->input_string( $input, $id );
			if ( '' === $value ) {
				continue;
			}
			if ( 'email' === $type && '' === $email ) {
				$email = $value;
			} elseif ( in_array( $type, array( 'text', 'textarea' ), true ) ) {
				$texts[] = $value;
				if ( '' === $author_name && in_array( $id, array( 'name', 'first_name' ), true ) ) {
					$author_name = $value;
				}
			}
		}

		return array(
			// Roh-Server-Snapshot; der Konsument normalisiert ihn selbst.
			'server'  => is_array( $_SERVER ) ? $_SERVER : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Roh-Header-Snapshot fuer die Spam-Analyse, wird vom Konsumenten aufbereitet.
			// Von einem Dritt-Plugin injizierte Signalfelder aus dem Submit.
			'signals' => array(
				'token'        => $this->input_string( $input, 'wb_token' ),
				'js_timestamp' => $this->input_string( $input, 'wb_js' ),
				'events'       => $this->input_string( $input, 'wb_events' ),
			),
			// Aufbereitete Werte fuer die Inhaltsheuristik.
			'fields'  => array(
				'email'       => $email,
				'author_name' => $author_name,
				'content'     => implode( "\n", $texts ),
			),
			// Submit-Kontext fuer eine originalgetreue Wiederherstellung.
			'submit'  => $context,
		);
	}

	/**
	 * Liest einen Roh-Submit-Wert als String (nur Skalare).
	 *
	 * @param array<string, mixed> $input Roh-Submit.
	 * @param string               $key   Feldschluessel.
	 * @return string
	 */
	private function input_string( array $input, string $key ): string {
		$value = $input[ $key ] ?? '';
		return is_scalar( $value ) ? (string) $value : '';
	}

	/**
	 * Erste E-Mail-Adresse aus den Werten (Feldtyp email).
	 *
	 * @param array<int, array<string, mixed>> $fields Felddefinitionen.
	 * @param array<string, mixed>             $values Validierte Werte.
	 * @return string
	 */
	private function find_email( array $fields, array $values ): string {
		foreach ( $fields as $field ) {
			if ( 'email' === (string) ( $field['type'] ?? '' ) ) {
				$value = (string) ( $values[ (string) $field['id'] ] ?? '' );
				if ( '' !== $value ) {
					return $value;
				}
			}
		}
		return '';
	}

	/**
	 * Speichert den DOI-Token-Hash am Entry.
	 *
	 * @param int    $entry_id Entry-ID.
	 * @param string $hash     Token-Hash.
	 * @return void
	 */
	private function store_doi_token_hash( int $entry_id, string $hash ): void {
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->update( EntryRepository::table(), array( 'doi_token_hash' => $hash ), array( 'id' => $entry_id ) );
	}

	/**
	 * Einheitliche Erfolgsantwort.
	 *
	 * @param int                  $entry_id Entry-ID (0 beim stillen Verwerfen).
	 * @param array<string, mixed> $settings Formular-Einstellungen.
	 * @param bool                 $doi      Ob eine DOI-Mail unterwegs ist.
	 * @return array<string, mixed>
	 */
	private function success_payload( int $entry_id, array $settings, bool $doi = false ): array {
		$message = (string) ( $settings['success_message'] ?? '' );
		if ( '' === $message ) {
			$message = $doi
				? __( 'Vielen Dank. Bitte pruefen Sie Ihr Postfach und bestaetigen Sie Ihre Anmeldung ueber den Link in der E-Mail.', 'leadlotse' )
				: __( 'Vielen Dank. Ihre Anfrage wurde gesendet. Wir melden uns so schnell wie moeglich bei Ihnen.', 'leadlotse' );
		}

		return array(
			'success'      => true,
			'entry_id'     => $entry_id,
			'message'      => $message,
			'redirect_url' => (string) ( $settings['redirect_url'] ?? '' ),
		);
	}
}
