<?php

class Tribe__Events__Aggregator__CLI__Command {
	/**
	 * @var int The polling interval timeout in seconds.
	 */
	protected $polling_timeout = 30;

	/**
	 * @var int The polling interval in seconds.
	 */
	protected $polling_interval = 2;

	/**
	 * Run an import of the specified type from a specified source.
	 *
	 * The command will use the API and licenses set for the site if required.
	 *
	 * <origin>
	 * : the import origin type
	 * ---
	 * options:
	 *   - ical
	 *   - gcal
	 *   - csv
	 *   - ics
	 *   - facebook
	 *   - meetup
	 *   - url
	 * ---
	 *
	 * <source>
	 * : The source to import events from; a URL or a file path for .ics and CSV files.
	 *
	 * [--keywords=<keywords>]
	 * : Optionally filter events by these keywords.
	 *
	 * [--location=<location>]
	 * : Filter events by this location, not supported by all origin types.
	 *
	 * [--radius=<radius>]
	 * : Only fetch events in this mile radius around the location.
	 * Will be ignored if the `--location` parameter is not set.
	 *
	 * [--start=<start>]
	 * : Only fetch events starting after this date.
	 * This should be a valid date string or a value supported by the `strtotime` PHP function.
	 * Not supported by all origin types.
	 *
	 * [--end=<end>]
	 * : Only fetch events starting before this date.
	 * This should be a valid date string or a value supported by the `strtotime` PHP function.
	 * When using natural language expressions keep in mind that those apply from the current time, not start.
	 * Not supported by all origin types.
	 * Defaults the range set in the import settings for this origin type.
	 *
	 * [--limit_type=<limit_type>]
	 * : The type of limit that should be used to limit the number of fetched events.
	 * ---
	 * options:
	 *   - count
	 *   - range
	 *   - no_limit
	 * ---
	 *
	 * [--limit=<limit>]
	 * : The value of the limit that should be applied; ignored if `--limit_type` is not set or set to `no_limit`.
	 * Either a value in seconds if the `--limit_type` is range or a number if `--limit_type` is set to `count`.
	 * When importing CSV files this limit will NOT apply.
	 *
	 * [--timeout=<timeout>]
	 * : How long should the command wait for the data from EA Service in seconds
	 * ---
	 * default: 30
	 * ---
	 *
	 * [--post_status=<post_status>]
	 * : The post status that should be assigned to the imported events; default to the one set in Import options.
	 * ---
	 * options:
	 *   - publish
	 *   - draft
	 *   - pending
	 *   - private
	 * ---
	 *
	 * [--category=<category>]
	 * : An optional category that should be assigned to the imported events.
	 *
	 * [--content_type=<content_type>]
	 * : The type of import for CSV files.
	 * The column mapping must be defined with the `--column_map` parameter.
	 * ---
	 * default: events
	 * options:
	 *   - events
	 *   - venues
	 *   - organizers
	 * ---
	 *
	 * [--column_map=<column_map>]
	 * : the column mapping that should be used for CSV imports; required when running CSV imports. A comma separated
	 * list where the order counts.
	 * For events the available columns are: name, description, excerpt, start_date, start_time, end_date, end_time,
	 * timezone, all_day, hide, sticky, venue_name, organizer_name, show_map_link, show_map, cost, currency_symbol,
	 * currency_position, category, tags, website, comment_status, ping_status, featured_image, feature_event
	 * For venues the available columns are: name, description, country, address, address2, city, state, zip, phone,
	 * url, featured_image
	 * For organizers the available columns are: name, description, email, website, phone, featured_image
	 *
	 * [--format=<format>]
	 * : The results output format
	 * ---
	 *
	 * ## Examples
	 *
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --start=tomorrow --end="+3 weeks"
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --limit_type=count --limit=20
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --location="Toronto" --radius=50
	 *      wp event-aggregator import-from ical https://some-ical-source/feed.ics --keywords=Party
	 *      wp event-aggregator import-from meetup https://www.meetup.com/wordpress-ile-de-france/
	 *      wp event-aggregator import-from gcal https://calendar.google.com/calendar/ical/me/public/basic.ics
	 *      wp event-aggregator import-from csv /Users/moi/events.csv --content_type=events --column_map=name,description,start_date,start_time,end_date,end_time
	 *      wp event-aggregator import-from ics /Users/moi/events.ics
	 *
	 *
	 * @since      4.6.15
	 *
	 * @subcommand import-from
	 *
	 * @when       after_wp_load
	 */
	public function import_from_source( array $args, array $assoc_args = [] ) {
		$this->ensure_timeout( $assoc_args );

		list( $origin, $source ) = $args;

		$is_csv = 'csv' === $origin;

		if ( $is_csv ) {
			$this->ensure_column_map( $assoc_args );
		}

		$record = $this->create_record_from( $assoc_args, $origin, $source );

		$this->fetch_and_process( $assoc_args, $record, $is_csv );
	}

	/**
	 * Check the timeout parameter if set.
	 *
	 * @since 4.6.15
	 */
	protected function ensure_timeout( array $assoc_args ) {
		if ( isset( $assoc_args['timeout'] ) && ! is_numeric( $assoc_args['timeout'] ) ) {
			WP_CLI::error( 'The timeout should be a numeric value.' );
		}
	}

	/**
	 * Creates a new record.
	 *
	 * @since 4.6.15
	 *
	 * @param array $assoc_args
	 * @param string $origin
	 * @param string $source
	 *
	 * @return Tribe__Events__Aggregator__Record__Abstract
	 */
	protected function create_record_from( array $assoc_args, $origin, $source ) {
		$is_csv = 'csv' === $origin;

		$types = [
			'ical'   => 'Tribe__Events__Aggregator__Record__iCal',
			'gcal'   => 'Tribe__Events__Aggregator__Record__gCal',
			'csv'    => 'Tribe__Events__Aggregator__Record__CSV',
			'ics'    => 'Tribe__Events__Aggregator__Record__ICS',
			'meetup' => 'Tribe__Events__Aggregator__Record__Meetup',
			'url'    => 'Tribe__Events__Aggregator__Record__Url',
		];

		$record_class = Tribe__Utils__Array::get( $types, $origin, reset( $types ) );

		/** @var Tribe__Events__Aggregator__Record__Abstract $record */
		$record = new $record_class;

		$record_args = [];

		$location   = Tribe__Utils__Array::get( $assoc_args, 'location' );
		$limit_type = Tribe__Utils__Array::get( $assoc_args, 'limit_type' );
		$limit      = Tribe__Utils__Array::get( $assoc_args, 'limit' );

		if ( isset( $assoc_args['start'], $assoc_args['end'] ) ) {
			$limit_type = 'no_limit';
			$limit      = 'not_set';
		}

		$category    = Tribe__Utils__Array::get( $assoc_args, 'category', '' );
		$category_id = '';

		if ( is_numeric( $category ) ) {
			$category_id = $category;
		} elseif ( ! empty( $category ) ) {
			$term = get_term_by( 'slug', $category, Tribe__Events__Main::TAXONOMY );
			if ( ! $term instanceof WP_Term ) {
				WP_CLI::error( "$category is not a valid category ID or slug." );
			}
			$category_id = $term->term_id;
		}

		$record_meta = [
			'origin'     => $origin,
			'type'       => 'manual',
			'keywords'   => Tribe__Utils__Array::get( $assoc_args, 'keywords', '' ),
			'location'   => $location,
			'start'      => Tribe__Utils__Array::get( $assoc_args, 'start' ),
			'end'        => Tribe__Utils__Array::get( $assoc_args, 'end' ),
			'radius'     => $location ? Tribe__Utils__Array::get( $assoc_args, 'radius' ) : null,
			'limit_type' => $limit_type,
			'limit'      => $limit,
			'source'     => $source,
			'preview'    => false,
			'category'   => $category_id,
		];

		if ( ! empty( $record_meta['start'] ) ) {
			$record_meta['start'] = Tribe__Date_Utils::reformat( $record_meta['start'], 'Y-m-d H:i:s' );
			if ( empty( $record_meta['start'] ) ) {
				WP_CLI::error( 'The --start parameter could not be parsed; review the argument description.' );
			}
		}

		if ( ! empty( $record_meta['end'] ) ) {
			$record_meta['end'] = Tribe__Date_Utils::reformat( $record_meta['end'], 'Y-m-d H:i:s' );
			if ( empty( $record_meta['end'] ) ) {
				WP_CLI::error( 'The --end parameter could not be parsed; review the argument description.' );
			}
		}

		if ( isset( $record_meta['start'], $record_meta['end'] ) ) {
			if ( strtotime( $record_meta['end'] ) < strtotime( $record_meta['start'] ) ) {
				WP_CLI::error( "End date [{$record_meta['end']}] cannot be before start date [{$record_meta['start']}]; review the argument description." );
			}
		}

		if ( $is_csv ) {
			$record_meta['file']         = $source;
			$record_meta['content_type'] = Tribe__Utils__Array::get( $assoc_args, 'content_type', 'events' );
		}

		if ( 'ics' === $origin ) {
			$record_meta['file'] = [
				'name'     => $source,
				'tmp_name' => $source,
			];
		}

		if ( empty( $assoc_args['post_status'] ) ) {
			$record_meta['post_status'] = tribe( 'events-aggregator.settings' )->default_post_status( $origin );
		} else {
			$record_meta['post_status'] = $assoc_args['post_status'];
		}

		WP_CLI::log( 'Creating record post...' );

		$created = $record->create( 'manual', $record_args, $record_meta );

		if ( $created instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while creating the import: ' . $created->get_error_message() );
		}

		WP_CLI::log( "Record created with post ID {$record->id}." );

		return $record;
	}

	/**
	 * Fetches the data from the Service and processes it.
	 *
	 * @since 4.6.15
	 *
	 * @param array $assoc_args
	 * @param Tribe__Events__Aggregator__Record__Abstract $record
	 * @param bool $is_csv
	 *
	 * @return array
	 */
	protected function fetch_and_process( array $assoc_args, $record, $is_csv ) {
		$queue_import_args = [];

		// remove anything that cannot be serialized
		foreach ( $record->meta as $key => $value ) {
			if ( is_array( $value ) || is_scalar( $value ) ) {
				$queue_import_args[ $key ] = $value;
			}
		}

		$queue_result = $record->queue_import( $queue_import_args );

		$record->finalize();

		$this->polling_timeout = (float) $assoc_args['timeout'];

		if ( $is_csv ) {
			$column_map = $assoc_args['column_map'];
			$activity   = $this->import_csv_file( $queue_result, $record, $column_map );
		} else {
			$activity = $this->import_from_service( $queue_result, $record );
		}

		if ( ! $activity instanceof Tribe__Events__Aggregator__Record__Activity ) {
			if ( $activity instanceof WP_Error ) {
				WP_CLI::error( $activity->get_error_message() );
			} else {
				WP_CLI::error( 'Something went wrong during the import process.' );
			}
			$record->set_status_as_failed();
		}

		$assoc_args['format'] = ! empty( $assoc_args['format'] ) ? $assoc_args['format'] : 'yaml';

		$items = $activity->get();

		// just a "cosmetic" refinement to make sure integers will be rendered as integers
		foreach ( $items as $type ) {
			foreach ( $type as &$action ) {
				$action = array_map( function ( $entry ) {
					return is_numeric( $entry ) ? (int) $entry : $entry;
				}, $action );
			}
		}

		WP_CLI::print_value( $items, $assoc_args );

		WP_CLI::success( 'Import done!' );

		$record->update_meta( 'activity', $activity );
		$record->delete_meta( 'queue' );
		$record->set_status_as_success();

		return $action;
	}

	/**
	 * Imports a CSV file.
	 *
	 * The logic to handle and import CSV files is different, primarily in it not relying on the Service, from
	 * other imports. Mind that CSV source files should have their columns in exactly the same order and named
	 * exactly as those found in the UI.
	 *
	 * @since 4.6.15
	 *
	 * @param Tribe__Events__Aggregator__Record__CSV $record
	 * @param array $record_meta
	 * @param string|array $column_map The column map that should be used for the import, either a comma-separated list
	 *                                 or an array.
	 *
	 * @return Tribe__Events__Aggregator__Record__Activity
	 */
	protected function import_csv_file( $queue_result, $record, $column_map ) {
		WP_CLI::log( 'Reading the file...' );

		if ( ! is_array( $column_map ) ) {
			$column_map = preg_split( '/\\s*,\\s*/', $column_map );
		}

		if ( empty( $column_map ) ) {
			WP_CLI::error( 'The provided column map is invalid.' );
		}

		$map    = [
			'events'     => 'event',
			'venues'     => 'venue',
			'organizers' => 'organizer',
		];
		$prefix = Tribe__Utils__Array::get( $map, $record->meta['content_type'], 'event' );

		$column_map = array_map( function ( $key ) use ( $prefix ) {
			return $key === 'featured_image' || $key === 'feature_event' ? $key : $prefix . '_' . $key;
		}, $column_map );

		$data = [
			'action'        => 'new',
			'import_id'     => $record->id,
			'origin'        => 'csv',
			'csv'           =>
				[
					'content_type' => 'tribe_' . $record->meta['content_type'],
					'file'         => $record->meta['file'],
				],
			'column_map'    => $column_map,
			'post_status'   => $record->meta['post_status'],
			'category'      => $record->meta['category'],
			'selected_rows' => 'all',
		];

		$response = $queue_result;

		if ( $response instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while reading the file: ' . $response->get_error_message() );
		}

		if ( isset( $response['data']['items'] ) && $response['data']['items'] instanceof WP_Error ) {
			WP_CLI::error( $response['data']['items']->get_error_message() );
		}

		$item_name = rtrim( $record->meta['content_type'], 's' );
		$items     = $response['data']['items'];

		if ( empty( $items ) ) {
			WP_CLI::success( 'No items found matching the query.' );
		}

		$items_count = count( $items );

		WP_CLI::log( "Read data for {$items_count} {$item_name}(s) from the file." );

		add_filter( 'tribe_aggregator_batch_size', function () use ( $items_count ) {
			return $items_count + 1;
		} );

		/** @var Tribe__Events__Aggregator__Record__Queue_Interface $result */
		$result = $record->process_posts( $data );

		if ( $result instanceof WP_Error ) {
			WP_CLI::error( $result->get_error_message() );
		}

		$activity = $result->activity();

		return $activity;
	}

	/**
	 * Imports the data for a record from the Service.
	 *
	 * This is a full end-to-end handling of the request; the method will queue the import on the Service,
	 * fetch the data from it and import the returned data (if any).
	 *
	 * @param array $assoc_args
	 * @param object|WP_Error $queue_result The result of the queue operation on the Service
	 * @param Tribe__Events__Aggregator__Record__Abstract $record
	 *
	 * @return Tribe__Events__Aggregator__Record__Activity
	 */
	protected function import_from_service( $queue_result, $record ) {
		$item_name = 'event';
		WP_CLI::log( 'Creating import on the service...' );

		if ( $queue_result instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while queueing the import on the service: ' . $queue_result->get_error_message() );
		}

		WP_CLI::log( "Import was assigned ID  {$record->meta['import_id']} from the service." );

		$start_time = microtime( true );
		$response   = null;

		if ( $record->is_polling() ) {

			WP_CLI::log( "Data will be fetched polling the service, timeout is {$this->polling_timeout} seconds." );

			$first = true;
			do {
				if ( ! $first ) {
					sleep( $this->polling_interval );
				}

				WP_CLI::log( 'Polling service to get data...' );

				$response = $record->get_import_data();
				$first    = false;
			} while (
				( $response instanceof stdClass && 'fetching' === $response->status )
				&& microtime( true ) - $start_time <= $this->polling_timeout
			);
		} else {
			WP_CLI::error( 'Batch data pushing is not supported in the CLI yet!' );
		}

		if ( null === $response ) {
			WP_CLI::error( 'Run out of time  while waiting for the data, check the source or explicitly set the `--timeout` parameter to a higher value and retry.' );
		}

		if ( $response instanceof WP_Error ) {
			WP_CLI::error( 'There was an error while fetching the data from the service: ' . $response->get_error_message() );
		}

		if ( ! property_exists( $response->data, 'events' ) ) {
			WP_CLI::error( 'Empty event data; response was ' . wp_json_encode( $response ) );
		}

		$items = $response->data->events;

		if ( empty( $items ) ) {
			WP_CLI::success( "No {$item_name}s found matching the query." );
		}

		$events_count = count( $items );

		WP_CLI::log( "Received data for {$events_count} event(s) from the service." );

		if ( empty( $response->data ) ) {
			WP_CLI::error( 'Empty data; response was ' . wp_json_encode( $response ) );
		}

		$progress = WP_CLI\Utils\make_progress_bar( 'Inserting posts', $events_count, $interval = 100 );

		// here we use the filter as an action to tick the progress
		add_action( 'tribe_aggregator_before_save_event', function ( array $event ) use ( $progress ) {
			$progress->tick();

			return $event;
		} );

		/** @var Tribe__Events__Aggregator__Record__Activity $activity */
		$activity = $record->insert_posts( $items );

		$progress->finish();

		return $activity;
	}

	/**
	 * Run a schuduled import.
	 *
	 * The command will use the API and licenses set for the site if required.
	 *
	 * <import_id>
	 * : the import ID, i.e. the import post ID in the site database
	 *
	 * [--timeout=<timeout>]
	 * : How long should the command wait for the data from EA Service in seconds
	 * ---
	 * default: 30
	 * ---
	 *
	 * [--format=<format>]
	 * : The results output format
	 * ---
	 *
	 * ## Examples
	 *
	 *      wp event-aggregator run-import 2389
	 *      wp event-aggregator run-import 2389 --timeout=180
	 *
	 * @since      4.6.15
	 *
	 * @subcommand run-import
	 *
	 * @when       after_wp_load
	 */
	public function run_import( array $args, array $assoc_args = [] ) {
		$this->ensure_timeout( $assoc_args );

		$record_id = $args[0];

		/** @var Tribe__Events__Aggregator__Records $records */
		$records = tribe( 'events-aggregator.records' );

		$parent_record = $records->get_by_post_id( $record_id );

		if ( ! $parent_record instanceof Tribe__Events__Aggregator__Record__Abstract ) {
			WP_CLI::error( "No scheduled record with a post ID of {$record_id} was found." );
		}

		// this should not be possible, yet let's take the possibility into account
		$is_csv = 'csv' === $parent_record->meta['origin'];

		if ( $is_csv ) {
			$this->ensure_column_map( $assoc_args );
		}

		WP_CLI::log( 'Creating child import post...' );

		$record = $parent_record->create_child_record();

		if ( ! $record instanceof Tribe__Events__Aggregator__Record__Abstract ) {
			if ( $record instanceof WP_Error ) {
				WP_CLI::error( "Could not create child record for record {$record_id}: " . $record->get_error_message() );
			} else {
				WP_CLI::error( "Could not create child record for record {$record_id}." );
			}
		}

		WP_CLI::log( "Created child import post with ID {$record->id}" );

		$record->update_meta( 'interactive', true );

		$this->fetch_and_process( $assoc_args, $record, $is_csv );
	}

	/**
	 * Checks the associative arguments to make sure the column map is provided for CSV imports.
	 *
	 * @since 4.6.15
	 *
	 * @param array $assoc_args
	 */
	protected function ensure_column_map( array $assoc_args = [] ) {
		if ( ! isset( $assoc_args['column_map'] ) ) {
			WP_CLI::error( 'the --column_map argument is required when importing CSV files.' );
		}
		$split = preg_split( '/\\s*,\\s*/', $assoc_args['column_map'] );

		if ( empty( $split ) ) {
			WP_CLI::error( 'the --column_map argument should contain a comma-separated list of column names for CSV imports.' );
		}
	}
}
