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

namespace Hafenstudios\Kurato\Booking;

use Hafenstudios\Kurato\Db\BookingTypeRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Reine Eingabe-Helfer der Termine-Verwaltung (K5-KONZEPT Abschnitt 7).
 *
 * Buendelt die Logik, die die Render-/Handler-Methoden der Admin-Seiten sonst
 * verstecken wuerden, in pruefbaren statischen Funktionen: Slug-Eindeutigkeit
 * (Kollision -> Suffix), Sanitisierung/Deckelung einer Terminart (Dauer, Puffer,
 * Vorlauf, Horizont) und die Validierung eines Wochenraster-Fensters (Ende muss
 * nach Start liegen). Kein wpdb, kein HTTP - nur Eingabe rein, saubere Werte raus.
 */
final class BookingInput {

	/**
	 * Obergrenzen der Terminart-Zahlen (verhindern Unsinns- und Riesenwerte).
	 */
	public const MAX_DURATION_MIN = 1440;   // 24 Stunden.
	public const MAX_BUFFER_MIN   = 1440;   // 24 Stunden je Seite.
	public const MAX_LEAD_HOURS   = 8760;   // Ein Jahr Vorlauf.
	public const MAX_HORIZON_DAYS = 3650;   // Zehn Jahre Maximalfrist.

	/**
	 * Erzeugt aus einem gewuenschten Slug einen, der nicht mit vorhandenen
	 * kollidiert. Bei Kollision wird -2, -3, ... angehaengt. Der Vergleich ist
	 * case-insensitiv auf bereits normalisierte Slugs ausgelegt.
	 *
	 * @param string            $desired  Gewuenschter (bereits per sanitize_title bereinigter) Slug.
	 * @param array<int, string> $existing Vorhandene Slugs (ohne den eigenen bei Bearbeitung).
	 * @param string            $fallback Basis, falls $desired leer ist.
	 * @return string
	 */
	public static function unique_slug( string $desired, array $existing, string $fallback = 'host' ): string {
		$base = '' !== $desired ? $desired : $fallback;
		$set  = array();
		foreach ( $existing as $slug ) {
			$set[ strtolower( (string) $slug ) ] = true;
		}
		$try = $base;
		$n   = 1;
		while ( isset( $set[ strtolower( $try ) ] ) ) {
			$n++;
			$try = $base . '-' . $n;
		}
		return $try;
	}

	/**
	 * Sanitisiert und deckelt die Zahlenfelder einer Terminart aus einem
	 * Roh-POST. Negative Werte werden zu 0 (bzw. dem Minimum), Riesenwerte auf die
	 * Obergrenzen gekappt. Dauer und Horizont haben ein Minimum von 1.
	 *
	 * @param array<string, mixed> $raw Rohwerte (label, duration_min, location_type,
	 *                                   location_detail, buffer_before_min,
	 *                                   buffer_after_min, lead_time_hours,
	 *                                   horizon_days, active).
	 * @return array{label:string,location_type:string,location_detail:string,duration_min:int,buffer_before_min:int,buffer_after_min:int,lead_time_hours:int,horizon_days:int,active:bool}
	 */
	public static function sanitize_type( array $raw ): array {
		return array(
			'label'             => trim( sanitize_text_field( (string) ( $raw['label'] ?? '' ) ) ),
			'location_type'     => self::location_type( (string) ( $raw['location_type'] ?? 'phone' ) ),
			'location_detail'   => trim( sanitize_text_field( (string) ( $raw['location_detail'] ?? '' ) ) ),
			'duration_min'      => self::clamp_int( $raw['duration_min'] ?? 30, 1, self::MAX_DURATION_MIN ),
			'buffer_before_min' => self::clamp_int( $raw['buffer_before_min'] ?? 0, 0, self::MAX_BUFFER_MIN ),
			'buffer_after_min'  => self::clamp_int( $raw['buffer_after_min'] ?? 0, 0, self::MAX_BUFFER_MIN ),
			'lead_time_hours'   => self::clamp_int( $raw['lead_time_hours'] ?? 0, 0, self::MAX_LEAD_HOURS ),
			'horizon_days'      => self::clamp_int( $raw['horizon_days'] ?? 60, 1, self::MAX_HORIZON_DAYS ),
			'active'            => ! empty( $raw['active'] ),
		);
	}

	/**
	 * Validiert einen Ortstyp gegen die erlaubte Menge des Repositories.
	 *
	 * @param string $value Rohwert.
	 * @return string phone|video|in_person.
	 */
	public static function location_type( string $value ): string {
		$loc = strtolower( trim( $value ) );
		return in_array( $loc, BookingTypeRepository::LOCATION_TYPES, true ) ? $loc : 'phone';
	}

	/**
	 * Normalisiert eine Uhrzeit auf 'H:i'. Ungueltig => null.
	 *
	 * @param string $time Rohzeit.
	 * @return string|null
	 */
	public static function normalize_time( string $time ): ?string {
		$time = trim( $time );
		if ( ! preg_match( '/^([01]?\d|2[0-3]):([0-5]\d)(?::[0-5]\d)?$/', $time, $m ) ) {
			return null;
		}
		return sprintf( '%02d:%02d', (int) $m[1], (int) $m[2] );
	}

	/**
	 * Prueft, ob ein Wochenraster-Fenster gueltig ist: beide Zeiten valide und
	 * Ende strikt nach Start (Ende <= Start wird abgelehnt).
	 *
	 * @param string $start Startzeit ('H:i').
	 * @param string $end   Endzeit ('H:i').
	 * @return bool
	 */
	public static function valid_window( string $start, string $end ): bool {
		$s = self::normalize_time( $start );
		$e = self::normalize_time( $end );
		if ( null === $s || null === $e ) {
			return false;
		}
		return self::minutes( $e ) > self::minutes( $s );
	}

	/**
	 * Minuten seit Mitternacht fuer eine normalisierte 'H:i'-Zeit.
	 *
	 * @param string $time Normalisierte Zeit.
	 * @return int
	 */
	private static function minutes( string $time ): int {
		list( $h, $m ) = array_map( 'intval', explode( ':', $time ) );
		return $h * 60 + $m;
	}

	/**
	 * Klemmt einen Rohwert in ein ganzzahliges Intervall. Nicht-numerische und
	 * negative Werte fallen auf das Minimum.
	 *
	 * @param mixed $value Rohwert.
	 * @param int   $min   Untergrenze.
	 * @param int   $max   Obergrenze.
	 * @return int
	 */
	private static function clamp_int( $value, int $min, int $max ): int {
		if ( is_bool( $value ) || ! is_numeric( $value ) ) {
			$value = 0;
		}
		$int = (int) $value;
		if ( $int < $min ) {
			return $min;
		}
		if ( $int > $max ) {
			return $max;
		}
		return $int;
	}
}
