<?php

class WPMDB_CLI extends WPMDB_Base {

	/**
	 * Instance of WPMDB.
	 *
	 * @var WPMDB
	 */
	protected $wpmdb;

	/**
	 * Migration profile.
	 *
	 * @var array
	 */
	protected $profile;

	/**
	 * Data to post during migration.
	 *
	 * @var array
	 */
	protected $post_data = array();

	/**
	 * Required PHP version
	 *
	 * @var string
	 */
	protected $php_version_required;

	/**
	 * Migration Data
	 *
	 * @var array
	 */
	protected $migration;

	function __construct( $plugin_file_path ) {
		parent::__construct( $plugin_file_path );

		if ( ! version_compare( PHP_VERSION, $this->php_version_required, '>=' ) ) {
			return;
		}

		global $wpmdb;
		$this->wpmdb = $wpmdb;

		add_filter( 'wpmdb_cli_finalize_migration_response', array( $this, 'finalize_ajax' ), 10, 1 );
	}

	/**
	 * Checks profile data before CLI migration.
	 *
	 * @param int|array $profile Profile key or array.
	 *
	 * @return mixed|WP_Error
	 */
	public function pre_cli_migration_check( $profile ) {
		if ( ! version_compare( PHP_VERSION, $this->php_version_required, '>=' ) ) {
			return $this->cli_error( sprintf( __( 'CLI addon requires PHP %1$s+', 'wp-migrate-db-cli' ), $this->php_version_required ) );
		}

		if ( is_array( $profile ) ) {
			$query_str = http_build_query( $profile );
			$profile   = $this->wpmdb->parse_migration_form_data( $query_str );
			$profile   = wp_parse_args( $profile, array(
				'save_computer'             => '0',
				'gzip_file'                 => '0',
				'replace_guids'             => '0',
				'exclude_transients'        => '0',
				'exclude_spam'              => '0',
				'keep_active_plugins'       => '0',
				'compatibility_older_mysql' => '0',
			) );
		}

		$this->profile = $profile = apply_filters( 'wpmdb_cli_profile_before_migration', $profile );

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

		return true;
	}

	/**
	 * Performs CLI migration given a profile data.
	 *
	 * @param  int|array $profile Profile key or array.
	 *
	 * @return bool|WP_Error Returns true if succeed or WP_Error if failed.
	 */
	public function cli_migration( $profile ) {
		$pre_check = $this->pre_cli_migration_check( $profile );
		if ( is_wp_error( $pre_check ) ) {
			return $pre_check;
		}

		// At this point, $profile has been checked a retrieved into $this->profile, so should not be used in this function any further.
		if ( empty( $this->profile ) ) {
			return $this->cli_error( __( 'Profile not found or unable to be generated from params.', 'wp-migrate-db-cli' ) );
		}
		unset( $profile );

		$this->set_time_limit();
		$this->wpmdb->set_cli_migration();

		if ( 'savefile' === $this->profile['action'] ) {
			$this->post_data['intent'] = 'savefile';
			if ( ! empty( $this->profile['export_dest'] ) ) {
				$this->post_data['export_dest'] = $this->profile['export_dest'];
			} else {
				$this->post_data['export_dest'] = 'ORIGIN';
			}
		}

		if ( 'find_replace' === $this->profile['action'] ) {
			$this->post_data['intent'] = 'find_replace';
		}

		// Ensure local site_details available.
		$this->post_data['site_details']['local'] = $this->site_details();

		// Check for tables specified in migration profile that do not exist in the source database
		if ( ! empty( $this->profile['select_tables'] ) ) {
			$source_tables = apply_filters( 'wpmdb_cli_filter_source_tables', $this->get_tables() );

			if ( ! empty( $source_tables ) ) {
				// Return error if selected tables do not exist in source database
				$nonexistent_tables = array();
				foreach ( $this->profile['select_tables'] as $table ) {
					if ( ! in_array( $table, $source_tables ) ) {
						$nonexistent_tables[] = $table;
					}
				}

				if ( ! empty( $nonexistent_tables ) ) {
					$local_or_remote = ( 'pull' === $this->profile['action'] ) ? 'remote' : 'local';

					return $this->cli_error( sprintf( __( 'The following table(s) do not exist in the %1$s database: %2$s', 'wp-migrate-db-cli' ), $local_or_remote, implode( ', ', $nonexistent_tables ) ) );
				}
			}
		}

		$this->profile = apply_filters( 'wpmdb_cli_filter_before_cli_initiate_migration', $this->profile );
		if ( is_wp_error( $this->profile ) ) {
			return $this->profile;
		}

		do_action( 'wpmdb_cli_before_migration', $this->post_data, $this->profile );
		$this->migration = $this->cli_initiate_migration();

		if ( is_wp_error( $this->migration ) ) {
			return $this->migration;
		}

		$this->post_data['migration_state_id'] = $this->migration['migration_state_id'];

		$tables_to_process = $this->migrate_tables();
		if ( is_wp_error( $tables_to_process ) ) {
			return $tables_to_process;
		}

		$this->post_data['tables'] = implode( ',', $tables_to_process );

		$finalize = $this->finalize_migration();
		if ( is_wp_error( $finalize ) || 'savefile' === $this->profile['action'] ) {
			return $finalize;
		}

		return true;
	}

	/**
	 * Verify CLI response from endpoint.
	 *
	 * @param  string $response      Response from endpoint.
	 * @param  string $function_name Name of called function.
	 *
	 * @return WP_Error|string
	 */
	function verify_cli_response( $response, $function_name ) {
		$response = trim( $response );
		if ( false === $response ) {
			return $this->cli_error( $this->error );
		}

		if ( false === $this->wpmdb->is_json( $response ) ) {
			return $this->cli_error( sprintf( __( 'We were expecting a JSON response, instead we received: %2$s (function name: %1$s)', 'wp-migrate-db-cli' ), $function_name, $response ) );
		}

		$response = json_decode( $response, true );
		if ( isset( $response['wpmdb_error'] ) ) {
			return $this->cli_error( $response['body'] );
		}

		// Display warnings and non fatal error messages as CLI warnings without aborting.
		if ( isset( $response['wpmdb_warning'] ) || isset( $response['wpmdb_non_fatal_error'] ) ) {
			$body     = ( isset ( $response['cli_body'] ) ) ? $response['cli_body'] : $response['body'];
			$messages = maybe_unserialize( $body );
			foreach ( ( array ) $messages as $message ) {
				if ( $message ) {
					WP_CLI::warning( self::cleanup_message( $message ) );
				}
			}
		}

		return $response;
	}

	/**
	 * Return instance of WP_Error.
	 *
	 * @param  string $message Error message.
	 *
	 * @return WP_Error.
	 */
	function cli_error( $message ) {
		return new WP_Error( 'wpmdb_cli_error', self::cleanup_message( $message ) );
	}

	/**
	 * Cleanup message, replacing <br> with \n and removing HTML.
	 *
	 * @param  string $message Error message.
	 *
	 * @return string $message.
	 */
	static function cleanup_message( $message ) {
		$message = html_entity_decode( $message, ENT_QUOTES );
		$message = preg_replace( '#<br\s*/?>#', "\n", $message );
		$message = trim( strip_tags( $message ) );

		return $message;
	}

	/**
	 * Initiates migration and verifies result
	 *
	 * @return array|WP_Error
	 */
	function cli_initiate_migration() {
		do_action( 'wpmdb_cli_before_initiate_migration', $this->profile );

		WP_CLI::log( __( 'Initiating migration...', 'wp-migrate-db-cli' ) );

		$migration_args                          = $this->post_data;
		$migration_args['form_data']             = http_build_query( $this->profile );
		$migration_args['stage']                 = 'migrate';
		$migration_args['site_details']['local'] = $this->site_details();

		if ( 'find_replace' === $this->profile['action'] ) {
			$migration_args['stage'] = 'find_replace';

		}

		$this->post_data = apply_filters( 'wpmdb_cli_initiate_migration_args', $migration_args, $this->profile );

		$this->post_data['site_details'] = json_encode( $this->post_data['site_details'] );

		$response = $this->initiate_migration( $this->post_data );

		$initiate_migration_response = $this->verify_cli_response( $response, 'initiate_migration()' );
		if ( ! is_wp_error( $initiate_migration_response ) ) {
			$initiate_migration_response = apply_filters( 'wpmdb_cli_initiate_migration_response', $initiate_migration_response );
		}

		return $initiate_migration_response;
	}

	/**
	 * Determine which tables to migrate
	 *
	 * @return array|WP_Error
	 */
	function get_tables_to_migrate() {
		$tables_to_migrate = $this->get_tables( 'prefix' );

		return apply_filters( 'wpmdb_cli_tables_to_migrate', $tables_to_migrate, $this->profile, $this->migration );
	}

	/**
	 * Returns a WP-CLI progress bar instance
	 *
	 * @param array $tables
	 * @param int   $stage
	 *
	 * @return \cli\progress\Bar
	 */
	function get_progress_bar( $tables, $stage ) {

		$progress_label = __( 'Exporting tables', 'wp-migrate-db-cli' );

		if ( 'find_replace' === $this->profile['action'] ) {
			$progress_label = __( 'Running find & replace', 'wp-migrate-db-cli' );
		}

		$progress_label = apply_filters( 'wpmdb_cli_progress_label', $progress_label, $stage, $tables );

		$progress_label = str_pad( $progress_label, 20, ' ' );

		$count = $this->get_total_rows_from_table_list( $tables, $stage );

		return new \cli\progress\Bar( $progress_label, $count );
	}

	/**
	 * Returns total rows from list of tables
	 *
	 * @param array $tables
	 * @param int   $stage
	 *
	 * @return Int
	 */
	function get_total_rows_from_table_list( $tables, $stage ) {
		static $cached_results = array();

		if ( isset( $cached_results[ $stage ] ) ) {
			return $cached_results[ $stage ];
		}

		$table_rows               = $this->get_row_counts_from_table_list( $tables, $stage );
		$cached_results[ $stage ] = array_sum( array_intersect_key( $table_rows, array_flip( $tables ) ) );

		return $cached_results[ $stage ];
	}

	/**
	 * Returns row counts from list of tables
	 *
	 * @param array $tables
	 * @param int   $stage
	 *
	 * @return mixed
	 */
	function get_row_counts_from_table_list( $tables, $stage ) {
		static $cached_results = array();

		if ( isset( $cached_results[ $stage ] ) ) {
			return $cached_results[ $stage ];
		}

		$local_table_rows         = $this->wpmdb->get_table_row_count();
		$cached_results[ $stage ] = apply_filters( 'wpmdb_cli_get_row_counts_from_table_list', $local_table_rows, $stage );

		return $cached_results[ $stage ];
	}

	/**
	 * @return array|mixed|string|void|WP_Error
	 */
	function migrate_tables() {
		$tables_to_migrate = $this->get_tables_to_migrate();

		$tables         = $tables_to_migrate;
		$stage_iterator = 2;

		$filtered_vars = apply_filters( 'wpmdb_cli_filter_before_migrate_tables', array(
			'tables'         => $tables,
			'stage_iterator' => $stage_iterator,
		) );
		if ( ! is_array( $filtered_vars ) ) {
			return $filtered_vars;
		} else {
			extract( $filtered_vars, EXTR_OVERWRITE );
		}

		if ( empty( $tables ) ) {
			return $this->cli_error( __( 'No tables selected for migration.', 'wp-migrate-db' ) );
		}

		$table_rows = $this->get_row_counts_from_table_list( $tables, $stage_iterator );

		do_action( 'wpmdb_cli_before_migrate_tables', $this->profile, $this->migration );

		$notify = $this->get_progress_bar( $tables, $stage_iterator );
		$args   = $this->post_data;

		do {
			$migration_progress = 0;

			foreach ( $tables as $key => $table ) {
				$current_row         = -1;
				$primary_keys        = '';
				$table_progress      = 0;
				$table_progress_last = 0;

				$args['table']      = $table;
				$args['last_table'] = ( $key == count( $tables ) - 1 ) ? '1' : '0';

				do {
					// reset the current chunk
					$this->wpmdb->empty_current_chunk();

					$args['current_row']  = $current_row;
					$args['primary_keys'] = $primary_keys;
					$args                 = apply_filters( 'wpmdb_cli_migrate_table_args', $args, $this->profile, $this->migration );

					$response = $this->migrate_table( $args );

					$migrate_table_response = $this->verify_cli_response( $response, 'migrate_table()' );

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

					$migrate_table_response = apply_filters( 'wpmdb_cli_migrate_table_response', $migrate_table_response, $_POST, $this->profile, $this->migration );

					$current_row  = $migrate_table_response['current_row'];
					$primary_keys = $migrate_table_response['primary_keys'];

					$last_migration_progress = $migration_progress;

					if ( -1 == $current_row ) {
						$migration_progress -= $table_progress;
						$migration_progress += $table_rows[ $table ];
					} else {
						if ( 0 === $table_progress_last ) {
							$table_progress_last = $current_row;
							$table_progress      = $table_progress_last;
							$migration_progress += $table_progress_last;
						} else {
							$iteration_progress  = $current_row - $table_progress_last;
							$table_progress_last = $current_row;
							$table_progress += $iteration_progress;
							$migration_progress += $iteration_progress;
						}
					}

					$increment = $migration_progress - $last_migration_progress;

					$notify->tick( $increment );

				} while ( -1 != $current_row );
			}

			$notify->finish();

			++$stage_iterator;
			$args['stage'] = 'migrate';
			$tables        = $tables_to_migrate;
			$table_rows    = $this->get_row_counts_from_table_list( $tables, $stage_iterator );

			if ( $stage_iterator < 3 ) {
				$notify = $this->get_progress_bar( $tables, $stage_iterator );
			}
		} while ( $stage_iterator < 3 );

		$this->post_data = $args;

		return $tables;
	}

	/**
	 * Finalize migration
	 *
	 * @return bool|WP_Error
	 */
	function finalize_migration() {
		do_action( 'wpmdb_cli_before_finalize_migration', $this->profile, $this->migration );

		WP_CLI::log( __( 'Cleaning up...', 'wp-migrate-db-cli' ) );

		$finalize = apply_filters( 'wpmdb_cli_finalize_migration', true, $this->profile, $this->migration );
		if ( is_wp_error( $finalize ) ) {
			return $finalize;
		}

		$this->post_data = apply_filters( 'wpmdb_cli_finalize_migration_args', $this->post_data, $this->profile, $this->migration );

		if ( 'savefile' === $this->post_data['intent'] ) {
			return $this->finalize_export();
		}

		$response = null;
		$response = apply_filters( 'wpmdb_cli_finalize_migration_response', $response );
		if ( ! empty( $response ) && '1' !== $response ) {
			return $this->cli_error( $response );
		}

		do_action( 'wpmdb_cli_after_finalize_migration', $this->profile, $this->migration );

		return true;
	}

	/**
	 * Stub for ajax_initiate_migration()
	 *
	 * @param array|bool $args
	 *
	 * @return string
	 */
	function initiate_migration( $args = false ) {
		$_POST    = $args;
		$response = $this->wpmdb->ajax_initiate_migration();

		return $response;
	}

	/**
	 * stub for ajax_migrate_table()
	 *
	 * @param array|bool $args
	 *
	 * @return string
	 */
	function migrate_table( $args = false ) {
		$_POST    = $args;
		$response = $this->wpmdb->ajax_migrate_table();

		return $response;
	}

	/**
	 * Stub for ajax_finalize_migration()
	 * hooks on: wpmdb_cli_finalize_migration_response
	 *
	 * @param string $response
	 *
	 * @return string
	 */
	function finalize_ajax( $response ) {
		// don't send redundant POST variables
		$args     = $this->filter_post_elements( $this->post_data, array( 'action', 'migration_state_id', 'prefix', 'tables' ) );
		$_POST    = $args;
		$response = $this->wpmdb->ajax_finalize_migration();

		return trim( $response );
	}

	/**
	 * Finalize Export by moving file to specified destination
	 *
	 * @return string|error
	 */
	function finalize_export() {
		$state_data = $this->wpmdb->state_data;
		$temp_file  = $state_data['dump_path'];
		if ( 'ORIGIN' === $state_data['export_dest'] ) {
			$response = $temp_file;
		} else {
			$dest_file = $state_data['export_dest'];
			if ( file_exists( $temp_file ) && rename( $temp_file, $dest_file ) ) {
				$response = $dest_file;
			} else {
				$response = $this->cli_error( __( 'Unable to move exported file.', 'wp-migrate-db' ) );
			}
		}

		return $response;
	}
	/**
	 * Returns array of CLI options that are unknown to plugin and addons.
	 *
	 * @param array $assoc_args
	 *
	 * @return array
	 */
	public function get_unknown_args( $assoc_args = array() ) {
		$unknown_args = array();

		if ( empty( $assoc_args ) ) {
			return $unknown_args;
		}

		$known_args = array(
			'action',
			'export_dest',
			'find',
			'replace',
			'exclude-spam',
			'gzip-file',
			'exclude-post-revisions',
			'skip-replace-guids',
			'include-transients',
		);

		$known_args = apply_filters( 'wpmdb_cli_filter_get_extra_args', $known_args );
		$unknown_args = array_diff( array_keys( $assoc_args ), $known_args );
		
		return $unknown_args;
	}

	/**
	 * Get profile data from CLI args.
	 *
	 * @param array $args
	 * @param array $assoc_args
	 *
	 * @return array|WP_Error
	 */
	public function get_profile_data_from_args( $args, $assoc_args ) {

		//load correct cli class
		if ( function_exists( 'wp_migrate_db_pro_cli_addon' ) ) {
			$wpmdb_cli = wp_migrate_db_pro_cli_addon();
		} elseif ( function_exists( 'wpmdb_pro_cli' ) ) {
			$wpmdb_cli = wpmdb_pro_cli();
		} else {
			$wpmdb_cli = wpmdb_cli();
		}

		$unknown_args = $this->get_unknown_args( $assoc_args );

		if ( ! empty( $unknown_args ) ) {
			$message = __( 'Parameter errors: ', 'wp-migrate-db-cli' );
			foreach ( $unknown_args as $unknown_arg ) {
				$message .= "\n " . sprintf( __( 'unknown %s parameter', 'wp-migrate-db-cli' ), '--' . $unknown_arg );
			}

			if ( is_a( $wpmdb_cli, 'WPMDBPro_CLI' ) ) {
				$message .= "\n" . __( 'Please make sure that you have activated the appropriate addons for WP Migrate DB Pro.', 'wp-migrate-db-cli' );
			}

			return $wpmdb_cli->cli_error( $message );
		}

		if ( empty( $assoc_args['action'] ) ) {
			return $wpmdb_cli->cli_error( __( 'Missing action parameter', 'wp-migrate-db-cli' ) );
		}

		if ( 'savefile' === $assoc_args['action'] && ! empty( $assoc_args['export_dest'] ) ) {
			$export_dest = $assoc_args['export_dest'];
		}

		$action = $assoc_args['action'];

		// --find=<old> and --replace=<new>
		$replace_old = array();
		$replace_new = array();
		if ( ! empty( $assoc_args['find'] ) ) {
			$replace_old = str_getcsv( $assoc_args['find'] );
		} else {
			if ( 'find_replace' === $assoc_args['action'] ) {
				return $wpmdb_cli->cli_error( __( 'Missing find and replace values.', 'wp-migrate-db-cli' ) );
			}
		}
		if ( ! empty( $assoc_args['replace'] ) ) {
			$replace_new = str_getcsv( $assoc_args['replace'] );
		}
		if ( count( $replace_old ) !== count( $replace_new ) ) {
			return $wpmdb_cli->cli_error( sprintf( __( '%1$s and %2$s must contain the same number of values', 'wp-migrate-db-cli' ), '--find', '--replace' ) );
		}
		array_unshift( $replace_old, '' );
		array_unshift( $replace_new, '' );

		// --exclude-spam
		$exclude_spam = intval( isset( $assoc_args['exclude-spam'] ) );

		// --gzip-file
		$gzip_file = intval( isset( $assoc_args['gzip-file'] ) );

		$select_post_types = array();

		// --exclude-post-revisions
		if ( ! empty( $assoc_args['exclude-post-revisions'] ) ) {
			$select_post_types[] = 'revision';
		}

		$exclude_post_types = count( $select_post_types ) > 0 ? 1 : 0;

		// --skip-replace-guids
		$replace_guids = 1;
		if ( isset( $assoc_args['skip-replace-guids'] ) ) {
			$replace_guids = 0;
		}

		$select_tables        = array();
		$table_migrate_option = 'migrate_only_with_prefix';

		// --include-transients.
		$exclude_transients = intval( ! isset( $assoc_args['include-transients'] ) );

		//cleanup filename for exports
		if ( ! empty( $export_dest ) ) {
			if ( $gzip_file ) {
				if ( 'gz' !== pathinfo( $export_dest, PATHINFO_EXTENSION ) ) {
					if ( 'sql' === pathinfo( $export_dest, PATHINFO_EXTENSION ) ) {
						$export_dest .= '.gz';
					} else {
						$export_dest .= '.sql.gz';
					}
				}
			} elseif ( 'sql' !== pathinfo( $export_dest, PATHINFO_EXTENSION ) ) {
				$export_dest = preg_replace( '/(\.sql)?(\.gz)?$/i', '', $export_dest ) . '.sql';
			}

			// ensure export destination is writable
			if ( ! @touch( $export_dest ) ) {
				return $wpmdb_cli->cli_error( sprintf( __( 'Cannot write to file "%1$s". Please ensure that the specified directory exists and is writable.', 'wp-migrate-db-cli' ), $export_dest ) );
			}
		}

		$profile = compact( 'action', 'replace_old', 'table_migrate_option', 'replace_new', 'select_tables', 'exclude_post_types', 'select_post_types', 'replace_guids', 'exclude_spam', 'gzip_file', 'exclude_transients', 'export_dest' );

		$profile = apply_filters( 'wpmdb_cli_filter_get_profile_data_from_args', $profile, $args, $assoc_args );

		return $profile;
	}
}
