<?php
/**
 * Methods for importing ics calendar files.
 *
 * @package Sunflower 26
 */

/**
 * Load class namespaces.
 */
require get_template_directory() . '/lib/vendor/autoload.php';

use Sabre\VObject\ParseException;
use Sabre\VObject\Reader;

/**
 * The import function.
 *
 * @param string|boolean $url The URL to the ics-file.
 * @param boolean        $auto_categories Additional categories to add to every imported event.
 */
function sunflower_icalimport( $url = false, $auto_categories = false ) {
	try {
		$vcalendar = Reader::read(
			wp_remote_retrieve_body(
				wp_remote_get( ( $url ) ),
				Reader::OPTION_FORGIVING
			)
		);
	} catch ( ParseException $parse_exception ) {
		return $parse_exception->getMessage();
	}

	try {
		// If timezone_string contains a valid timezone string, we create a timezone object of it.
		$timezone = new \DateTimeZone( get_option( 'timezone_string' ) );
	} catch ( Exception $e ) {
		// Otherwise we fall back to 'Europe/Berlin'.
		$timezone = new \DateTimeZone( 'Europe/Berlin' );
	}

	$time_range_history   = sunflower_get_constant( 'SUNFLOWER_EVENT_TIME_RANGE_BACK' ) ? sunflower_get_constant( 'SUNFLOWER_EVENT_TIME_RANGE_BACK' ) : '0 months';
	$time_range_future    = sunflower_get_constant( 'SUNFLOWER_EVENT_TIME_RANGE' ) ? sunflower_get_constant( 'SUNFLOWER_EVENT_TIME_RANGE' ) : '6 months';
	$recurring_events_max = (int) sunflower_get_constant( 'SUNFLOWER_EVENT_RECURRING_EVENTS' ) ? (int) sunflower_get_constant( 'SUNFLOWER_EVENT_RECURRING_EVENTS' ) : 10;

	$time_range_start = new \DateTime();
	$time_range_start->setTimestamp( strtotime( '-' . $time_range_history ) );

	$time_range_stop = new \DateTime();
	$time_range_stop->setTimestamp( strtotime( (string) $time_range_future ) );

	$timezone_fix = null;
	if ( sunflower_get_setting( 'sunflower_fix_time_zone_error' ) ) {
		$timezone_fix = $timezone;
	}

	// expand RRULE events to new vCalendar which has all events in the given time range.
	$new_vcalendar = $vcalendar->expand( $time_range_start, $time_range_stop, $timezone_fix );

	$all_events = array();
	if ( is_iterable( $new_vcalendar->VEVENT ) ) { // phpcs:ignore
		foreach ( $new_vcalendar->VEVENT as $event ) { // phpcs:ignore
			// Limit to events in the given time range.
			if ( $event->isInTimeRange( $time_range_start, $time_range_stop ) ) {
				$all_events[] = $event;
			}
		}
	}

	$updated_events         = 0;
	$new_events             = 0;
	$ids_from_remote        = array();
	$count_recurring_events = array();

	foreach ( $all_events as $event ) {
		$uid = $event->UID->getValue(); // phpcs:ignore

		// modified instances of a recurring event, RECURRENCE-ID is set but no RRULE.
		if ( isset( $event->RRULE ) || isset( $event->{'RECURRENCE-ID'} ) ) { // phpcs:ignore
			if ( isset( $count_recurring_events[ $uid ] ) ) {
				++$count_recurring_events[ $uid ];
			} else {
				$count_recurring_events[ $uid ] = 1;
			}

			if ( $count_recurring_events[ $uid ] > $recurring_events_max ) {
				continue;
			}

			$uid .= '_' . $event->DTSTART->getValue(); // phpcs:ignore
		}

		// Is this event already imported.
		$is_imported = sunflower_get_event_by_uid( $uid );
		$wp_id       = 0;
		if ( $is_imported->have_posts() ) {
			$is_imported->the_post();
			$wp_id = get_the_ID();
		}

		// Make plain URLs and e-mail addresses in the description clickable
		// before newlines are converted, so links pasted into the iCal
		// DESCRIPTION (e.g. from Nextcloud) become usable (#1155).
		$post_content = sprintf( '<!-- wp:paragraph --><p>%s</p><!-- /wp:paragraph -->', nl2br( make_clickable( (string) $event->DESCRIPTION ) ) ); // phpcs:ignore

		// The iCal URL field may omit the scheme (e.g. "www.example.com");
		// normalise it once for both the link below and the stored meta (#1154).
		$event_url = isset( $event->URL ) ? sunflower_normalize_url( (string) $event->URL ) : ''; // phpcs:ignore

		if ( $event_url ) {
			$post_content .= sprintf( '<!-- wp:paragraph --><p><a href="%1$s" title="%2$s" target="_blank">%3$s&nbsp;<i class="fa-solid fa-up-right-from-square"></i></a></p><!-- /wp:paragraph -->', $event_url, __( 'Open link in a new tab', 'default' ), __( 'More Information', 'sunflower' ) );
		}

		$post = array(
			'ID'           => $wp_id,
			'post_type'    => 'sunflower_event',
			'post_title'   => $event->SUMMARY->getValue(), // phpcs:ignore
			'post_content' => $post_content,
			'post_status'  => 'publish',
		);
		$id   = wp_insert_post( (array) $post, true );

		if ( ! is_int( $id ) ) {
			printf(
				'
				<p class="notice notice-warning notice-large">
				%1$s: %2$s - "%3$s"
				</p>',
				esc_attr__( 'Failed to import event', 'sunflower' ),
				esc_attr( $event->DTSTART->getDateTime()->format( 'd.m.Y' ) ), // phpcs:ignore
				esc_attr( $event->SUMMARY->getValue() ) // phpcs:ignore
			);
			continue;
		}

		if ( 0 === $wp_id ) {
			++$new_events;
		} elseif ( $wp_id === $id ) {
			++$updated_events;
		}

		// Save all event post ids from imported ics ressources.
		$ids_from_remote[] = $id;

		// Write start and end time to event post metadata.
		if ( $event->DTSTART instanceof \Sabre\VObject\Property\ICalendar\Date || // phpcs:ignore
			( $event->DTSTART->getDateTime()->format( 'H:i' ) == '00:00' && $event->DTEND->getDateTime()->format( 'H:i' ) == '00:00' ) // phpcs:ignore
		) {
			update_post_meta( $id, '_sunflower_event_whole_day', 'checked' );
			update_post_meta( $id, '_sunflower_event_from', $event->DTSTART->getDateTime()->format( 'Y-m-d' ) ); // phpcs:ignore
			update_post_meta( $id, '_sunflower_event_until', $event->DTEND?->getDateTime()->format( 'Y-m-d' ) ); // phpcs:ignore
		} else {
			delete_post_meta( $id, '_sunflower_event_whole_day' );
			update_post_meta( $id, '_sunflower_event_from', $event->DTSTART->getDateTime( $timezone_fix )->setTimezone( $timezone )->format( 'Y-m-d H:i' ) ); // phpcs:ignore
			update_post_meta( $id, '_sunflower_event_until', $event->DTEND?->getDateTime( $timezone_fix )->setTimezone( $timezone )->format( 'Y-m-d H:i' ) ); // phpcs:ignore
		}

		update_post_meta( $id, '_sunflower_event_uid', $uid );
		update_post_meta( $id, '_sunflower_event_source', md5( $url ) );

		if ( isset( $event->LOCATION ) ) { // phpcs:ignore
			update_post_meta( $id, '_sunflower_event_location_name', (string) $event->LOCATION ); // phpcs:ignore

			if ( ! filter_var( (string) $event->LOCATION, FILTER_VALIDATE_URL ) ) { // phpcs:ignore
				$coordinates = sunflower_geocode( (string) $event->LOCATION ); // phpcs:ignore
				if ( $coordinates ) {
					[$lon, $lat] = $coordinates;
					update_post_meta( $id, '_sunflower_event_lat', $lat );
					update_post_meta( $id, '_sunflower_event_lon', $lon );
					$zoom = sunflower_get_constant( 'SUNFLOWER_EVENT_IMPORTED_ZOOM' ) ? sunflower_get_constant( 'SUNFLOWER_EVENT_IMPORTED_ZOOM' ) : 12;
					update_post_meta( $id, '_sunflower_event_zoom', $zoom );
				}
			}
		}

		if ( $event_url ) {
			update_post_meta( $id, '_sunflower_event_url', $event_url );
		}

		$categories  = $event->CATEGORIES ?? ''; // phpcs:ignore
		$categories .= ( $auto_categories ) ? ',' . $auto_categories : '';
		if ( '' === $categories ) {
			continue;
		}

		if ( '0' === $categories ) {
			continue;
		}

		wp_set_post_terms( $id, $categories, 'sunflower_event_tag' );
	}

	return array(
		$ids_from_remote,
		count( $all_events ) - $updated_events,
		'updated_events' => $updated_events,
		'new_events'     => $new_events,
	);
}

/**
 * Ensure an event URL has a scheme so it validates and links absolutely.
 *
 * The iCal URL field may hold a URL without a scheme (e.g. "www.example.com").
 * Such values fail FILTER_VALIDATE_URL and would link relative to the current
 * page, so a missing scheme is filled with "https://".
 *
 * @param string $url The raw URL from the iCal event.
 * @return string The validated URL including a scheme, or '' if it is no URL.
 */
function sunflower_normalize_url( $url ) {
	$url = trim( $url );

	if ( '' === $url ) {
		return '';
	}

	if ( ! wp_parse_url( $url, PHP_URL_SCHEME ) ) {
		$url = 'https://' . $url;
	}

	return filter_var( $url, FILTER_VALIDATE_URL ) ? $url : '';
}

/**
 * Get post type "sunflower_event" for given uid.
 *
 * @param int $uid The uid of the sunflower event.
 */
function sunflower_get_event_by_uid( $uid ) {
	return new WP_Query(
		array(
			'post_type'  => 'sunflower_event',
			'meta_key'   => '_sunflower_event_uid',
			'orderby'    => 'meta_value',
			'meta_query' => array(
				array(
					'key'     => '_sunflower_event_uid',
					'value'   => $uid,
					'compare' => '=',
				),
			),
		)
	);
}

/**
 * Get all sunflower events with an uid set.
 *
 * @param int $source The hash value of the source URL.
 */
function sunflower_get_events_having_uid( $source ) {
	$events_with_uid = new WP_Query(
		array(
			'nopaging'   => true,
			'post_type'  => 'sunflower_event',
			'meta_key'   => '_sunflower_event_uid',
			'orderby'    => 'meta_value',
			'meta_query' => array(
				'relation' => 'AND',

				array(
					'key'     => '_sunflower_event_uid',
					'compare' => 'EXISTS',
				),
				array(
					'key'   => '_sunflower_event_source',
					'value' => $source,
				),
			),
		)
	);

	$ids = array();
	while ( $events_with_uid->have_posts() ) {
		$events_with_uid->the_post();
		$ids[] = get_the_ID();
	}

	return $ids;
}

/**
 * Get all post type "sunflower_event" from given source.
 *
 * @param int $source The hash value of the source URL.
 */
function sunflower_get_event_by_source( $source ) {
	return new WP_Query(
		array(
			'nopaging'   => true,
			'post_type'  => 'sunflower_event',
			'meta_key'   => '_sunflower_event_source',
			'orderby'    => 'meta_value',
			'meta_query' => array(
				array(
					'key'     => '_sunflower_event_source',
					'value'   => $source,
					'compare' => '=',
				),
			),
		)
	);
}

/**
 * Run the import job.
 *
 * @param boolean $force Force the import even if transient time is not past.
 */
function sunflower_import_icals( $force = false ) {

	if ( ! $force && get_transient( 'sunflower_ical_imported' ) ) {
		return false;
	}

	if ( ! sunflower_get_setting( 'sunflower_ical_urls' ) ) {
		return false;
	}

	$import_every_n_hour = sunflower_get_constant( 'SUNFLOWER_EVENT_IMPORT_EVERY_N_HOUR' ) ? sunflower_get_constant( 'SUNFLOWER_EVENT_IMPORT_EVERY_N_HOUR' ) : 3;
	set_transient( 'sunflower_ical_imported', 1, $import_every_n_hour * 3600 );

	$lines = explode( "\n", (string) sunflower_get_setting( 'sunflower_ical_urls' ) );

	$ids_from_remote = array();
	$ids_from_source = array();
	foreach ( $lines as $index => $line ) {
		$info = explode( ';', $line );

		$url             = trim( $info[0] );
		$auto_categories = $info[1] ?? false;

		if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			continue;
		}

		// Get all already imported events of the given source.
		$events_of_source = sunflower_get_event_by_source( md5( $url ) );
		$ids_from_source  = array();

		while ( $events_of_source->have_posts() ) {
			$events_of_source->the_post();
			$ids_from_source[] = get_the_ID();
		}

		$response = sunflower_icalimport( $url, $auto_categories );
		if ( ! empty( $response ) && is_array( $response ) && is_array( $response[0] ) ) {
			$ids_from_remote = array_merge( $ids_from_remote, $response[0] );
		} else {
			$report[ $index ] = array(
				'url'            => $url,
				'error'          => $response,
				'new_events'     => 0,
				'updated_events' => 0,
				'deleted_events' => 0,
			);
			continue;
		}

		$deleted_on_remote = array_diff( sunflower_get_events_having_uid( md5( $url ) ), $ids_from_remote );

		$deleted_events = 0;
		foreach ( $deleted_on_remote as $to_be_deleted ) {
			wp_delete_post( $to_be_deleted );
			++$deleted_events;
		}

		$report[ $index ] = array(
			'url'            => $url,
			'new_events'     => $response['new_events'] ?? 0,
			'updated_events' => $response['updated_events'] ?? 0,
			'deleted_events' => $deleted_events,
		);
	}

	return $report;
}
if ( sunflower_get_setting( 'sunflower_events_enabled' ) ) {

	add_action( 'init', 'sunflower_import_icals' );

}

/**
 * Make georeferencing via nominatim for unknown locations and cache result in database.
 *
 * @param string $location The location as human readable string.
 * @return array | false The lon/lat array or false on failure.
 */
function sunflower_geocode( $location ) {
	static $i  = 0;
	$transient = sprintf( 'sunflower_geocache_%s', sanitize_title( $location ) );

	$cached = get_transient( $transient );
	if ( $cached ) {
		return $cached;
	}

	if ( $i > 3 ) {
		// Download 3 geodata per import.
		return false;
	}

	$url = sprintf( 'https://nominatim.openstreetmap.org/search?q=%s&format=geocodejson', rawurlencode( (string) $location ) );

	$response = wp_remote_get(
		$url,
		array(
			'headers' => array(
				'User-Agent'        => 'SunflowerTheme/' . SUNFLOWER_VERSION . ' (technik@verdigado.com)',
				'Accepted-Language' => 'en-US,en;q=0.9',
			),
			'timeout' => 15,
		)
	);

	if ( is_wp_error( $response ) ) {
		return false;
	}

	$body = wp_remote_retrieve_body( $response );
	$json = json_decode( $body );

	if ( ! $json ) {
		return false;
	}

	$lonlat = isset( $json->features[0] ) ? $json->features[0]->geometry->coordinates : false;

	++$i;

	set_transient( $transient, $lonlat );

	return $lonlat;
}
