<?php
/**
 * Calendar integration for table reservations.
 *
 * @package FoodOrd
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Foodord_Table_Reservation_Calendar
 */
class Foodord_Table_Reservation_Calendar {
	private const QUERY_RESERVATION = 'foodord_reservation_calendar';
	private const QUERY_SIGNATURE   = 'foodord_reservation_calendar_key';

	/**
	 * Register the signed calendar download endpoint.
	 */
	public static function init(): void {
		add_action( 'template_redirect', array( __CLASS__, 'maybe_download_calendar' ), 1 );
	}

	/**
	 * Create a temporary calendar attachment for a customer email.
	 *
	 * @param object $reservation Reservation row.
	 * @param string $email_status Email status being sent.
	 */
	public static function create_email_attachment( $reservation, string $email_status ): string {
		if ( ! self::supports_email_status( $reservation, $email_status ) ) {
			return '';
		}

		$content = self::build_calendar( $reservation, $email_status );
		if ( '' === $content ) {
			return '';
		}

		$filename       = self::get_filename( $reservation );
		if ( ! function_exists( 'wp_tempnam' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}
		$temporary_file = wp_tempnam( $filename );
		if ( ! is_string( $temporary_file ) || '' === $temporary_file ) {
			return '';
		}

		$directory = dirname( $temporary_file );
		$filepath  = trailingslashit( $directory ) . wp_unique_filename( $directory, $filename );
		wp_delete_file( $temporary_file );

		if ( ! class_exists( 'WP_Filesystem_Base' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
		}
		if ( ! class_exists( 'WP_Filesystem_Direct' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
		}

		$filesystem = new WP_Filesystem_Direct( null );
		if ( ! $filesystem->put_contents( $filepath, $content, 0600 ) ) {
			wp_delete_file( $filepath );
			return '';
		}

		return $filepath;
	}

	/**
	 * Delete a temporary calendar attachment.
	 *
	 * @param string $filepath Temporary file path.
	 */
	public static function delete_email_attachment( string $filepath ): void {
		if ( '' !== $filepath ) {
			wp_delete_file( $filepath );
		}
	}

	/**
	 * Serve a signed calendar download request.
	 */
	public static function maybe_download_calendar(): void {
		$reservation_id = filter_input( INPUT_GET, self::QUERY_RESERVATION, FILTER_VALIDATE_INT );
		$signature      = filter_input( INPUT_GET, self::QUERY_SIGNATURE, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
		$signature      = is_string( $signature ) ? sanitize_text_field( $signature ) : '';

		if ( ! is_int( $reservation_id ) || $reservation_id <= 0 || '' === $signature ) {
			return;
		}

		$reservations = new Foodord_Table_Reservations();
		$reservation  = $reservations->get_reservation( $reservation_id );
		if ( ! $reservation || ! hash_equals( self::get_signature( $reservation ), $signature ) ) {
			self::send_not_found_response();
		}

		$status = isset( $reservation->status ) ? Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) : '';
		if ( ! in_array( $status, array( 'confirmed', 'cancelled', 'completed', 'no_show' ), true ) ) {
			self::send_not_found_response();
		}

		$calendar_status = 'cancelled' === $status ? 'cancelled' : 'confirmed';
		$content         = self::build_calendar( $reservation, $calendar_status );
		if ( '' === $content ) {
			self::send_not_found_response();
		}

		$method = 'cancelled' === $calendar_status ? 'CANCEL' : 'REQUEST';
		nocache_headers();
		header( 'Content-Type: text/calendar; charset=UTF-8; method=' . $method );
		header( 'Content-Disposition: attachment; filename="' . self::get_filename( $reservation ) . '"' );
		header( 'X-Content-Type-Options: nosniff' );
		header( 'X-Robots-Tag: noindex, nofollow', true );
		echo wp_kses( $content, array() );
		exit;
	}

	/**
	 * Determine whether an email should include calendar integration.
	 *
	 * @param object $reservation Reservation row.
	 * @param string $email_status Email status being sent.
	 */
	private static function supports_email_status( $reservation, string $email_status ): bool {
		$reservation_status = isset( $reservation->status ) ? Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) : '';

		if ( 'confirmed' === $email_status ) {
			return 'confirmed' === $reservation_status;
		}

		if ( 'updated' === $email_status ) {
			return 'confirmed' === $reservation_status;
		}

		return 'cancelled' === $email_status && 'cancelled' === $reservation_status;
	}

	/**
	 * Build a signature for a reservation calendar link.
	 *
	 * @param object $reservation Reservation row.
	 */
	private static function get_signature( $reservation ): string {
		$reservation_id   = absint( $reservation->id ?? 0 );
		$reservation_code = sanitize_text_field( (string) ( $reservation->reservation_code ?? '' ) );

		return hash_hmac( 'sha256', $reservation_id . '|' . $reservation_code, wp_salt( 'auth' ) );
	}

	/**
	 * Build an RFC 5545 calendar payload.
	 *
	 * @param object $reservation Reservation row.
	 * @param string $calendar_status Calendar status.
	 */
	private static function build_calendar( $reservation, string $calendar_status ): string {
		$start = self::get_start_datetime( $reservation );
		if ( ! $start ) {
			return '';
		}

		$duration = Foodord_Table_Reservations::sanitize_reservation_duration_minutes( $reservation->duration_minutes ?? 90 );
		$end      = $start->modify( '+' . $duration . ' minutes' );
		$method   = 'cancelled' === $calendar_status ? 'CANCEL' : 'REQUEST';
		$status   = 'cancelled' === $calendar_status ? 'CANCELLED' : 'CONFIRMED';
		$now      = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
		$updated  = self::get_gmt_datetime( (string) ( $reservation->updated_at_gmt ?? '' ), $now );
		$created  = self::get_gmt_datetime( (string) ( $reservation->created_at_gmt ?? '' ), $updated );
		$business = self::get_business_name();
		$customer = sanitize_text_field( (string) ( $reservation->customer_name ?? '' ) );
		$email    = sanitize_email( (string) ( $reservation->customer_email ?? '' ) );
		$from     = self::get_organizer();
		$lines    = array(
			'BEGIN:VCALENDAR',
			'PRODID:-//FoodOrd//Table Reservations//EN',
			'VERSION:2.0',
			'CALSCALE:GREGORIAN',
			'METHOD:' . $method,
			'BEGIN:VEVENT',
			'UID:' . self::escape_text( self::get_uid( $reservation ) ),
			'DTSTAMP:' . self::format_utc( $now ),
			'CREATED:' . self::format_utc( $created ),
			'LAST-MODIFIED:' . self::format_utc( $updated ),
			'SEQUENCE:' . max( 0, $updated->getTimestamp() ),
			'DTSTART:' . self::format_utc( $start ),
			'DTEND:' . self::format_utc( $end ),
			'STATUS:' . $status,
			'SUMMARY:' . self::escape_text( self::get_summary( $business ) ),
			'DESCRIPTION:' . self::escape_text( self::get_description( $reservation ) ),
			'LOCATION:' . self::escape_text( self::get_location() ),
			'TRANSP:OPAQUE',
		);

		if ( '' !== $from['email'] ) {
			$lines[] = 'ORGANIZER;CN="' . self::escape_parameter( $from['name'] ) . '":mailto:' . $from['email'];
		}

		if ( is_email( $email ) ) {
			$lines[] = 'ATTENDEE;CN="' . self::escape_parameter( $customer ) . '";RSVP=TRUE:mailto:' . $email;
		}

		$lines[] = 'END:VEVENT';
		$lines[] = 'END:VCALENDAR';

		return self::fold_lines( $lines ) . "\r\n";
	}

	/**
	 * Parse the reservation start in the site timezone.
	 *
	 * @param object $reservation Reservation row.
	 */
	private static function get_start_datetime( $reservation ): ?DateTimeImmutable {
		$date = sanitize_text_field( (string) ( $reservation->reservation_date ?? '' ) );
		$time = sanitize_text_field( (string) ( $reservation->reservation_time ?? '' ) );
		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) || ! preg_match( '/^\d{2}:\d{2}(?::\d{2})?$/', $time ) ) {
			return null;
		}

		if ( 5 === strlen( $time ) ) {
			$time .= ':00';
		}

		$start = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', $date . ' ' . $time, wp_timezone() );

		return $start instanceof DateTimeImmutable ? $start : null;
	}

	/**
	 * Parse a GMT database timestamp.
	 *
	 * @param string            $value Timestamp value.
	 * @param DateTimeImmutable $fallback Fallback timestamp.
	 */
	private static function get_gmt_datetime( string $value, DateTimeImmutable $fallback ): DateTimeImmutable {
		$datetime = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', $value, new DateTimeZone( 'UTC' ) );

		return $datetime instanceof DateTimeImmutable ? $datetime : $fallback;
	}

	/**
	 * Format a timestamp as UTC iCalendar date-time.
	 *
	 * @param DateTimeImmutable $datetime Timestamp to format.
	 */
	private static function format_utc( DateTimeImmutable $datetime ): string {
		return $datetime->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Ymd\THis\Z' );
	}

	/**
	 * Build the stable event UID.
	 *
	 * @param object $reservation Reservation row.
	 */
	private static function get_uid( $reservation ): string {
		$code = sanitize_key( (string) ( $reservation->reservation_code ?? '' ) );
		if ( '' === $code ) {
			$code = (string) absint( $reservation->id ?? 0 );
		}

		$host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
		$host = is_string( $host ) && '' !== $host ? strtolower( $host ) : 'localhost';

		return 'foodord-reservation-' . $code . '@' . $host;
	}

	/**
	 * Get the translated calendar summary.
	 *
	 * @param string $business Business name.
	 */
	private static function get_summary( string $business ): string {
		/* translators: %s: Restaurant name. */
		return sprintf( __( 'Reservation at %s', 'foodord' ), $business );
	}

	/**
	 * Get the calendar event description.
	 *
	 * @param object $reservation Reservation row.
	 */
	private static function get_description( $reservation ): string {
		$code       = sanitize_text_field( (string) ( $reservation->reservation_code ?? '' ) );
		$party_size = max( 1, absint( $reservation->party_size ?? 1 ) );
		$table      = sanitize_text_field( (string) ( $reservation->table_name ?? '' ) );
		$floor      = sanitize_text_field( (string) ( $reservation->floor_name ?? '' ) );
		$location   = implode( ' - ', array_filter( array( $floor, $table ) ) );
		$lines      = array(
			__( 'Reservation code', 'foodord' ) . ': ' . $code,
			__( 'Guests', 'foodord' ) . ': ' . $party_size,
		);

		if ( '' !== $location ) {
			$lines[] = __( 'Table', 'foodord' ) . ': ' . $location;
		}

		return implode( "\n", $lines );
	}

	/**
	 * Get the restaurant location.
	 */
	private static function get_location(): string {
		$parts = array_filter(
			array_map(
				'sanitize_text_field',
				array(
					(string) get_option( 'woocommerce_store_address', '' ),
					(string) get_option( 'woocommerce_store_address_2', '' ),
					(string) get_option( 'woocommerce_store_city', '' ),
					(string) get_option( 'woocommerce_store_postcode', '' ),
				)
			)
		);

		return ! empty( $parts ) ? implode( ', ', $parts ) : self::get_business_name();
	}

	/**
	 * Get the calendar organizer details.
	 *
	 * @return array{name:string,email:string}
	 */
	private static function get_organizer(): array {
		$email = sanitize_email( (string) get_option( 'woocommerce_email_from_address', get_option( 'admin_email', '' ) ) );
		$name  = sanitize_text_field( (string) get_option( 'woocommerce_email_from_name', self::get_business_name() ) );

		return array(
			'name'  => '' !== $name ? $name : self::get_business_name(),
			'email' => is_email( $email ) ? $email : '',
		);
	}

	/**
	 * Get the business name.
	 */
	private static function get_business_name(): string {
		$name = sanitize_text_field( (string) get_bloginfo( 'name' ) );

		return '' !== $name ? $name : 'FoodOrd';
	}

	/**
	 * Build the calendar filename.
	 *
	 * @param object $reservation Reservation row.
	 */
	private static function get_filename( $reservation ): string {
		$code = sanitize_file_name( (string) ( $reservation->reservation_code ?? '' ) );
		$code = '' !== $code ? strtolower( $code ) : (string) absint( $reservation->id ?? 0 );

		return 'reservation-' . $code . '.ics';
	}

	/**
	 * Escape an iCalendar text value.
	 *
	 * @param string $value Text value.
	 */
	private static function escape_text( string $value ): string {
		$value = str_replace( "\r\n", "\n", $value );
		$value = str_replace( "\r", "\n", $value );
		$value = str_replace( '\\', '\\\\', $value );
		$value = str_replace( array( ';', ',', "\n" ), array( '\\;', '\\,', '\\n' ), $value );

		return $value;
	}

	/**
	 * Escape an iCalendar parameter value.
	 *
	 * @param string $value Parameter value.
	 */
	private static function escape_parameter( string $value ): string {
		return str_replace( array( '"', "\r", "\n" ), '', sanitize_text_field( $value ) );
	}

	/**
	 * Fold iCalendar content lines at 75 octets.
	 *
	 * @param array<int,string> $lines Calendar content lines.
	 */
	private static function fold_lines( array $lines ): string {
		$folded = array();

		foreach ( $lines as $line ) {
			$characters = preg_split( '//u', $line, -1, PREG_SPLIT_NO_EMPTY );
			if ( ! is_array( $characters ) ) {
				$folded[] = $line;
				continue;
			}

			$current = '';
			$limit   = 75;
			foreach ( $characters as $character ) {
				if ( strlen( $current . $character ) > $limit ) {
					$folded[] = $current;
					$current  = ' ' . $character;
					$limit    = 75;
					continue;
				}
				$current .= $character;
			}

			$folded[] = $current;
		}

		return implode( "\r\n", $folded );
	}

	/**
	 * Send a generic not-found response without exposing reservation data.
	 */
	private static function send_not_found_response(): void {
		status_header( 404 );
		nocache_headers();
		exit;
	}
}
