<?php
/**
 * WC_Bookings_CSV_Importer_Controller class
 *
 * @package WooCommerce Bookings
 * @since   3.6.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_Importer' ) ) {
	return;
}

/**
 * Multi-step controller for the booking CSV importer admin screen.
 *
 * Mirrors WC_GC_CSV_Importer_Controller and WC core's product CSV importer:
 * upload → mapping → import (driven by AJAX) → done. The AJAX work happens in
 * WC_Bookings_Admin_Importers; this class only renders/handles the form steps.
 *
 * @version 3.6.0
 */
class WC_Bookings_CSV_Importer_Controller {

	/**
	 * Submenu page slug used for the importer screen URL.
	 */
	const PAGE_URL = 'edit.php?post_type=wc_booking&page=bookings_importer';

	/**
	 * Path to the uploaded CSV file for the current session.
	 *
	 * @var string
	 */
	protected $file = '';

	/**
	 * The active step key.
	 *
	 * @var string
	 */
	protected $step = '';

	/**
	 * Step definitions ({ key => { name, view, handler } }).
	 *
	 * @var array
	 */
	protected $steps = array();

	/**
	 * Form-level errors collected during step handlers.
	 *
	 * @var array
	 */
	protected $errors = array();

	/**
	 * Active CSV delimiter for this session.
	 *
	 * @var string
	 */
	protected $delimiter = ',';

	/**
	 * Whether to reuse the user's last column-mapping selection.
	 *
	 * @var bool
	 */
	protected $map_preferences = false;

	/**
	 * Whether existing bookings (matched by ID) should be updated.
	 *
	 * @var bool
	 */
	protected $update_existing = false;

	/**
	 * Build a CSV importer instance for the supplied file/args.
	 *
	 * @param string $file File to import.
	 * @param array  $args Importer arguments.
	 * @return WC_Bookings_CSV_Importer
	 */
	public static function get_importer( $file, $args = array() ) {
		/**
		 * Filters the class used to instantiate the bookings CSV importer.
		 *
		 * @since 3.6.0
		 *
		 * @param string $importer_class Fully-qualified class name. Must extend WC_Bookings_Importer.
		 */
		$importer_class = apply_filters( 'woocommerce_bookings_csv_importer_class', 'WC_Bookings_CSV_Importer' );

		/**
		 * Filters the arguments passed to the bookings CSV importer constructor.
		 *
		 * @since 3.6.0
		 *
		 * @param array  $args           Importer arguments.
		 * @param string $importer_class Importer class name.
		 */
		$args = apply_filters( 'woocommerce_bookings_csv_importer_args', $args, $importer_class );

		return new $importer_class( $file, $args );
	}

	/**
	 * Validate that the supplied file is a CSV (by extension/MIME).
	 *
	 * @param string $file       File path.
	 * @param bool   $check_path Also check that the path is local (no remote streams).
	 * @return bool
	 */
	public static function is_file_valid_csv( $file, $check_path = true ) {
		/**
		 * Filters whether the importer enforces a local-path check (rejecting remote URL schemes).
		 *
		 * @since 3.6.0
		 *
		 * @param bool $check Whether to reject paths containing `://`.
		 */
		if ( $check_path && apply_filters( 'woocommerce_bookings_csv_importer_check_import_file_path', true ) && false !== stripos( $file, '://' ) ) {
			return false;
		}

		$valid_filetypes = self::get_valid_csv_filetypes();
		$filetype        = wp_check_filetype( $file, $valid_filetypes );

		return in_array( $filetype['type'], $valid_filetypes, true );
	}

	/**
	 * Whether the path resolves inside a PHAR archive (defence in depth).
	 *
	 * @param string $filepath The filepath to be checked.
	 * @return bool
	 */
	public static function is_within_phar( $filepath ) {
		return 'phar' === strtolower( (string) wp_parse_url( $filepath, PHP_URL_SCHEME ) );
	}

	/**
	 * Whether the path resolves inside ABSPATH.
	 *
	 * @param string $filepath The filepath to be checked.
	 * @return bool
	 */
	public static function is_within_abspath( $filepath ) {
		return 0 === stripos( realpath( $filepath ), trailingslashit( realpath( ABSPATH ) ) );
	}

	/**
	 * Whether the path resolves inside WP_CONTENT_DIR.
	 *
	 * @param string $filepath The filepath to be checked.
	 * @return bool
	 */
	public static function is_within_wp_content( $filepath ) {
		return 0 === stripos( realpath( $filepath ), trailingslashit( realpath( WP_CONTENT_DIR ) ) );
	}

	/**
	 * Allow-list of CSV-ish MIME types accepted by the uploader.
	 *
	 * @return array
	 */
	protected static function get_valid_csv_filetypes() {
		/**
		 * Filters the MIME types accepted by the bookings CSV importer uploader.
		 *
		 * @since 3.6.0
		 *
		 * @param array $filetypes Map of file extension to MIME type.
		 */
		return apply_filters(
			'woocommerce_bookings_import_valid_filetypes',
			array(
				'csv' => 'text/csv',
				'txt' => 'text/plain',
			)
		);
	}

	/**
	 * Constructor.
	 */
	public function __construct() {
		$default_steps = array(
			'upload'  => array(
				'name'    => __( 'Upload CSV file', 'woocommerce-bookings' ),
				'view'    => array( $this, 'upload_form' ),
				'handler' => array( $this, 'upload_form_handler' ),
			),
			'mapping' => array(
				'name'    => __( 'Column mapping', 'woocommerce-bookings' ),
				'view'    => array( $this, 'mapping_form' ),
				'handler' => '',
			),
			'import'  => array(
				'name'    => __( 'Import', 'woocommerce-bookings' ),
				'view'    => array( $this, 'import' ),
				'handler' => '',
			),
			'done'    => array(
				'name'    => __( 'Done!', 'woocommerce-bookings' ),
				'view'    => array( $this, 'done' ),
				'handler' => '',
			),
		);

		/**
		 * Filters the steps shown by the bookings CSV importer wizard.
		 *
		 * @since 3.6.0
		 *
		 * @param array $default_steps Map of step key to { name, view, handler }.
		 */
		$this->steps = apply_filters( 'woocommerce_bookings_csv_importer_steps', $default_steps );

		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- step-level nonces verified inside handlers.
		$this->step            = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) );
		$this->file            = isset( $_REQUEST['file'] ) ? wc_clean( wp_unslash( $_REQUEST['file'] ) ) : '';
		$this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false;
		$this->delimiter       = ! empty( $_REQUEST['delimiter'] ) ? wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ) : ',';
		$this->map_preferences = isset( $_REQUEST['map_preferences'] ) ? (bool) $_REQUEST['map_preferences'] : false;
		// phpcs:enable

		if ( $this->map_preferences ) {
			add_filter( 'woocommerce_bookings_csv_import_mapped_columns', array( $this, 'auto_map_user_preferences' ), 9999 );
		}
	}

	/**
	 * Build the URL for the next step.
	 *
	 * @param string $step Step slug (default: current step).
	 * @return string Next-step URL, admin URL on the last step, or empty string on failure.
	 */
	public function get_next_step_link( $step = '' ) {
		if ( ! $step ) {
			$step = $this->step;
		}

		$keys = array_keys( $this->steps );

		if ( end( $keys ) === $step ) {
			return admin_url();
		}

		$step_index = array_search( $step, $keys, true );

		if ( false === $step_index ) {
			return '';
		}

		$params = array(
			'step'            => $keys[ $step_index + 1 ],
			'file'            => str_replace( DIRECTORY_SEPARATOR, '/', $this->file ),
			'delimiter'       => $this->delimiter,
			'update_existing' => $this->update_existing,
			'map_preferences' => $this->map_preferences,
			'_wpnonce'        => wp_create_nonce( 'woocommerce-bookings-csv-importer' ),
		);

		// nosemgrep: audit.php.wp.security.xss.query-arg.
		return add_query_arg( $params );
	}

	/**
	 * Render the importer header.
	 */
	protected function output_header() {
		include __DIR__ . '/views/html-admin-csv-import-header.php';
	}

	/**
	 * Render the step indicator.
	 */
	protected function output_steps() {
		include __DIR__ . '/views/html-admin-csv-import-steps.php';
	}

	/**
	 * Render the importer footer.
	 */
	protected function output_footer() {
		include __DIR__ . '/views/html-admin-csv-import-footer.php';
	}

	/**
	 * Add a form-level error.
	 *
	 * @param string $message Error message.
	 * @param array  $actions Action links { url, label }.
	 */
	protected function add_error( $message, $actions = array() ) {
		$this->errors[] = array(
			'message' => $message,
			'actions' => $actions,
		);
	}

	/**
	 * Render any collected form errors.
	 */
	protected function output_errors() {
		if ( ! $this->errors ) {
			return;
		}

		foreach ( $this->errors as $error ) {
			echo '<div class="error inline">';
			echo '<p>' . esc_html( $error['message'] ) . '</p>';

			if ( ! empty( $error['actions'] ) ) {
				echo '<p>';
				foreach ( $error['actions'] as $action ) {
					echo '<a class="button button-primary" href="' . esc_url( $action['url'] ) . '">' . esc_html( $action['label'] ) . '</a> ';
				}
				echo '</p>';
			}
			echo '</div>';
		}
	}

	/**
	 * Run the active step (and its save handler if a form was submitted).
	 */
	public function dispatch() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! empty( $_POST['save_step'] ) && ! empty( $this->steps[ $this->step ]['handler'] ) ) {
			call_user_func( $this->steps[ $this->step ]['handler'], $this );
		}
		$this->output_header();
		$this->output_steps();
		$this->output_errors();
		call_user_func( $this->steps[ $this->step ]['view'], $this );
		$this->output_footer();
	}

	/**
	 * Upload step view.
	 */
	protected function upload_form() {
		/**
		 * Filters the maximum upload size accepted by the bookings CSV importer.
		 *
		 * Reuses WordPress core's `import_upload_size_limit` filter so any global override
		 * (such as the one set by importers in core) applies here as well.
		 *
		 * @since 3.6.0
		 *
		 * @param int $bytes Maximum upload size in bytes.
		 */
		$bytes      = apply_filters( 'import_upload_size_limit', wp_max_upload_size() );
		$size       = size_format( $bytes );
		$upload_dir = wp_upload_dir();

		include __DIR__ . '/views/html-admin-csv-import-form.php';
	}

	/**
	 * Upload step handler — validates the upload then redirects to the mapping step.
	 */
	public function upload_form_handler() {
		check_admin_referer( 'woocommerce-bookings-csv-importer' );

		$file = $this->handle_upload();

		if ( is_wp_error( $file ) ) {
			$this->add_error( $file->get_error_message() );
			return;
		}

		$this->file = $file;

		wp_safe_redirect( esc_url_raw( $this->get_next_step_link() ) );
		exit;
	}

	/**
	 * Handle the CSV upload (file input or server-path advanced option).
	 *
	 * @return string|WP_Error Uploaded file path on success.
	 */
	public function handle_upload() {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified by upload_form_handler() before this is called.
		$file_url = isset( $_POST['file_url'] ) ? wc_clean( wp_unslash( $_POST['file_url'] ) ) : '';

		if ( empty( $file_url ) ) {
			if ( ! isset( $_FILES['import'] ) ) {
				return new WP_Error( 'woocommerce_bookings_csv_importer_upload_file_empty', __( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini.', 'woocommerce-bookings' ) );
			}

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			if ( ! self::is_file_valid_csv( wc_clean( wp_unslash( $_FILES['import']['name'] ) ), false ) ) {
				return new WP_Error( 'woocommerce_bookings_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce-bookings' ) );
			}

			$overrides = array(
				'test_form' => false,
				'mimes'     => self::get_valid_csv_filetypes(),
			);
			$import    = $_FILES['import']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			$upload    = wp_handle_upload( $import, $overrides );

			if ( isset( $upload['error'] ) ) {
				return new WP_Error( 'woocommerce_bookings_csv_importer_upload_error', $upload['error'] );
			}

			$object = array(
				'post_title'     => basename( $upload['file'] ),
				'post_content'   => $upload['url'],
				'post_mime_type' => $upload['type'],
				'guid'           => $upload['url'],
				'context'        => 'import',
				'post_status'    => 'private',
			);

			$id = wp_insert_attachment( $object, $upload['file'] );

			// Schedule a cleanup in case the import fails halfway through.
			wp_schedule_single_event( time() + ( 2 * HOUR_IN_SECONDS ), 'importer_scheduled_cleanup', array( $id ) );

			return $upload['file'];
		} elseif (
			self::is_within_abspath( ABSPATH . $file_url )
			&& file_exists( ABSPATH . $file_url )
		) {
			if ( ! self::is_file_valid_csv( ABSPATH . $file_url ) ) {
				return new WP_Error( 'woocommerce_bookings_csv_importer_upload_file_invalid', __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce-bookings' ) );
			}

			return ABSPATH . $file_url;
		}
		// phpcs:enable

		return new WP_Error( 'woocommerce_bookings_csv_importer_upload_invalid_file', __( 'Please upload or provide the link to a valid CSV file.', 'woocommerce-bookings' ) );
	}

	/**
	 * Mapping step view — read the file headers and let the user assign them to internal column ids.
	 */
	protected function mapping_form() {
		check_admin_referer( 'woocommerce-bookings-csv-importer' );

		$args = array(
			'lines'     => 1,
			'delimiter' => $this->delimiter,
		);

		$importer = self::get_importer( $this->file, $args );
		$headers  = $importer->get_raw_keys();
		$sample   = current( $importer->get_raw_data() );

		foreach ( $headers as $index => $name ) {
			if ( $this->is_export_only_column( $name ) ) {
				unset( $headers[ $index ], $sample[ $index ] );
			}
		}

		$mapped_items = $this->auto_map_columns( $headers );

		if ( empty( $sample ) ) {
			$this->add_error(
				__( 'The file is empty or using a different encoding than UTF-8, please try again with a new file.', 'woocommerce-bookings' ),
				array(
					array(
						'url'   => admin_url( self::PAGE_URL ),
						'label' => __( 'Upload a new file', 'woocommerce-bookings' ),
					),
				)
			);

			$this->output_errors();
			return;
		}

		include_once __DIR__ . '/views/html-admin-csv-import-mapping.php';
	}

	/**
	 * Import step view — kicks off the AJAX import loop.
	 */
	public function import() {
		check_admin_referer( 'woocommerce-bookings-csv-importer' );

		if ( self::is_within_phar( $this->file ) ) {
			$this->add_error( __( 'Invalid file path. Files must not be within a PHAR executable.', 'woocommerce-bookings' ) );
			$this->output_errors();
			return;
		}

		if ( ! self::is_within_abspath( $this->file ) && ! self::is_within_wp_content( $this->file ) ) {
			$this->add_error( __( 'Invalid file path. Files must be uploaded to a location inside the WordPress absolute path or wp-content.', 'woocommerce-bookings' ) );
			$this->output_errors();
			return;
		}

		if ( ! self::is_file_valid_csv( $this->file ) ) {
			$this->add_error( __( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce-bookings' ) );
			$this->output_errors();
			return;
		}

		if ( ! is_file( $this->file ) ) {
			$this->add_error( __( 'The file does not exist, please try again.', 'woocommerce-bookings' ) );
			$this->output_errors();
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified at the top of this method.
		if ( ! empty( $_POST['map_from'] ) && ! empty( $_POST['map_to'] ) ) {
			$mapping_from = wc_clean( wp_unslash( $_POST['map_from'] ) );
			$mapping_to   = wc_clean( wp_unslash( $_POST['map_to'] ) );

			update_user_option( get_current_user_id(), 'woocommerce_bookings_import_mapping', $mapping_to );
		} else {
			wp_safe_redirect( esc_url_raw( $this->get_next_step_link( 'upload' ) ) );
			exit;
		}
		// phpcs:enable

		wp_localize_script(
			'wc_bookings_admin_import_js',
			'wc_bookings_import_params',
			array(
				'import_nonce'    => wp_create_nonce( 'wc-bookings-import' ),
				'mapping'         => array(
					'from' => $mapping_from,
					'to'   => $mapping_to,
				),
				'file'            => $this->file,
				'update_existing' => $this->update_existing,
				'delimiter'       => $this->delimiter,
			)
		);
		wp_enqueue_script( 'wc_bookings_admin_import_js' );

		include_once __DIR__ . '/views/html-admin-csv-import-progress.php';
	}

	/**
	 * Done step view — show counts of imported / updated / skipped / failed.
	 */
	protected function done() {
		check_admin_referer( 'woocommerce-bookings-csv-importer' );
		$imported  = isset( $_GET['bookings-imported'] ) ? absint( $_GET['bookings-imported'] ) : 0;
		$updated   = isset( $_GET['bookings-updated'] ) ? absint( $_GET['bookings-updated'] ) : 0;
		$failed    = isset( $_GET['bookings-failed'] ) ? absint( $_GET['bookings-failed'] ) : 0;
		$skipped   = isset( $_GET['bookings-skipped'] ) ? absint( $_GET['bookings-skipped'] ) : 0;
		$file_name = isset( $_GET['file-name'] ) ? sanitize_text_field( wp_unslash( $_GET['file-name'] ) ) : '';
		$errors    = array_filter( (array) get_user_option( 'bookings_import_error_log' ) );
		$warnings  = array_filter( (array) get_user_option( 'bookings_import_warning_log' ) );

		include_once __DIR__ . '/views/html-admin-csv-import-done.php';
	}

	/**
	 * Lowercase the keys of a column-name map for case-insensitive header matching.
	 *
	 * @param array $columns List of columns names and keys.
	 * @return array
	 */
	protected function normalize_columns_names( $columns ) {
		$normalized = array();

		foreach ( $columns as $key => $value ) {
			$normalized[ strtolower( $key ) ] = $value;
		}

		return $normalized;
	}

	/**
	 * Whether a CSV column is one of the export-only fields the importer cannot consume.
	 *
	 * The exporter emits derived/read-only columns (product/resource name, persons total,
	 * order status, customer name/email, modified date) that have no import counterpart.
	 * We hide these from the mapping UI so re-uploaded export files don't surface
	 * unmappable rows.
	 *
	 * @param string $header Raw CSV column header.
	 * @return bool
	 */
	protected function is_export_only_column( $header ) {
		$export_only_labels = array(
			__( 'Product name', 'woocommerce-bookings' ),
			__( 'Resource name', 'woocommerce-bookings' ),
			__( 'Persons total', 'woocommerce-bookings' ),
			__( 'Order status', 'woocommerce-bookings' ),
			__( 'Customer name', 'woocommerce-bookings' ),
			__( 'Customer email', 'woocommerce-bookings' ),
			__( 'Modified date', 'woocommerce-bookings' ),
		);

		$normalized = strtolower( trim( $header ) );

		foreach ( $export_only_labels as $label ) {
			if ( strtolower( $label ) === $normalized ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Auto-map raw CSV headers to internal column ids based on the default exporter labels.
	 *
	 * @param array $raw_headers Raw header columns from the CSV.
	 * @param bool  $num_indexes Whether to key the result by numeric index (default) or raw header value.
	 * @return array
	 */
	protected function auto_map_columns( $raw_headers, $num_indexes = true ) {
		$default_column_map = array(
			__( 'ID', 'woocommerce-bookings' )             => 'id',
			__( 'Status', 'woocommerce-bookings' )         => 'status',
			__( 'Attendance status', 'woocommerce-bookings' ) => 'attendance_status',
			__( 'All day', 'woocommerce-bookings' )        => 'all_day',
			__( 'Start date', 'woocommerce-bookings' )     => 'start',
			__( 'End date', 'woocommerce-bookings' )       => 'end',
			__( 'Customer timezone', 'woocommerce-bookings' ) => 'local_timezone',
			__( 'Cost', 'woocommerce-bookings' )           => 'cost',
			__( 'Product ID', 'woocommerce-bookings' )     => 'product_id',
			__( 'Resource ID', 'woocommerce-bookings' )    => 'resource_id',
			__( 'Person counts', 'woocommerce-bookings' )  => 'person_counts',
			__( 'Order ID', 'woocommerce-bookings' )       => 'order_id',
			__( 'Order item ID', 'woocommerce-bookings' )  => 'order_item_id',
			__( 'Parent booking ID', 'woocommerce-bookings' ) => 'parent_id',
			__( 'Customer user ID', 'woocommerce-bookings' ) => 'customer_id',
			__( 'Analytics customer ID', 'woocommerce-bookings' ) => 'woo_customer_id',
			__( 'Google Calendar event ID', 'woocommerce-bookings' ) => 'google_calendar_event_id',
			__( 'Customer note', 'woocommerce-bookings' )  => 'note',
			__( 'Created date', 'woocommerce-bookings' )   => 'date_created',
			__( 'Cancelled date', 'woocommerce-bookings' ) => 'date_cancelled',
		);

		/**
		 * Filters the default header→column-id auto-mapping used by the importer.
		 *
		 * @since 3.6.0
		 *
		 * @param array $columns     Map of header label to internal column id.
		 * @param array $raw_headers Raw CSV header row.
		 */
		$default_column_map = apply_filters( 'woocommerce_bookings_csv_import_mapping_default_columns', $default_column_map, $raw_headers );
		$default_columns    = $this->normalize_columns_names( $default_column_map );

		$special_column_map = array(
			/* translators: %s: meta key */
			__( 'Meta: %s', 'woocommerce-bookings' ) => 'meta:',
		);

		/**
		 * Filters the special-column patterns (e.g. `Meta: %s`) recognised during auto-mapping.
		 *
		 * @since 3.6.0
		 *
		 * @param array $columns     Map of label pattern to internal column-id prefix.
		 * @param array $raw_headers Raw CSV header row.
		 */
		$special_column_map = apply_filters( 'woocommerce_bookings_csv_import_mapping_special_columns', $special_column_map, $raw_headers );
		$special_columns    = $this->get_special_columns( $this->normalize_columns_names( $special_column_map ) );

		$headers = array();
		foreach ( $raw_headers as $key => $field ) {
			$normalized_field  = strtolower( $field );
			$index             = $num_indexes ? $key : $field;
			$headers[ $index ] = $normalized_field;

			if ( isset( $default_columns[ $normalized_field ] ) ) {
				$headers[ $index ] = $default_columns[ $normalized_field ];
			} else {
				foreach ( $special_columns as $regex => $special_key ) {
					if ( preg_match( $regex, $field, $matches ) ) {
						$headers[ $index ] = $special_key . $matches[1];
						break;
					}
				}
			}
		}

		/**
		 * Filters the auto-mapped header → column-id assignments.
		 *
		 * @since 3.6.0
		 *
		 * @param array $headers     Map of header index/value to internal column id.
		 * @param array $raw_headers Raw CSV header row.
		 */
		return apply_filters( 'woocommerce_bookings_csv_import_mapped_columns', $headers, $raw_headers );
	}

	/**
	 * Override auto_map_columns() with the user's last-saved mapping when "use previous preferences" is checked.
	 *
	 * @param array $headers Header columns.
	 * @return array
	 */
	public function auto_map_user_preferences( $headers ) {
		$mapping_preferences = get_user_option( 'woocommerce_bookings_import_mapping' );

		if ( ! empty( $mapping_preferences ) && is_array( $mapping_preferences ) ) {
			return $mapping_preferences;
		}

		return $headers;
	}

	/**
	 * Convert a printf-style label (e.g. "Meta: %s") into a regex used to capture special columns.
	 *
	 * @param string $value Raw special column name.
	 * @return string
	 */
	protected function sanitize_special_column_name_regex( $value ) {
		return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/i';
	}

	/**
	 * Compile special column patterns into anchored regex keys.
	 *
	 * @param array $columns Raw special columns.
	 * @return array
	 */
	protected function get_special_columns( $columns ) {
		$formatted = array();

		foreach ( $columns as $key => $value ) {
			$regex               = $this->sanitize_special_column_name_regex( $key );
			$formatted[ $regex ] = $value;
		}

		return $formatted;
	}

	/**
	 * Mapping options shown in the per-column dropdown.
	 *
	 * @param string $item Currently mapped key (used to surface the right meta variant).
	 * @return array
	 */
	protected function get_mapping_options( $item = '' ) {
		// Properly format meta keys preserved across re-renders.
		$meta = str_replace( 'meta:', '', $item );

		$options = array(
			'id'                       => __( 'ID', 'woocommerce-bookings' ),
			'status'                   => __( 'Status', 'woocommerce-bookings' ),
			'attendance_status'        => __( 'Attendance status', 'woocommerce-bookings' ),
			'all_day'                  => __( 'All day', 'woocommerce-bookings' ),
			'start'                    => __( 'Start date', 'woocommerce-bookings' ),
			'end'                      => __( 'End date', 'woocommerce-bookings' ),
			'local_timezone'           => __( 'Customer timezone', 'woocommerce-bookings' ),
			'cost'                     => __( 'Cost', 'woocommerce-bookings' ),
			'product_id'               => __( 'Product ID', 'woocommerce-bookings' ),
			'resource_id'              => __( 'Resource ID', 'woocommerce-bookings' ),
			'person_counts'            => __( 'Person counts', 'woocommerce-bookings' ),
			'order_id'                 => __( 'Order ID', 'woocommerce-bookings' ),
			'order_item_id'            => __( 'Order item ID', 'woocommerce-bookings' ),
			'parent_id'                => __( 'Parent booking ID', 'woocommerce-bookings' ),
			'customer_id'              => __( 'Customer user ID', 'woocommerce-bookings' ),
			'woo_customer_id'          => __( 'Analytics customer ID', 'woocommerce-bookings' ),
			'google_calendar_event_id' => __( 'Google Calendar event ID', 'woocommerce-bookings' ),
			'note'                     => __( 'Customer note', 'woocommerce-bookings' ),
			'date_created'             => __( 'Created date', 'woocommerce-bookings' ),
			'date_cancelled'           => __( 'Cancelled date', 'woocommerce-bookings' ),
			'meta:' . $meta            => __( 'Import as meta data', 'woocommerce-bookings' ),
		);

		/**
		 * Filters the options shown in each mapping-step column dropdown.
		 *
		 * @since 3.6.0
		 *
		 * @param array  $options Map of internal column id to human-readable label.
		 * @param string $item    The currently selected mapping for the column.
		 */
		return apply_filters( 'woocommerce_bookings_csv_import_mapping_options', $options, $item );
	}
}
