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

namespace Hafenstudios\Kurato\Booking;

use Hafenstudios\Core\Core;
use Hafenstudios\Core\Events\EventBus;
use Hafenstudios\Kurato\Db\AvailabilityRepository;
use Hafenstudios\Kurato\Db\BookingRepository;
use Hafenstudios\Kurato\Db\BookingTypeRepository;
use Hafenstudios\Kurato\Db\ContactRepository;
use Hafenstudios\Kurato\Db\HostRepository;
use Hafenstudios\Kurato\Db\StageRepository;
use Hafenstudios\Kurato\Db\TaskRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Kernlogik einer Terminbuchung (K5c, K5-KONZEPT Abschnitt 4-6).
 *
 * REIN von HTTP/Rendering: die oeffentliche Seite (Site\BookingPage) sammelt die
 * Eingaben und ruft {@see book()}. Der Service validiert (Honeypot, Zeitfalle,
 * Pflicht-Checkbox, Slot erneut ueber den SlotCalculator), legt die Buchung mit
 * Race-Abfang an und macht die CRM-Verzahnung, die das Alleinstellungsmerkmal ist:
 *
 * - Kontakt anlegen/ergaenzen per E-Mail-Dedup (Muster LeadIntake, Race-Fallback).
 * - Consent-Nachweis purpose `booking` (Checkbox-Text + Version).
 * - Flotten-Event `hafenstudios/booking_created`.
 * - Pipeline-Stufe setzen, wenn ein Setting sie nennt und der Kontakt frueher steht
 *   (nie zurueckstufen; Positionen aus dem StageRepository).
 * - Wiedervorlage "Nachfassen: {Terminart}" faellig 1 Tag nach dem Termin.
 * - Bestaetigungsmails an Besucher und Host (ueber den BookingMailer).
 *
 * Nicht final; die Test-Seams `stage_id()`, `record_consent()`, `emit_created()`,
 * `send_confirmations()`, `is_slot_available()` und `site_timezone()` kapseln die
 * WordPress-/Core-Abhaengigkeiten, damit die Verzahnung ohne echtes wpdb/Core
 * pruefbar ist (Muster PreferenceCenter/WelcomeMailService).
 */
class BookingService {

	/**
	 * Version des Einwilligungstexts fuer den Buchungs-Consent-Nachweis. Bei einer
	 * Textaenderung wird diese Version erhoeht (Muster PreferenceCenter).
	 */
	public const CONSENT_TEXT_VERSION = '1';

	/**
	 * Mindestdauer zwischen Formularanzeige und Absenden (Zeitfalle). Ein Bot fuellt
	 * schneller aus; darunter gilt die Buchung als Spam.
	 */
	public const MIN_FILL_SECONDS = 3;

	private HostRepository $hosts;

	private BookingTypeRepository $types;

	private AvailabilityRepository $availability;

	private BookingRepository $bookings;

	private ContactRepository $contacts;

	private StageRepository $stages;

	private TaskRepository $tasks;

	private SlotCalculator $slots;

	private ?BookingMailer $mailer;

	/**
	 * @param HostRepository         $hosts        Host-Repository.
	 * @param BookingTypeRepository  $types        Terminarten-Repository.
	 * @param AvailabilityRepository $availability Verfuegbarkeits-Repository.
	 * @param BookingRepository      $bookings     Buchungs-Repository.
	 * @param ContactRepository      $contacts     Kontakt-Repository.
	 * @param StageRepository        $stages       Stufen-Repository.
	 * @param TaskRepository         $tasks        Wiedervorlagen-Repository.
	 * @param SlotCalculator         $slots        Slot-Rechner (rein).
	 * @param BookingMailer|null     $mailer       Transaktionsmailer (optional; im Test null).
	 */
	public function __construct(
		HostRepository $hosts,
		BookingTypeRepository $types,
		AvailabilityRepository $availability,
		BookingRepository $bookings,
		ContactRepository $contacts,
		StageRepository $stages,
		TaskRepository $tasks,
		SlotCalculator $slots,
		?BookingMailer $mailer = null
	) {
		$this->hosts        = $hosts;
		$this->types        = $types;
		$this->availability = $availability;
		$this->bookings     = $bookings;
		$this->contacts     = $contacts;
		$this->stages       = $stages;
		$this->tasks        = $tasks;
		$this->slots        = $slots;
		$this->mailer       = $mailer;
	}

	/**
	 * Verarbeitet eine Buchungsanfrage.
	 *
	 * Rueckgabe-Struktur: array{ok:bool, error:string, booking_id:int,
	 * booking?:array, host?:array, type?:array, contact?:array}. Fehlercodes:
	 * `spam` (Honeypot/Zeitfalle), `consent` (Pflicht-Checkbox fehlt), `name`,
	 * `email`, `host`, `type`, `slot_invalid` (Slot nicht mehr buchbar),
	 * `slot_taken` (Race beim Insert), `error`.
	 *
	 * @param array<string, mixed> $input     Eingaben: name, email, phone, note,
	 *      consent (Checkbox), hp (Honeypot, leer erwartet), started (Formular-
	 *      Zeitstempel als Unix-Sekunden), slot ('Y-m-d H:i:s' UTC).
	 * @param array<string, mixed> $host      Host (aus der Seite geladen).
	 * @param array<string, mixed> $type      Terminart (aus der Seite geladen).
	 * @param int                  $now_ts    "Jetzt" als Unix-Zeit (0 = time()).
	 * @return array<string, mixed>
	 */
	public function book( array $input, array $host, array $type, int $now_ts = 0 ): array {
		$now_ts = $now_ts > 0 ? $now_ts : time();

		// Honeypot: ein ausgefuelltes verstecktes Feld ist immer ein Bot.
		if ( '' !== trim( (string) ( $input['hp'] ?? '' ) ) ) {
			return $this->fail( 'spam' );
		}

		// Zeitfalle: zu schnelles Absenden ist ein Bot. Fehlt der Zeitstempel oder
		// ist er unplausibel (Zukunft), wird ebenfalls abgelehnt.
		$started = (int) ( $input['started'] ?? 0 );
		if ( $started <= 0 || ( $now_ts - $started ) < self::MIN_FILL_SECONDS ) {
			return $this->fail( 'spam' );
		}

		// Pflicht-Checkbox Datenschutz (nicht vorangekreuzt).
		if ( empty( $input['consent'] ) ) {
			return $this->fail( 'consent' );
		}

		$name  = trim( sanitize_text_field( (string) ( $input['name'] ?? '' ) ) );
		$email = sanitize_email( (string) ( $input['email'] ?? '' ) );
		if ( '' === $name ) {
			return $this->fail( 'name' );
		}
		if ( '' === $email || ! is_email( $email ) ) {
			return $this->fail( 'email' );
		}

		if ( empty( $host['active'] ) ) {
			return $this->fail( 'host' );
		}
		if ( empty( $type['active'] ) ) {
			return $this->fail( 'type' );
		}

		$start = $this->parse_utc( (string) ( $input['slot'] ?? '' ) );
		if ( null === $start ) {
			return $this->fail( 'slot_invalid' );
		}

		// Slot beim Submit erneut pruefen (Muster K5-KONZEPT Abschnitt 4): der
		// Slot koennte seit der Anzeige belegt oder aus der Verfuegbarkeit gefallen
		// sein.
		if ( ! $this->is_slot_available( $host, $type, $start, $now_ts ) ) {
			return $this->fail( 'slot_invalid' );
		}

		$duration = max( 1, (int) ( $type['duration_min'] ?? 0 ) );
		$end      = $start->add( new \DateInterval( 'PT' . $duration . 'M' ) );

		// Kontakt dedup (Muster LeadIntake): neue E-Mail legt an, bekannte ergaenzt
		// nur leere Felder.
		$contact_id = $this->resolve_contact( $email, $name, (string) ( $input['phone'] ?? '' ) );
		if ( $contact_id <= 0 ) {
			return $this->fail( 'error' );
		}

		$booking_id = $this->bookings->create(
			array(
				'contact_id' => $contact_id,
				'host_id'    => (int) $host['id'],
				'type_id'    => (int) $type['id'],
				'start_utc'  => $start->format( 'Y-m-d H:i:s' ),
				'end_utc'    => $end->format( 'Y-m-d H:i:s' ),
				'note'       => (string) ( $input['note'] ?? '' ),
			)
		);
		if ( $booking_id <= 0 ) {
			// Race: der Slot wurde soeben vergeben (UNIQUE-Kollision, K5a).
			return $this->fail( 'slot_taken' );
		}

		// --- CRM-Verzahnung ------------------------------------------------

		$this->record_consent(
			array(
				'source'        => 'kurato:booking',
				'subject_email' => $email,
				'purpose'       => 'booking',
				'consent_text'  => $this->consent_text(),
				'text_version'  => self::CONSENT_TEXT_VERSION,
				'reference'     => 'booking:' . $booking_id,
			)
		);

		$this->emit_created(
			array(
				'contact_id' => $contact_id,
				'booking_id' => $booking_id,
				'host_id'    => (int) $host['id'],
				'type_id'    => (int) $type['id'],
				'start_utc'  => $start->format( 'Y-m-d H:i:s' ),
			)
		);

		$this->maybe_advance_stage( $contact_id );
		$this->create_follow_up_task( $contact_id, (string) $type['label'], $end );
		$this->mark_first_booking();

		$booking = array(
			'id'         => $booking_id,
			'contact_id' => $contact_id,
			'host_id'    => (int) $host['id'],
			'type_id'    => (int) $type['id'],
			'start_utc'  => $start->format( 'Y-m-d H:i:s' ),
			'end_utc'    => $end->format( 'Y-m-d H:i:s' ),
			'status'     => 'confirmed',
			'note'       => (string) ( $input['note'] ?? '' ),
		);
		$contact = $this->contacts->get( $contact_id ) ?? array( 'id' => $contact_id, 'email' => $email, 'display_name' => $name );

		$this->send_confirmations( $booking, $host, $type, $contact );

		return array(
			'ok'         => true,
			'error'      => '',
			'booking_id' => $booking_id,
			'booking'    => $booking,
			'host'       => $host,
			'type'       => $type,
			'contact'    => $contact,
		);
	}

	// --- CRM-Schritte ------------------------------------------------------

	/**
	 * Kontakt-Dedup per E-Mail (Muster LeadIntake): neue E-Mail legt einen Kontakt
	 * in der ersten Pipeline-Stufe an, eine bekannte E-Mail ergaenzt nur leere
	 * Felder (nie ueberschreiben). Faengt den UNIQUE-Insert-Race ab.
	 *
	 * @param string $email E-Mail.
	 * @param string $name  Anzeigename.
	 * @param string $phone Telefon (optional).
	 * @return int Kontakt-ID (0 bei Fehler).
	 */
	private function resolve_contact( string $email, string $name, string $phone ): int {
		$phone    = trim( sanitize_text_field( $phone ) );
		$existing = $this->contacts->find_by_email( $email );

		if ( null === $existing ) {
			$stage_id = $this->stages->first_stage_id();
			$new_id   = $this->contacts->insert(
				array(
					'email'        => $email,
					'display_name' => $name,
					'phone'        => $phone,
					'stage_id'     => $stage_id > 0 ? $stage_id : null,
					'source'       => 'booking',
				)
			);
			if ( $new_id > 0 ) {
				return $new_id;
			}
			// Insert scheiterte (paralleler Submit derselben E-Mail): den nun
			// vorhandenen Kontakt als bestehend behandeln.
			$existing = $this->contacts->find_by_email( $email );
		}

		if ( null === $existing ) {
			return 0;
		}

		$contact_id = (int) $existing['id'];
		$patch      = array();
		if ( '' === (string) ( $existing['display_name'] ?? '' ) && '' !== $name ) {
			$patch['display_name'] = $name;
		}
		if ( '' === (string) ( $existing['phone'] ?? '' ) && '' !== $phone ) {
			$patch['phone'] = $phone;
		}
		if ( array() !== $patch ) {
			$this->contacts->update( $contact_id, $patch );
		}
		return $contact_id;
	}

	/**
	 * Setzt die Pipeline-Stufe "Termin gebucht", wenn ein Setting sie nennt und der
	 * Kontakt aktuell in einer frueheren Stufe steht. NIE zurueckstufen: steht der
	 * Kontakt bereits auf der Zielstufe oder weiter, passiert nichts. Die Position
	 * ergibt sich aus dem sort_order der Stufen (StageRepository).
	 *
	 * @param int $contact_id Kontakt-ID.
	 * @return void
	 */
	private function maybe_advance_stage( int $contact_id ): void {
		$target_id = $this->stage_id();
		if ( $target_id <= 0 ) {
			return; // Stufen-Automatik aus (Default).
		}
		$target = $this->stages->get( $target_id );
		if ( null === $target ) {
			return;
		}

		$contact = $this->contacts->get( $contact_id );
		if ( null === $contact ) {
			return;
		}
		$current_id = isset( $contact['stage_id'] ) && null !== $contact['stage_id'] ? (int) $contact['stage_id'] : 0;
		if ( $current_id === $target_id ) {
			return;
		}

		$target_pos = (int) $target['sort_order'];
		if ( $current_id > 0 ) {
			$current = $this->stages->get( $current_id );
			// Nur vorwaerts: nur setzen, wenn die aktuelle Position frueher liegt.
			if ( null !== $current && (int) $current['sort_order'] >= $target_pos ) {
				return;
			}
		}

		$this->contacts->set_stage( $contact_id, $target_id, 0 );
	}

	/**
	 * Legt die Wiedervorlage "Nachfassen: {Terminart}" faellig einen Tag nach dem
	 * Terminende an (TaskRepository). Das Faelligkeitsdatum wird in der Site-
	 * Zeitzone bestimmt.
	 *
	 * @param int                $contact_id Kontakt-ID.
	 * @param string             $type_label Name der Terminart.
	 * @param \DateTimeImmutable $end_utc    Terminende (UTC).
	 * @return void
	 */
	private function create_follow_up_task( int $contact_id, string $type_label, \DateTimeImmutable $end_utc ): void {
		$due = $end_utc->setTimezone( $this->site_timezone() )->add( new \DateInterval( 'P1D' ) )->format( 'Y-m-d' );
		/* translators: %s: Name der Terminart */
		$title = sprintf( __( 'Nachfassen: %s', 'kurato' ), $type_label );
		$this->tasks->create( $contact_id, $title, $due );
	}

	// --- Slot-Pruefung -----------------------------------------------------

	/**
	 * Prueft ueber den SlotCalculator, ob der gewuenschte Slot (noch) buchbar ist.
	 * Laedt Wochenraster, Ausnahmen und bestehende Buchungen des Hosts fuer den
	 * Kalendertag des Slots und vergleicht die Startzeit mit der Liste buchbarer
	 * Slots. Test-Seam.
	 *
	 * @param array<string, mixed> $host   Host.
	 * @param array<string, mixed> $type   Terminart.
	 * @param \DateTimeImmutable   $start  Gewuenschte Startzeit (UTC).
	 * @param int                  $now_ts "Jetzt" als Unix-Zeit.
	 * @return bool
	 */
	protected function is_slot_available( array $host, array $type, \DateTimeImmutable $start, int $now_ts ): bool {
		$utc     = new \DateTimeZone( 'UTC' );
		$site_tz = $this->site_timezone();
		$host_id = (int) $host['id'];
		$type_id = (int) $type['id'];

		// Fenster: der Kalendertag des Slots (Site-Zeit), grosszuegig plus/minus
		// einen Tag, damit Zeitzonen-Raender sicher abgedeckt sind.
		$day_site = $start->setTimezone( $site_tz )->format( 'Y-m-d' );
		$from     = ( new \DateTimeImmutable( $day_site . ' 00:00:00', $site_tz ) )->sub( new \DateInterval( 'P1D' ) )->setTimezone( $utc );
		$to       = ( new \DateTimeImmutable( $day_site . ' 00:00:00', $site_tz ) )->add( new \DateInterval( 'P2D' ) )->setTimezone( $utc );

		$now = ( new \DateTimeImmutable( '@' . $now_ts ) )->setTimezone( $utc );

		$slots = $this->slots->available_slots(
			$type,
			$this->availability->weekly_for_type( $host_id, $type_id ),
			$this->availability->exceptions_for_host( $host_id ),
			$this->bookings->bookings_for_host_between( $host_id, $from->format( 'Y-m-d H:i:s' ), $to->format( 'Y-m-d H:i:s' ) ),
			$from,
			$to,
			$now,
			$site_tz
		);

		$target = $start->setTimezone( $utc )->format( 'Y-m-d H:i:s' );
		foreach ( $slots as $slot ) {
			if ( $slot->format( 'Y-m-d H:i:s' ) === $target ) {
				return true;
			}
		}
		return false;
	}

	// --- Test-Seams --------------------------------------------------------

	/**
	 * ID der Pipeline-Stufe "Termin gebucht" (Setting, 0 = Automatik aus). Seam.
	 *
	 * @return int
	 */
	protected function stage_id(): int {
		return (int) Core::instance()->settings( 'kurato' )->get( 'booking.stage_id', 0 );
	}

	/**
	 * Schreibt einen Consent-Nachweis. Seam (Muster PreferenceCenter).
	 *
	 * @param array<string, mixed> $data Consent-Daten (Core-ConsentLog-Format).
	 * @return int Consent-ID (0 bei Fehler).
	 */
	protected function record_consent( array $data ): int {
		return Core::instance()->consent()->record( $data );
	}

	/**
	 * Feuert das Flotten-Event `booking_created`. Seam.
	 *
	 * @param array<string, mixed> $payload Event-Nutzlast.
	 * @return void
	 */
	protected function emit_created( array $payload ): void {
		EventBus::emit( 'booking_created', $payload );
	}

	/**
	 * Verschickt die Bestaetigungsmails an Besucher und Host. Seam.
	 *
	 * @param array<string, mixed> $booking Buchung.
	 * @param array<string, mixed> $host    Host.
	 * @param array<string, mixed> $type    Terminart.
	 * @param array<string, mixed> $contact Kontakt.
	 * @return void
	 */
	protected function send_confirmations( array $booking, array $host, array $type, array $contact ): void {
		if ( null !== $this->mailer ) {
			$this->mailer->send_confirmation( $booking, $host, $type, $contact );
		}
	}

	/**
	 * Merkt die erste eingegangene Buchung fuer den Hans-Meilenstein in der
	 * Buchungsliste (Option). Seam.
	 *
	 * @return void
	 */
	protected function mark_first_booking(): void {
		if ( false === get_option( 'kurato_first_booking_at', false ) ) {
			update_option( 'kurato_first_booking_at', time() );
		}
	}

	/**
	 * Site-Zeitzone. Seam (Standard UTC ohne WordPress).
	 *
	 * @return \DateTimeZone
	 */
	protected function site_timezone(): \DateTimeZone {
		if ( function_exists( 'wp_timezone' ) ) {
			return wp_timezone();
		}
		return new \DateTimeZone( 'UTC' );
	}

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

	/**
	 * Text der Datenschutz-Einwilligung fuer den Consent-Nachweis.
	 *
	 * @return string
	 */
	private function consent_text(): string {
		return __( 'Einwilligung in die Verarbeitung der angegebenen Daten zur Vereinbarung und Durchfuehrung des Termins (Buchung ueber die oeffentliche Terminseite).', 'kurato' );
	}

	/**
	 * Parst einen UTC-'Y-m-d H:i:s'-String in ein UTC-DateTimeImmutable.
	 *
	 * @param string $value Zeit.
	 * @return \DateTimeImmutable|null
	 */
	private function parse_utc( string $value ): ?\DateTimeImmutable {
		$value = trim( $value );
		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value ) ) {
			return null;
		}
		try {
			return new \DateTimeImmutable( $value, new \DateTimeZone( 'UTC' ) );
		} catch ( \Exception $e ) {
			return null;
		}
	}

	/**
	 * Fehler-Rueckgabe.
	 *
	 * @param string $code Fehlercode.
	 * @return array<string, mixed>
	 */
	private function fail( string $code ): array {
		return array(
			'ok'         => false,
			'error'      => $code,
			'booking_id' => 0,
		);
	}
}
