<?php
/**
 * ActivityPub Transmogrify for the GatherPress event plugin.
 *
 * Handles converting incoming external ActivityPub events to GatherPress Events.
 *
 * @package Event_Bridge_For_ActivityPub
 * @since 1.0.0
 * @license AGPL-3.0-or-later
 */

namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier;

use Activitypub\Activity\Extended_Object\Event;
use DateTime;
use Exception;
use WP_Error;

use function Activitypub\object_to_uri;
use function Activitypub\sanitize_url;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

use GatherPress\Core\Event as GatherPress_Event;

/**
 * ActivityPub Transmogrifier for the GatherPress event plugin.
 *
 * Handles converting incoming external ActivityPub events to GatherPress Events.
 *
 * @since 1.0.0
 */
class GatherPress {
	/**
	 * The current GatherPress Event object.
	 *
	 * @var Event
	 */
	protected $activitypub_event;

	/**
	 * Extend the constructor, to also set the GatherPress objects.
	 *
	 * This is a special class object form The Events Calendar which
	 * has a lot of useful functions, we make use of our getter functions.
	 *
	 * @param array $activitypub_event The ActivityPub Event as associative array.
	 */
	public function __construct( $activitypub_event ) {
		$activitypub_event = Event::init_from_array( $activitypub_event );

		if ( is_wp_error( $activitypub_event ) ) {
			return;
		}

		$this->activitypub_event = $activitypub_event;
	}

	/**
	 * Validate a time string if it is according to the ActivityPub specification.
	 *
	 * @param string $time_string The time string.
	 * @return bool
	 */
	public static function is_valid_activitypub_time_string( $time_string ) {
		// Try to create a DateTime object from the input string.
		try {
			$date = new DateTime( $time_string );
		} catch ( Exception $e ) {
			// If parsing fails, it's not valid.
			return false;
		}

		// Ensure the timezone is correctly formatted (e.g., 'Z' or a valid offset).
		$timezone           = $date->getTimezone();
		$formatted_timezone = $timezone->getName();

		// Return true only if the time string includes 'Z' or a valid timezone offset.
		$valid = 'Z' === $formatted_timezone || preg_match( '/^[+-]\d{2}:\d{2}$/ ', $formatted_timezone );
		return $valid;
	}

	/**
	 * Get a list of Post IDs of events that have ended.
	 *
	 * @param int $cache_retention_period Additional time buffer in seconds.
	 * @return int[]
	 */
	public static function get_past_events( $cache_retention_period = 0 ) {
		global $wpdb;

		$time_limit = gmdate( 'Y-m-d H:i:s', time() - $cache_retention_period );

		$results = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT post_id FROM {$wpdb->prefix}gatherpress_events WHERE datetime_end < %s",
				$time_limit
			)
		);

		return $results;
	}

	/**
	 * Get post.
	 */
	private function get_post_id_from_activitypub_id() {
		global $wpdb;
		return $wpdb->get_var(
			$wpdb->prepare(
				"SELECT ID FROM $wpdb->posts WHERE guid=%s",
				esc_sql( $this->activitypub_event->get_id() )
			)
		);
	}

	/**
	 * Get the image URL and alt-text of an ActivityPub object.
	 *
	 * @param array $data The ActivityPub object as ann associative array.
	 * @return ?array Array containing the images URL and alt-text.
	 */
	private static function extract_image_alt_and_url( $data ) {
		$image = array(
			'url' => null,
			'alt' => null,
		);

		// Check whether it is already simple.
		if ( ! $data || is_string( $data ) ) {
			$image['url'] = $data;
			return $image;
		}

		if ( ! isset( $data['type'] ) ) {
			return $image;
		}

		if ( ! in_array( $data['type'], array( 'Document', 'Image' ), true ) ) {
			return $image;
		}

		if ( isset( $data['url'] ) ) {
			$image['url'] = $data['url'];
		} elseif ( isset( $data['id'] ) ) {
			$image['id'] = $data['id'];
		}

		if ( isset( $data['name'] ) ) {
			$image['alt'] = $data['name'];
		}

		return $image;
	}

	/**
	 * Returns the URL of the featured image.
	 *
	 * @return array
	 */
	private function get_featured_image() {
		$event = $this->activitypub_event;
		$image = $event->get_image();
		if ( $image ) {
			return self::extract_image_alt_and_url( $image );
		}
		$attachment = $event->get_attachment();
		if ( is_array( $attachment ) && ! empty( $attachment ) ) {
			$supported_types = array( 'Image', 'Document' );
			$match           = null;

			foreach ( $attachment as $item ) {
				if ( in_array( $item['type'], $supported_types, true ) ) {
					$match = $item;
					break;
				}
			}
			$attachment = $match;
		}
		return self::extract_image_alt_and_url( $attachment );
	}

	/**
	 * Given an image URL return an attachment ID. Image will be side-loaded into the media library if it doesn't exist.
	 *
	 * Forked from https://gist.github.com/kingkool68/a66d2df7835a8869625282faa78b489a.
	 *
	 * @param int    $post_id   The post ID where the image will be set as featured image.
	 * @param string $url The image URL to maybe sideload.
	 * @uses media_sideload_image
	 * @return string|int|WP_Error
	 */
	public static function maybe_sideload_image( $post_id, $url = '' ) {
		global $wpdb;

		// Include necessary WordPress file for media handling.
		if ( ! function_exists( 'media_sideload_image' ) ) {
			require_once ABSPATH . 'wp-admin/includes/media.php';
			require_once ABSPATH . 'wp-admin/includes/file.php';
			require_once ABSPATH . 'wp-admin/includes/image.php';
		}

		// Check to see if the URL has already been fetched, if so return the attachment ID.
		$attachment_id = $wpdb->get_var(
			$wpdb->prepare( "SELECT `post_id` FROM {$wpdb->postmeta} WHERE `meta_key` = '_source_url' AND `meta_value` = %s", sanitize_url( $url ) )
		);
		if ( ! empty( $attachment_id ) ) {
			return $attachment_id;
		}

		$attachment_id = $wpdb->get_var(
			$wpdb->prepare( "SELECT `ID` FROM {$wpdb->posts} WHERE guid=%s", $url )
		);
		if ( ! empty( $attachment_id ) ) {
			return $attachment_id;
		}

		// If the URL doesn't exist, sideload it to the media library.
		return media_sideload_image( sanitize_url( $url ), $post_id, sanitize_url( $url ), 'id' );
	}


	/**
	 * Sideload an image_url set it as featured image and add the alt-text.
	 *
	 * @param int    $post_id   The post ID where the image will be set as featured image.
	 * @param string $image_url The image URL.
	 * @param string $alt_text  The alt-text of the image.
	 * @return int The attachment ID
	 */
	private static function set_featured_image_with_alt( $post_id, $image_url, $alt_text = '' ) {
		// Maybe sideload the image or get the Attachment ID of an existing one.
		$image_id = self::maybe_sideload_image( $post_id, $image_url );

		if ( is_wp_error( $image_id ) ) {
			// Handle the error.
			return $image_id;
		}

		// Set the image as the featured image for the post.
		set_post_thumbnail( $post_id, $image_id );

		// Update the alt text.
		if ( ! empty( $alt_text ) ) {
			update_post_meta( $image_id, '_wp_attachment_image_alt', sanitize_text_field( $alt_text ) );
		}

		return $image_id; // Return the attachment ID for further use if needed.
	}

	/**
	 * Add tags to post.
	 *
	 * @param int $post_id The post ID.
	 */
	private function add_tags_to_post( $post_id ) {
		$tags_array = $this->activitypub_event->get_tag();

		// Ensure the input is valid.
		if ( empty( $tags_array ) || ! is_array( $tags_array ) || ! $post_id ) {
			return false;
		}

		// Extract and process tag names.
		$tag_names = array();
		foreach ( $tags_array as $tag ) {
			if ( isset( $tag['name'] ) && 'Hashtag' === $tag['type'] ) {
				$tag_names[] = ltrim( $tag['name'], '#' ); // Remove the '#' from the name.
			}
		}

		// Add the tags as terms to the post.
		if ( ! empty( $tag_names ) ) {
			wp_set_object_terms( $post_id, $tag_names, 'gatherpress_topic', true ); // 'true' appends to existing terms.
		}

		return true;
	}

	/**
	 * Returns the URL of the online event link.
	 *
	 * @return ?string
	 */
	protected function get_online_event_link_from_attachments() {
		$attachments = $this->activitypub_event->get_attachment();

		if ( ! is_array( $attachments ) || empty( $attachments ) ) {
			return;
		}

		foreach ( $attachments as $attachment ) {
			if ( array_key_exists( 'type', $attachment ) && 'Link' === $attachment['type'] && isset( $attachment['href'] ) ) {
				return $attachment['href'];
			}
		}
	}

	/**
	 * Convert a PostalAddress to a string.
	 *
	 * @link https://schema.org/PostalAddress
	 *
	 * @param array $postal_address The PostalAddress as an associative array.
	 * @return string
	 */
	protected static function postal_address_to_string( $postal_address ) {
		if ( ! is_array( $postal_address ) || 'PostalAddress' !== $postal_address['type'] ) {
			_doing_it_wrong(
				__METHOD__,
				'The parameter postal_address must be an associate array like schema.org/PostalAddress.',
				esc_html( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_VERSION )
			);
		}

		$address = array();

		$known_attributes = array(
			'streetAddress',
			'postalCode',
			'addressLocality',
			'addressState',
			'addressCountry',
		);

		foreach ( $known_attributes as $attribute ) {
			if ( isset( $postal_address[ $attribute ] ) && is_string( $postal_address[ $attribute ] ) ) {
				$address[] = $postal_address[ $attribute ];
			}
		}

		$address_string = implode( ' ,', $address );

		return $address_string;
	}

	/**
	 * Convert an address to a string.
	 *
	 * @param mixed $address The address as an object, string or associative array.
	 * @return string
	 */
	protected static function address_to_string( $address ) {
		if ( is_string( $address ) ) {
			return $address;
		}

		if ( is_object( $address ) ) {
			$address = (array) $address;
		}

		if ( ! is_array( $address ) || ! isset( $address['type'] ) ){
			return '';
		}

		if ( 'PostalAddress' === $address['type'] ) {
			return self::postal_address_to_string( $address );
		}
		return '';
	}

	/**
	 * Add venue.
	 *
	 * @param int $post_id The post ID.
	 */
	private function add_venue( $post_id ) {
		$location = $this->activitypub_event->get_location();

		if ( ! $location ) {
			return;
		}

		if ( ! isset( $location['name'] ) ) {
			return;
		}

		// Fallback for Gancio instances.
		if ( 'online' === $location['name'] ) {
			$online_event_link = $this->get_online_event_link_from_attachments();
			if ( ! $online_event_link ) {
				return;
			}
			update_post_meta( $post_id, 'gatherpress_online_event_link', sanitize_url( $online_event_link ) );
			wp_set_object_terms( $post_id, 'online-event', '_gatherpress_venue', false );
			return;
		}

		$venue_instance = \GatherPress\Core\Venue::get_instance();
		$venue_name     = sanitize_title( $location['name'] );
		$venue_slug     = $venue_instance->get_venue_term_slug( $venue_name );
		$venue_post     = $venue_instance->get_venue_post_from_term_slug( $venue_slug );

		if ( ! $venue_post ) {
			$venue_id = wp_insert_post(
				array(
					'post_title'  => sanitize_text_field( $location['name'] ),
					'post_type'   => 'gatherpress_venue',
					'post_status' => 'publish',
				)
			);
		} else {
			$venue_id = $venue_post->ID;
		}

		$venue_information = array();

		$address = $this->address_to_string();

		$venue_information['fullAddress']  = $address;
		$venue_information['phone_number'] = '';
		$venue_information['website']      = '';
		$venue_information['permalink']    = '';

		$venue_json = wp_json_encode( $venue_information );

		update_post_meta( $venue_id, 'gatherpress_venue_information', $venue_json );

		wp_set_object_terms( $post_id, $venue_slug, '_gatherpress_venue', false );
	}

	/**
	 * Save the ActivityPub event object as GatherPress Event.
	 */
	public function save() {
		// Limit this as a safety measure.
		add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) );

		$post_id = $this->get_post_id_from_activitypub_id();

		$args = array(
			'post_title'   => sanitize_text_field( $this->activitypub_event->get_name() ),
			'post_type'    => 'gatherpress_event',
			'post_content' => wp_kses_post( $this->activitypub_event->get_content() ) . '<!-- wp:gatherpress/venue /-->',
			'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ),
			'post_status'  => 'publish',
			'guid'         => sanitize_url( $this->activitypub_event->get_id() ),
			'meta_input'   => array(
				'event_bridge_for_activitypub_is_cached' => 'GatherPress',
				'activitypub_content_visibility'         => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL,
			),
		);

		if ( $post_id ) {
			$args['ID'] = $post_id;
		}

		// Insert new GatherPress Event post.
		$post_id = wp_update_post( $args );

		if ( ! $post_id || is_wp_error( $post_id ) ) {
			return;
		}

		// Insert the dates.
		$event      = new GatherPress_Event( $post_id );
		$start_time = $this->activitypub_event->get_start_time();
		$end_time   = $this->activitypub_event->get_end_time();
		if ( ! $end_time ) {
			$end_time = new DateTime( $start_time );
			$end_time->modify( '+1 hour' );
			$end_time = $end_time->format( 'Y-m-d H:i:s' );
		}
		$params = array(
			'datetime_start' => $start_time,
			'datetime_end'   => $end_time,
			'timezone'       => $this->activitypub_event->get_timezone(),
		);
		// Sanitization of the params is done in the save_datetimes function just in time.
		$event->save_datetimes( $params );

		// Insert featured image.
		$image = $this->get_featured_image();
		self::set_featured_image_with_alt( $post_id, $image['url'], $image['alt'] );

		// Add hashtags.
		$this->add_tags_to_post( $post_id );

		$this->add_venue( $post_id );

		// Limit this as a safety measure.
		remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) );
	}

	/**
	 * Save the ActivityPub event object as GatherPress event.
	 */
	public function delete() {
		$post_id = $this->get_post_id_from_activitypub_id();

		if ( ! $post_id ) {
			return new WP_Error(
				'event_bridge_for_activitypub_remote_event_not_found',
				\__( 'Remote event not found in cache', 'event-bridge-for-activitypub' ),
				array( 'status' => 404 )
			);
		}

		$thumbnail_id = get_post_thumbnail_id( $post_id );

		if ( $thumbnail_id ) {
			wp_delete_attachment( $thumbnail_id, true );
		}

		wp_delete_post( $post_id, true );
	}

	/**
	 * Return the number of revisions to keep.
	 *
	 * @return     int   The number of revisions to keep.
	 */
	public static function revisions_to_keep() {
		return 5;
	}
}