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

namespace Hafenstudios\Kurato\Booking;

defined( 'ABSPATH' ) || exit;

/**
 * Erzeugt iCalendar-Dateien (VCALENDAR/VEVENT) fuer Terminbuchungen (K5a,
 * K5-KONZEPT Abschnitt 5). Rein testbar: keine WordPress-Abhaengigkeit.
 *
 * - UID = `booking-{id}@{site_host}` (stabil ueber Bestaetigung und Storno).
 * - DTSTART/DTEND in UTC mit `Z`-Suffix (Ymd\THis\Z).
 * - SUMMARY/LOCATION/DESCRIPTION escaped nach RFC 5545 (Backslash, Semikolon,
 *   Komma, Zeilenumbruch), Zeilen auf 75 Oktette gefaltet, Zeilenende CRLF.
 * - METHOD:REQUEST (Einladung, SEQUENCE 0, STATUS CONFIRMED) und METHOD:CANCEL
 *   (Absage, SEQUENCE 1, STATUS CANCELLED). Minimales SEQUENCE-Handling reicht,
 *   weil v1 keine Umbuchung am selben Termin kennt (Umbuchung = Storno + neu).
 */
final class IcsBuilder {

	private const PRODID = '-//Hafenstudios//Kurato//DE';

	/**
	 * Baut eine Einladung (METHOD:REQUEST).
	 *
	 * @param array<string, mixed> $event id, site_host, start_utc, end_utc,
	 *      summary, location, description, optional organizer_email, dtstamp.
	 * @return string ICS-Text.
	 */
	public function build( array $event ): string {
		return $this->build_vcalendar( $event, 'REQUEST', 0, 'CONFIRMED' );
	}

	/**
	 * Baut eine Absage (METHOD:CANCEL).
	 *
	 * @param array<string, mixed> $event Wie {@see build()}.
	 * @return string ICS-Text.
	 */
	public function build_cancel( array $event ): string {
		return $this->build_vcalendar( $event, 'CANCEL', 1, 'CANCELLED' );
	}

	/**
	 * Baut das komplette VCALENDAR-Dokument.
	 *
	 * @param array<string, mixed> $event    Termindaten.
	 * @param string               $method   REQUEST | CANCEL.
	 * @param int                  $sequence SEQUENCE-Wert.
	 * @param string               $status   CONFIRMED | CANCELLED.
	 * @return string
	 */
	private function build_vcalendar( array $event, string $method, int $sequence, string $status ): string {
		$id        = absint( $event['id'] ?? 0 );
		$site_host = $this->host( (string) ( $event['site_host'] ?? 'localhost' ) );
		$uid       = 'booking-' . $id . '@' . $site_host;

		$dtstamp = $this->format_utc( $event['dtstamp'] ?? 'now' );
		$dtstart = $this->format_utc( $event['start_utc'] ?? 'now' );
		$dtend   = $this->format_utc( $event['end_utc'] ?? ( $event['start_utc'] ?? 'now' ) );

		$lines   = array();
		$lines[] = 'BEGIN:VCALENDAR';
		$lines[] = 'VERSION:2.0';
		$lines[] = 'PRODID:' . self::PRODID;
		$lines[] = 'CALSCALE:GREGORIAN';
		$lines[] = 'METHOD:' . $method;
		$lines[] = 'BEGIN:VEVENT';
		$lines[] = 'UID:' . $this->escape_text( $uid );
		$lines[] = 'SEQUENCE:' . $sequence;
		$lines[] = 'DTSTAMP:' . $dtstamp;
		$lines[] = 'DTSTART:' . $dtstart;
		$lines[] = 'DTEND:' . $dtend;
		$lines[] = 'SUMMARY:' . $this->escape_text( (string) ( $event['summary'] ?? '' ) );

		$location = (string) ( $event['location'] ?? '' );
		if ( '' !== $location ) {
			$lines[] = 'LOCATION:' . $this->escape_text( $location );
		}
		$description = (string) ( $event['description'] ?? '' );
		if ( '' !== $description ) {
			$lines[] = 'DESCRIPTION:' . $this->escape_text( $description );
		}

		$organizer = (string) ( $event['organizer_email'] ?? '' );
		if ( '' !== $organizer ) {
			$lines[] = 'ORGANIZER:mailto:' . $organizer;
		}

		$lines[] = 'STATUS:' . $status;
		$lines[] = 'END:VEVENT';
		$lines[] = 'END:VCALENDAR';

		$folded = array_map( array( $this, 'fold' ), $lines );
		return implode( "\r\n", $folded ) . "\r\n";
	}

	/**
	 * Formatiert eine Zeit als UTC im iCalendar-Format (Ymd\THis\Z).
	 *
	 * @param mixed $value DateTimeInterface oder ein von DateTimeImmutable
	 *                     parsbarer String (als UTC interpretiert).
	 * @return string
	 */
	private function format_utc( $value ): string {
		$utc = new \DateTimeZone( 'UTC' );
		if ( $value instanceof \DateTimeInterface ) {
			$dt = \DateTimeImmutable::createFromInterface( $value )->setTimezone( $utc );
		} else {
			try {
				$dt = new \DateTimeImmutable( (string) $value, $utc );
			} catch ( \Exception $e ) {
				$dt = new \DateTimeImmutable( 'now', $utc );
			}
			$dt = $dt->setTimezone( $utc );
		}
		return $dt->format( 'Ymd\THis\Z' );
	}

	/**
	 * Escapt einen Textwert nach RFC 5545 (Reihenfolge: Backslash zuerst).
	 *
	 * @param string $text Rohtext.
	 * @return string
	 */
	private function escape_text( string $text ): string {
		$text = str_replace( '\\', '\\\\', $text );
		$text = str_replace( array( "\r\n", "\r", "\n" ), '\\n', $text );
		$text = str_replace( ';', '\\;', $text );
		$text = str_replace( ',', '\\,', $text );
		return $text;
	}

	/**
	 * Faltet eine Content-Zeile auf hoechstens 75 Oktette (RFC 5545). Fortsetzungs-
	 * zeilen beginnen mit einem Leerzeichen; Mehrbyte-UTF-8-Zeichen werden nie
	 * zwischen zwei Zeilen zerschnitten.
	 *
	 * @param string $line Content-Zeile (ohne CRLF).
	 * @return string Gefaltete Zeile(n), mit CRLF getrennt.
	 */
	private function fold( string $line ): string {
		if ( strlen( $line ) <= 75 ) {
			return $line;
		}
		$chars     = preg_split( '//u', $line, -1, PREG_SPLIT_NO_EMPTY );
		$physical  = array();
		$current   = '';
		$cur_bytes = 0;
		foreach ( (array) $chars as $ch ) {
			$b = strlen( $ch );
			if ( $cur_bytes + $b > 75 ) {
				$physical[] = $current;
				$current    = ' ' . $ch; // Fortsetzung mit fuehrendem Leerzeichen.
				$cur_bytes  = 1 + $b;
			} else {
				$current   .= $ch;
				$cur_bytes += $b;
			}
		}
		$physical[] = $current;
		return implode( "\r\n", $physical );
	}

	/**
	 * Reduziert einen Site-Host-Wert auf einen sauberen Hostnamen fuer die UID.
	 *
	 * @param string $host Roher Host (evtl. mit Schema/Pfad).
	 * @return string
	 */
	private function host( string $host ): string {
		$host = trim( $host );
		if ( false !== strpos( $host, '//' ) ) {
			$parsed = wp_parse_url( $host, PHP_URL_HOST );
			if ( is_string( $parsed ) && '' !== $parsed ) {
				$host = $parsed;
			}
		}
		$host = preg_replace( '/[^A-Za-z0-9.\-]/', '', $host );
		return '' !== (string) $host ? (string) $host : 'localhost';
	}
}
