<?php

namespace EFFE_Plugin;

/**
 * Class for handling calls to the Eventbrite API.
 *
 * @since      1.0.0
 * @package    Event_Feed_For_Eventbrite
 * @subpackage Event_Feed_For_Eventbrite/includes
 * @author     Bohemia Plugins <https://www.bohemiaplugins.com>
 * 			   Fullworks <https://fullworks.net/>
 * 			   Automattic <https://automattic.com>
 */
class Event_Feed_For_Eventbrite_Api {

	/**
	 * The base URL for the Eventbrite API.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string    $api_base    The base URL for the Eventbrite API.
	 */
	const API_BASE = 'https://www.eventbriteapi.com/v3/';

	/**
	 * Class instance used by themes and plugins.
	 * 
	 * @since    1.0.0
	 * @access   public
	 * @var      object
	 */
	public static $instance;

	/**
	 * The ID of this plugin.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string    $plugin_name    The ID of this plugin.
	 */
	private $plugin_name;

	/**
	 * The options for the Event Feed for Eventbrite plugin.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var 	 array    $event_feed_for_eventbrite_options    The options for the plugin.
	 */
	private $event_feed_for_eventbrite_options;

	/**
	 * The Eventbrite API token.
	 *
	 * @since    1.0.0
	 * @access   private
	 * @var      string    $token    The Eventbrite API token.
	 */
	private $token;

	/**
	 * The cache duration for transients.
	 * 
	 * @since    1.0.0
	 * @access   private
	 * @var      integer    $cache_duration    The cache duration for transients.
	 */
	private $cache_duration;

	/**
	 * Initialize the class and set its properties.
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    string     $plugin_name     The name of the plugin.
	 * @param    string     $version         The version of this plugin.
	 */
	public function __construct() {

		// Assign our instance.
		self::$instance = $this;

		$this->plugin_name = 'event-feed-for-eventbrite';
		$this->event_feed_for_eventbrite_options = get_option( $this->plugin_name );
		$this->token = isset( $this->event_feed_for_eventbrite_options[ 'api_key' ] ) ? $this->event_feed_for_eventbrite_options[ 'api_key' ] : '';
		$this->cache_duration = isset( $this->event_feed_for_eventbrite_options[ 'cache_duration' ] )  ? $this->event_feed_for_eventbrite_options[ 'cache_duration' ] : 86400;

	}

	/**
	 * Add a transient name to the list of registered transients, stored in the
	 * 'event-feed-for-eventbrite-transients' option.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @param    string      $transient_name     The transient name/key used to store the transient.
	 */
	protected function register_transient( $transient_name ) {

		// Get any existing list of transients.
		$transients = get_option( $this->plugin_name . '-transients', array() );

		// Add the new transient if it doesn't already exist.
		if ( ! in_array( $transient_name, $transients ) ) {
			$transients[] = $transient_name;
		}

		// Save the updated list of transients.
		update_option( $this->plugin_name . '-transients', $transients );

	}

	/**
	 * Flush all transients.
	 * 
	 * @since    1.0.0
	 * @access   public
	 */
	public function flush_transients() {

		// Get the list of registered transients.
		$transients = get_option( $this->plugin_name . '-transients', array() );

		// Bail if we have no transients.
		if ( ! $transients ) {
			return;
		}

		// Loop through all registered transients, deleting each one.
		foreach ( $transients as $transient ) {
			delete_transient( $transient );
		}

		// Reset the list of registered transients.
		delete_option( $this->plugin_name .  '-transients' );

	}

	/**
	 * Increase the timeout for Eventbrite API calls from the default 5 seconds to 30.
	 *
	 * @since    1.0.0
	 * @access   public
	 */
	public function increase_timeout() {

		return 30;
		
	}

	/**
	 * Get the transient for a certain endpoint and combination of parameters.
	 * get_transient() returns false if not found.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @param    string      $endpoint     Endpoint being called.
	 * @param    array       $params       Parameters to be passed during the API call.
	 * @return   mixed                     Transient if found, false if not.
	 */
	protected function get_cache( $endpoint, $params, $id ) {

		return get_transient( $this->get_transient_name( $endpoint, $params, $id ) );

	}

	/**
	 * Determine a transient's name based on endpoint and parameters.
	 *
	 * @since    1.0.0
	 * @access   protected
	 * @param    string      $endpoint     Endpoint being called.
	 * @param    array       $params       Parameters to be passed during the API call.
	 * @return   string
	 */
	protected function get_transient_name( $endpoint, $params, $id) {

		// Results in 62 characters for the timeout option name (maximum is 64).
		$transient_name = 'effe_' . md5( $endpoint . implode( $params ) . $id );

		return $transient_name;

	}

	/**
	 * API request using GET method
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    string    $url                    API endpoint to be called.
	 * @param    array     $query_params           Parameters to be passed with the API call.
	 * @return   object    Event_Feed_For_Eventbrite_Api
	 */
	private function request_api( $url, array $query_params = array() ) {

		$params = array( 'method' => 'GET' );

		if ( ! isset( $query_params['token'] ) || empty ( $query_params['token'] ) ) {
			$params['headers']['Authorization'] = 'Bearer' . ' ' . (string) $this->token;
		} else {
			$params['headers']['Authorization'] = 'Bearer' . ' ' . (string) $query_params['token'];
		}

		$res = wp_remote_get( $url, $params );

		if ( in_array( wp_remote_retrieve_response_code( $res ), array( 200, 201, 202 ) ) ) {
			return json_decode( wp_remote_retrieve_body( $res ) );
		} else {
			return new \WP_Error( EFFE_PLUGIN_NAME . '-api-request-error', $res );
		}
		
	}
	
	/**
	 * Define all types of calls to the Eventbrite API
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    string     $endpoint       API endpoint supported by the plugin.
	 * @param    array      $query_params   Parameters to be passed with the API call.
	 * @param    integer    $object_id      Eventbrite event ID used when requesting a single event from the API.
	 * @return   object                     API response if successful, error otherwise.
	 */
	public function call( $endpoint, $query_params = array(), $object_id = null ) {

		$endpoint_map = array(
			'events' 			=> 'organizations/' . $object_id . '/events',
			'organizations'     => 'users/me/organizations',
			'media'       		=> 'media/' . $object_id,
			'organizers'       	=> 'organizations/' . $object_id . '/organizers',
		);
		
		$endpoint_base = trailingslashit( self::API_BASE . $endpoint_map[ $endpoint ] );
		
		if ( $endpoint == 'events' ) {

			// set default to live events (tickets can be obtained for the event)
			if ( ! isset( $query_params['status'] ) ) {
				$query_params['status'] = 'live';
			}

			// set default to current future events (excluding past events)
			if ( ! isset( $query_params['time_filter'] ) ) {
				$query_params['time_filter'] = 'current_future';
			}

			$expansions = [
				'logo',
				'venue',
				'organizer',
				'format',
				'category',
				'subcategory',
				//'bookmark_info',
				'refund_policy',
				'ticket_availability',
				'external_ticketing',
				'music_properties',
				//'publish_settings',
				// 'basic_inventory_info',
				'event_sales_status',
				//'checkout_settings',
				//'listing_properties',
				//'has_digital_content',
				'ticket_classes',
			];
			$query_params['expand'] = implode( ',', $expansions );

			$endpoint_url = add_query_arg( $query_params, $endpoint_base );

		} elseif ( $endpoint == 'organizations' ) {

			$url          = explode( '?', esc_url_raw( $endpoint_base ) );
			$endpoint_url = $url[0];

		} elseif ( $endpoint == 'media' || $endpoint == 'organizers' ) {

			$endpoint_url = add_query_arg( $query_params, $endpoint_base );

		}
		
		$response = $this->request_api( $endpoint_url, $query_params );

		if ( ! is_wp_error( $response ) ) {

			if ( isset( $response->pagination->has_more_items ) ) {

				while ( $response->pagination->has_more_items ) {
					$next_response        = $this->request_api( $endpoint_url . '&continuation=' . $response->pagination->continuation, array() );
					$response->events     = array_merge( $response->events, $next_response->events );
					$response->pagination = $next_response->pagination;
				}
			}

			// Encode event images to base64
			if ( $endpoint == 'media' ) {

				$image_response = wp_remote_get($response->url, array(
					'timeout' => 30,
					'sslverify' => false
				));

				if (!is_wp_error($image_response) && wp_remote_retrieve_response_code($image_response) === 200) {
					$img = wp_remote_retrieve_body($image_response);
					if (!empty($img)) {
						$encoded_img = 'data:image/jpg;base64,' . base64_encode($img);
					} else if (!empty($response) && !empty($response->url)) {
						$encoded_img = $response->url;
					} else if (file_exists(__DIR__ . '/../public/assets/img/placeholder.png')) {
						$encoded_img = plugin_dir_url(__FILE__) . '../public/assets/img/placeholder.png';
					} else {
						$encoded_img = 'Could not load image';
					}
				} else {
					// Fallback to original response if image fetch fails
					$encoded_img = $response;
				}

				$response = $encoded_img;

			}

		}

		return $response;

	}

	/**
	 * Make a call to the Eventbrite API or return an existing transient.
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    string              $endpoint     Valid Eventbrite v3 API endpoint.
	 * @param    array               $params       Parameters passed to the API during a call.
	 * @param    int|string|bool     $id           A specific event ID used for calls to the event_details endpoint.
	 * @param    bool                $force        Force a fresh API call, ignoring any existing transient.
	 * @return   object                            Request results.
	 */
	public function request( $endpoint, $params = array(), $id = false, $force = false ) {

		// Return a cached result if we have one.
		if ( ! $force ) {
			$cached = $this->get_cache( $endpoint, $params, $id );
			if ( ! empty( $cached ) ) {
				return $cached;
			} else {
				if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
					error_log( print_r( array(
						'msg'   => 'Eventbite Events Debug:  No Cache so will try refresh',
						'call'  => array(
							'endpoint' => $endpoint,
							'params'   => $params,
						),
					), true ) );
				}
			}
		}

		// Extend the HTTP timeout to account for Eventbrite API calls taking longer than ~5 seconds.
		add_filter( 'http_request_timeout', array( $this, 'increase_timeout' ) );

		// Make a fresh request.
		if ( isset( $params['token'] ) && ! empty( $params['token'] ) ) {

			$request = $this->call( $endpoint, $params, $id );

		} elseif( isset( $this->token ) && ! empty( $this->token ) ) {

			$params['token'] = $this->token;
			$request = $this->call( $endpoint, $params, $id );

		} else {

			$request = new \WP_Error( 'noconnection', esc_html__( 'No connection available for Eventbrite', 'event-feed-for-eventbrite' ) );

		}

		// Remove the timeout extension for any non-Eventbrite calls.
		remove_filter( 'http_request_timeout', array( $this, 'increase_timeout' ) );

		// If we get back a proper response, cache it.
		if ( ! is_wp_error( $request ) ) {

			$transient_name = $this->get_transient_name( $endpoint, $params, $id);
			set_transient( $transient_name, $request, $this->cache_duration );
			$this->register_transient( $transient_name );

		} else {

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG === true ) {
				$debug = array(
					'msg'   => 'Eventbite Events Debug: Call issue',
					'call'  => array(
						'endpoint' => $endpoint,
						'params'   => $params,
						'id'       => $id,
					)
				);
				if ( isset( $request->errors ) ) {
					$debug['error'] = $request->errors;
				}
				error_log( print_r( $debug, true ) );
			}

		}

		return $request;
	}

	/**
	 * Get event's original image in desired resolution
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    int|string|bool	 $id  			A specific media ID used for call to the media endpoint.
	 * @param    array               $params        Parameters passed to the API during a call.
	 * @param    bool                $force        	Force a fresh API call, ignoring any existing transient.
	 * @return   array      						Eventbrite API media data.
	 */
	public function get_event_original_image( $id = null, $params = array(), $force = false ) {

		$api_result = $this->request( 'media', $params, $id, $force );
		if ( is_wp_error( $api_result ) ) {
			return $api_result;
		}

		return $api_result;

	}

	/**
	 * Get user's organizations before loading feed edit page
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    array               $params        Parameters passed to the API during a call.
	 * @param    bool                $force        	Force a fresh API call, ignoring any existing transient.
	 * @return   array      					    Eventbrite API organizations data.
	 */
	public function get_user_organizations( $params = array(), $force = false ) {

		$api_result = $this->request( 'organizations', $params, false, $force );
		if ( is_wp_error( $api_result ) ) {
			return $api_result;
		}

		// If we have organizations, map them to the specified format
		if ( ! empty( $api_result->organizations ) ) {
			$organizations = array_map( array( $this, 'map_organization_keys' ), $api_result->organizations );
		}
		
		return $organizations;

	}

	/**
	 * Convert the Eventbrite API properties into properties used by our organization object.
	 * 
	 * @since    1.0.0
	 * @access   protected     
	 * @param    object        $api_organization     A single organization from the API results.
	 * @return   object                              Organization with specified keys.
	 */
	protected function map_organization_keys( $api_organization ) {

		$organization = array();

		$organization['id']   = ( isset( $api_organization->id ) ) ? $api_organization->id : '';
		$organization['name'] = ( isset( $api_organization->name ) ) ? $api_organization->name : '';

		return (object) $organization;

	}

	/**
	 * Get user's organizers before loading feed edit page
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    array               $params        Parameters passed to the API during a call.
	 * @param    bool                $force        	Force a fresh API call, ignoring any existing transient.
	 * @return   array      					    Eventbrite API organizers data.
	 */
	public function get_user_organizers( $params = array(), $force = false ) {
		
		$api_result = $this->request( 'organizers', $params, false, $force );
		if ( is_wp_error( $api_result ) ) {
			return $api_result;
		}

		// If we have organizers, map them to the specified format
		if ( ! empty( $api_result->organizers ) ) {
			$organizers = array_map( array( $this, 'map_organizer_keys' ), $api_result->organizers );
		}
		
		return $organizers;

	}

	/**
	 * Convert the Eventbrite API properties into properties used by our organizer object.
	 * 
	 * @since    1.0.0
	 * @access   protected     
	 * @param    object        $api_organizer     A single organizer from the API results.
	 * @return   object                           Organizer with specified keys.
	 */
	protected function map_organizer_keys( $api_organizer ) {
		
		$organizer = array();

		$organizer['id']   = ( isset( $api_organizer->id ) ) ? $api_organizer->id : '';
		$organizer['name'] = ( isset( $api_organizer->name ) ) ? $api_organizer->name : '';

		return (object) $organizer;

	}

	/**
	 * Get user-owned private and public events.
	 *
	 * @since    1.0.0
	 * @access   public
	 * @param    array               $params        Parameters passed to the API during a call.
	 * @param    bool                $force        	Force a fresh API call, ignoring any existing transient.
	 * @return   object                 			Eventbrite API events data.
	 */
	public function get_organizations_events( $params = array(), $force = false ) {

		if ( isset( $params['organization_id'] ) ) {

			$organizations = (object) array(
				'organizations' => array(
					(object) array(
						'id' => $params['organization_id']
					)
				)
			);
			unset( $params['organization_id'] );

		} else {

			$organizations = $this->request( 'organizations', $params, false, $force );
			if ( is_wp_error( $organizations ) ) {
				return $organizations;
			}

		}

		$merged_results = (object) array( 'events' => array() );

		foreach ( $organizations->organizations as $organization ) {

			$org_id = $organization->id;

			// Get the raw results.
			$results = $this->request( 'events', $params, $org_id, $force );
			if ( is_wp_error( $results ) ) {
				return $results;
			}

			// If we have events, map them to the specified format
			if ( ! empty( $results->events ) ) {
				$results->events = array_map( array( $this, 'map_event_keys' ), $results->events );
			}

			$merged_results->events = array_merge( $merged_results->events, $results->events );

		}

		return $merged_results;
	}

	/**
	 * Convert the Eventbrite API properties into properties used by our event object.
	 * 
	 * @since    1.0.0
	 * @access   protected     
	 * @param    object        $api_event     A single event from the API results.
	 * @return   object                       Event with specified keys.
	 */
	protected function map_event_keys( $api_event ) {

		$event = array();

		$event['ID']                     = ( isset( $api_event->id ) ) ? $api_event->id : '';
		$event['post_title']             = ( isset( $api_event->name->text ) ) ? $api_event->name->text : '';
		$event['post_content']           = ( isset( $api_event->summary ) ) ? $api_event->summary : '';
		$event['post_date']              = ( isset( $api_event->start->local ) ) ? $api_event->start->local : '';
		$event['created']                = ( isset( $api_event->created ) ) ? $api_event->created : '';
		$event['post_date_gmt']          = ( isset( $api_event->start->utc ) ) ? $api_event->start->utc : '';
		$event['logo_url']               = ( isset( $api_event->logo->url ) ) ? $api_event->logo->url : '';
		$event['logo']                   = ( isset( $api_event->logo ) ) ? $api_event->logo : '';
		$event['start']                  = ( isset( $api_event->start ) ) ? $api_event->start : '';
		$event['end']                    = ( isset( $api_event->end ) ) ? $api_event->end : '';
		$event['organizer']              = ( isset( $api_event->organizer ) ) ? $api_event->organizer : '';
		$event['venue']                  = ( isset( $api_event->venue ) ) ? $api_event->venue : '';
		$event['public']                 = ( isset( $api_event->listed ) ) ? $api_event->listed : '';
		$event['tickets']                = ( isset( $api_event->ticket_classes ) ) ? $api_event->ticket_classes : '';
		$event['category']               = ( isset( $api_event->category ) ) ? $api_event->category : '';
		$event['subcategory']            = ( isset( $api_event->subcategory ) ) ? $api_event->subcategory : '';
		$event['format']                 = ( isset( $api_event->format ) ) ? $api_event->format : '';
		$event['series_id']              = ( isset( $api_event->series_id ) ) ? $api_event->series_id : '';
		$event['is_series']              = ( isset( $api_event->is_series ) ) ? $api_event->is_series : '';
		$event['is_free']                = ( isset( $api_event->is_free ) ) ? $api_event->is_free : '';
		$event['is_series_parent']       = ( isset( $api_event->is_series_parent ) ) ? $api_event->is_series_parent : '';
		$event['is_externally_ticketed'] = ( isset( $api_event->is_externally_ticketed ) ) ? $api_event->is_externally_ticketed : false;
		$event['status']                 = ( isset( $api_event->status ) ) ? $api_event->status : '';
		$event['online_event']           = ( isset( $api_event->online_event ) ) ? $api_event->online_event : false;
		$event['ticket_availability']    = ( isset( $api_event->ticket_availability ) ) ? $api_event->ticket_availability : '';
		$event['capacity']               = ( isset( $api_event->capacity ) ) ? $api_event->capacity : 9999999;
		$event['event_sales_status']     = ( isset( $api_event->event_sales_status ) ) ? $api_event->event_sales_status : false;
		$event['url']                    = ( isset( $api_event->url ) ) ? $api_event->url : '';
		$event['vue']                    = '';

		if ( isset( $api_event->music_properties ) ) {
			$event['music_properties'] = $api_event->music_properties;
		}
		
		if ( isset( $api_event->is_externally_ticketed ) && $api_event->is_externally_ticketed ) {
			$event['external_ticketing'] = $api_event->external_ticketing;
		}

		// Remaining tickets - added in 1.1.0
		if( isset( $api_event->ticket_classes ) ) {
			$all_quantity_total = 0;
			$all_quantity_sold = 0;
			foreach( $api_event->ticket_classes as $ticket_class ) {
				if( $ticket_class->on_sale_status == 'AVAILABLE' && isset( $ticket_class->quantity_total ) && isset( $ticket_class->quantity_sold ) ) {
					$all_quantity_total += (int) $ticket_class->quantity_total;
					$all_quantity_sold += (int) $ticket_class->quantity_sold;
				}
			}
			$tickets_remaining = $all_quantity_total - $all_quantity_sold;
			if( is_int( $tickets_remaining ) && $tickets_remaining > 0 ) {
				$event['tickets_remaining'] = $tickets_remaining;
			}
		}

		return (object) $event;
	}

}

new Event_Feed_For_Eventbrite_Api;

/**
 * Allow themes and plugins a simple function to access Event_Feed_For_Eventbrite_Api methods and properties.
 *
 * @since    1.0.0
 * @access   public
 * @return   object     Event_Feed_For_Eventbrite_Api
 */
function eventbrite() {

	return Event_Feed_For_Eventbrite_Api::$instance;
	
}