<?php
/**
 * Safe data migration tools for Yatoon Booking.
 *
 * @package Yatoon_Booking_System
 */

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

class YATOON_Data_Portability {
	const MAX_FILE_BYTES = 5242880;
	const MAX_ROWS       = 20000;
	const EXPORT_BATCH   = 2000;

	public static function init() {
		add_action( 'admin_menu', array( __CLASS__, 'register_page' ), 40 );
		add_action( 'admin_post_yatoon_run_import', array( __CLASS__, 'handle_import' ) );
		add_action( 'admin_post_yatoon_export_portable_data', array( __CLASS__, 'handle_export' ) );
	}

	public static function register_page() {
		add_submenu_page(
			'yatoon-booking-system',
			__( 'Import & Migration', 'yatoon-booking-system' ),
			__( 'Import & Migration', 'yatoon-booking-system' ),
			'manage_options',
			'yatoon-import',
			array( __CLASS__, 'render_page' )
		);
	}

	private static function table_exists( $table ) {
		global $wpdb;
		return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
	}

	private static function detected_sources() {
		global $wpdb;
		return array(
			'bookly' => self::table_exists( $wpdb->prefix . 'bookly_services' ),
			'amelia' => self::table_exists( $wpdb->prefix . 'amelia_services' ) || self::table_exists( $wpdb->prefix . 'amelia_users' ),
		);
	}

	public static function render_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		$detected = self::detected_sources();
		$result   = get_transient( 'yatoon_import_result_' . get_current_user_id() );
		$from_wizard = 'wizard' === sanitize_key( wp_unslash( $_GET['from'] ?? '' ) );
		$export_types = array(
			'all'      => __( 'Complete portable ZIP', 'yatoon-booking-system' ),
			'services' => __( 'Services CSV', 'yatoon-booking-system' ),
			'staff'    => __( 'Staff CSV', 'yatoon-booking-system' ),
			'clients'  => __( 'Clients CSV', 'yatoon-booking-system' ),
			'bookings' => __( 'Appointments CSV', 'yatoon-booking-system' ),
		);
		delete_transient( 'yatoon_import_result_' . get_current_user_id() );
		?>
		<div class="wrap yatoon-import-wrap">
			<h1><?php esc_html_e( 'Import & Migration', 'yatoon-booking-system' ); ?></h1>
			<p><?php esc_html_e( 'Move services, staff, and clients into Yatoon without retyping them. Run a preview first; existing matching records are updated instead of duplicated.', 'yatoon-booking-system' ); ?></p>
			<?php if ( $from_wizard ) : ?>
				<div class="notice notice-info inline"><p><strong><?php esc_html_e( 'Setup wizard: import your data now, or continue setup without importing.', 'yatoon-booking-system' ); ?></strong> <a class="button button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=yatoon-setup-wizard&wizard_step=3' ) ); ?>"><?php esc_html_e( 'Continue setup wizard', 'yatoon-booking-system' ); ?></a></p></div>
			<?php endif; ?>
			<?php if ( is_array( $result ) ) : ?>
				<div class="notice <?php echo ! empty( $result['error'] ) ? 'notice-error' : 'notice-success'; ?> is-dismissible"><p>
					<?php echo esc_html( $result['message'] ); ?>
				</p></div>
			<?php endif; ?>
			<style>
			.yatoon-import-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;max-width:1120px;margin-top:18px}.yatoon-import-card{background:#fff;border:1px solid #dcdcde;border-radius:12px;padding:20px}.yatoon-import-card h2{margin-top:0}.yatoon-import-card label{display:block;font-weight:600;margin:14px 0 5px}.yatoon-import-card select,.yatoon-import-card input[type=file]{width:100%;max-width:100%}.yatoon-import-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}.yatoon-import-status{display:inline-flex;border-radius:999px;padding:3px 9px;font-size:12px;font-weight:700;background:#f3f4f6}.yatoon-import-status.is-ready{background:#dcfce7;color:#166534}.yatoon-import-help{color:#646970;font-size:13px}.yatoon-import-checklist{line-height:1.8}.yatoon-export-card,.yatoon-full-backup-card,.yatoon-target-export-card{max-width:1080px}.yatoon-export-card,.yatoon-full-backup-card{border-color:#a7d7b8;background:linear-gradient(135deg,#fff 0%,#f4fbf6 100%)}.yatoon-export-layout,.yatoon-backup-columns{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,1fr);gap:24px}.yatoon-export-layout{align-items:end}.yatoon-full-backup-card,.yatoon-target-export-card{margin-top:18px}.yatoon-backup-columns>form{border:1px solid #dcdcde;border-radius:10px;padding:16px;background:#fff}.yatoon-backup-columns h3{margin-top:0}.yatoon-restore-confirm{color:#8a2424}.yatoon-export-note{margin:14px 0 0;padding:10px 12px;border-left:4px solid #dba617;background:#fff8e5}@media(max-width:782px){.yatoon-export-layout,.yatoon-backup-columns{grid-template-columns:1fr}}
			</style>
			<?php
			// 4.6.1: both files happen to sit behind the same "data_tools"
			// switch today, which is the only reason this static call has not
			// fataled yet. Load on demand instead of relying on that coupling.
			if ( ! class_exists( 'YATOON_Backup_Migration' ) && class_exists( 'YATOON_Feature_Modules' ) ) {
				YATOON_Feature_Modules::ensure_loaded( 'data_tools' );
			}
			if ( class_exists( 'YATOON_Backup_Migration' ) ) {
				YATOON_Backup_Migration::render_controls();
			}
			?>
			<section class="yatoon-import-card yatoon-export-card">
				<div class="yatoon-export-layout">
					<div>
						<h2><?php esc_html_e( 'Portable CSV exports', 'yatoon-booking-system' ); ?></h2>
						<p><?php esc_html_e( 'Download selected UTF-8 CSV files for spreadsheets and manual data transfer. These are convenient data exports, not a complete restorable backup.', 'yatoon-booking-system' ); ?></p>
					</div>
					<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
						<?php wp_nonce_field( 'yatoon_export_portable_data' ); ?>
						<input type="hidden" name="action" value="yatoon_export_portable_data">
						<label for="yatoon-export-type"><?php esc_html_e( 'Export file', 'yatoon-booking-system' ); ?></label>
						<select id="yatoon-export-type" name="export_type"><?php foreach ( $export_types as $export_key => $export_label ) : ?><option value="<?php echo esc_attr( $export_key ); ?>"><?php echo esc_html( $export_label ); ?></option><?php endforeach; ?></select>
						<div class="yatoon-import-actions"><button class="button button-primary" type="submit"><?php esc_html_e( 'Download export', 'yatoon-booking-system' ); ?></button></div>
					</form>
				</div>
				<p class="yatoon-export-note"><strong><?php esc_html_e( 'Privacy notice:', 'yatoon-booking-system' ); ?></strong> <?php esc_html_e( 'Client and appointment exports contain personal information. Store them securely. Use Complete Yatoon backup above when you need an exact cross-site restore.', 'yatoon-booking-system' ); ?></p>
			</section>
			<h2 style="margin:26px 0 0"><?php esc_html_e( 'Import into Yatoon', 'yatoon-booking-system' ); ?></h2>
			<div class="yatoon-import-grid">
				<section class="yatoon-import-card">
					<h2><?php esc_html_e( 'Generic CSV', 'yatoon-booking-system' ); ?></h2>
					<p class="yatoon-import-help"><?php esc_html_e( 'Works with exports from most booking systems. UTF-8 CSV, up to 5 MB and 20,000 rows per run.', 'yatoon-booking-system' ); ?></p>
					<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
						<?php wp_nonce_field( 'yatoon_run_import' ); ?>
						<input type="hidden" name="action" value="yatoon_run_import">
						<input type="hidden" name="source" value="csv">
						<?php if ( $from_wizard ) : ?><input type="hidden" name="from_wizard" value="1"><?php endif; ?>
						<label for="yatoon-import-entity"><?php esc_html_e( 'Data type', 'yatoon-booking-system' ); ?></label>
						<select id="yatoon-import-entity" name="entity"><option value="services"><?php esc_html_e( 'Services', 'yatoon-booking-system' ); ?></option><option value="staff"><?php esc_html_e( 'Staff', 'yatoon-booking-system' ); ?></option><option value="clients"><?php esc_html_e( 'Clients', 'yatoon-booking-system' ); ?></option></select>
						<label for="yatoon-import-file"><?php esc_html_e( 'CSV file', 'yatoon-booking-system' ); ?></label>
						<input id="yatoon-import-file" name="import_file" type="file" accept=".csv,text/csv" required>
						<label><input type="checkbox" name="dry_run" value="1" checked> <?php esc_html_e( 'Preview only (recommended first)', 'yatoon-booking-system' ); ?></label>
						<div class="yatoon-import-actions"><button class="button button-primary" type="submit"><?php esc_html_e( 'Check CSV', 'yatoon-booking-system' ); ?></button></div>
					</form>
				</section>

				<?php if ( class_exists( 'YATOON_SaaS_Import' ) ) : ?>
					<?php foreach ( array_intersect_key( YATOON_SaaS_Import::sources(), array_flip( array( 'fresha', 'booksy', 'vagaro' ) ) ) as $source => $label ) : ?>
						<section class="yatoon-import-card yatoon-saas-import-card">
							<h2><?php echo esc_html( $label ); ?></h2>
							<p class="yatoon-import-help"><?php esc_html_e( 'Import customers, appointment history, remaining package visits, and gift card balances from a CSV export. You can preview column matching before anything is saved and undo a completed import.', 'yatoon-booking-system' ); ?></p>
							<div class="yatoon-import-actions"><a class="button button-primary" href="<?php echo esc_url( add_query_arg( array( 'page' => 'yatoon-migrate', 'source' => $source ), admin_url( 'admin.php' ) ) ); ?>"><?php esc_html_e( 'Open importer', 'yatoon-booking-system' ); ?></a></div>
						</section>
					<?php endforeach; ?>
				<?php endif; ?>

				<?php foreach ( array( 'bookly' => 'Bookly', 'amelia' => 'Amelia' ) as $source => $label ) : ?>
					<section class="yatoon-import-card">
						<h2><?php echo esc_html( $label ); ?></h2>
						<p><span class="yatoon-import-status <?php echo $detected[ $source ] ? 'is-ready' : ''; ?>"><?php echo esc_html( $detected[ $source ] ? __( 'Detected', 'yatoon-booking-system' ) : __( 'Not detected', 'yatoon-booking-system' ) ); ?></span></p>
						<p class="yatoon-import-help"><?php esc_html_e( 'Imports directly from the existing WordPress tables. The source plugin data is never changed or deleted.', 'yatoon-booking-system' ); ?></p>
						<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
							<?php wp_nonce_field( 'yatoon_run_import' ); ?>
							<input type="hidden" name="action" value="yatoon_run_import"><input type="hidden" name="source" value="<?php echo esc_attr( $source ); ?>">
							<?php if ( $from_wizard ) : ?><input type="hidden" name="from_wizard" value="1"><?php endif; ?>
							<label><input type="checkbox" name="entities[]" value="services" checked> <?php esc_html_e( 'Services', 'yatoon-booking-system' ); ?></label>
							<label><input type="checkbox" name="entities[]" value="staff" checked> <?php esc_html_e( 'Staff', 'yatoon-booking-system' ); ?></label>
							<label><input type="checkbox" name="entities[]" value="clients" checked> <?php esc_html_e( 'Clients', 'yatoon-booking-system' ); ?></label>
							<label><input type="checkbox" name="dry_run" value="1" checked> <?php esc_html_e( 'Preview only (recommended first)', 'yatoon-booking-system' ); ?></label>
							<div class="yatoon-import-actions"><button class="button button-primary" type="submit" <?php disabled( ! $detected[ $source ] ); ?>><?php esc_html_e( 'Preview migration', 'yatoon-booking-system' ); ?></button></div>
						</form>
					</section>
				<?php endforeach; ?>

				<section class="yatoon-import-card">
					<h2><?php esc_html_e( 'Square', 'yatoon-booking-system' ); ?></h2>
					<p class="yatoon-import-help"><?php esc_html_e( 'Square is imported through Yatoon\'s version-aware catalog sync so service variation IDs, staff eligibility, bookings, and future updates stay connected.', 'yatoon-booking-system' ); ?></p>
					<div class="yatoon-import-actions"><a class="button button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=yatoon-booking-system&section=platform' ) ); ?>"><?php esc_html_e( 'Open Square setup', 'yatoon-booking-system' ); ?></a><a class="button" href="<?php echo esc_url( admin_url( 'admin.php?page=sbs-services' ) ); ?>"><?php esc_html_e( 'Open service sync', 'yatoon-booking-system' ); ?></a></div>
				</section>
			</div>
			<div class="yatoon-import-card" style="max-width:1080px;margin-top:16px"><h2><?php esc_html_e( 'Before you import', 'yatoon-booking-system' ); ?></h2><ol class="yatoon-import-checklist"><li><?php esc_html_e( 'Create a database backup from Tools > Operations.', 'yatoon-booking-system' ); ?></li><li><?php esc_html_e( 'Preview and review the detected row counts.', 'yatoon-booking-system' ); ?></li><li><?php esc_html_e( 'Uncheck Preview only and run again to save.', 'yatoon-booking-system' ); ?></li><li><?php esc_html_e( 'Review staff-to-service assignments after migration.', 'yatoon-booking-system' ); ?></li></ol></div>
		</div>
		<?php
	}

	/**
	 * Return the portable, deliberately non-secret export definitions.
	 *
	 * Tokens, payment-provider IDs, signatures, webhook data, and credentials
	 * are intentionally excluded from every export.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	private static function export_datasets() {
		return array(
			'services' => array(
				'table'    => 'yatoon_services',
				'filename' => 'services.csv',
				'columns'  => array( 'id', 'square_id', 'name', 'description', 'duration_minutes', 'price', 'estimated_cost', 'category', 'image_url', 'is_active', 'require_policy', 'require_consent', 'allow_staff_select', 'allow_design_upload', 'buffer_before_min', 'buffer_after_min', 'min_lead_time_min', 'sort_order', 'is_featured', 'created_at', 'updated_at' ),
			),
			'service_options' => array(
				'table'    => 'yatoon_service_options',
				'filename' => 'service-options.csv',
				'columns'  => array( 'id', 'service_id', 'square_id', 'square_version', 'name', 'description', 'duration_minutes', 'price_modifier', 'is_required', 'group_name', 'badge_label', 'sort_order', 'created_at', 'updated_at' ),
			),
			'staff' => array(
				'table'    => 'yatoon_staff',
				'filename' => 'staff.csv',
				'columns'  => array( 'id', 'square_id', 'name', 'email', 'phone', 'bio', 'image_url', 'calendar_color', 'is_bookable', 'is_active', 'sort_order', 'portal_role', 'created_at', 'updated_at' ),
			),
			'staff_services' => array(
				'table'    => 'yatoon_staff_services',
				'filename' => 'staff-service-assignments.csv',
				'columns'  => array( 'id', 'staff_id', 'service_id', 'created_at' ),
			),
			'clients' => array(
				'table'    => 'yatoon_clients',
				'filename' => 'clients.csv',
				'columns'  => array( 'id', 'customer_name', 'customer_email', 'customer_phone', 'notes', 'allergies', 'preferences', 'preferred_staff', 'tags', 'created_at', 'updated_at' ),
			),
			'bookings' => array(
				'table'    => 'yatoon_bookings',
				'filename' => 'appointments.csv',
				'columns'  => array( 'id', 'square_booking_id', 'customer_name', 'customer_email', 'customer_phone', 'client_timezone', 'booking_timezone', 'location_id', 'service_id', 'staff_id', 'start_time', 'end_time', 'status', 'notes', 'design_attachment_id', 'design_image_url', 'total_price', 'net_amount', 'tax_rate', 'tax_amount', 'tax_inclusive', 'tip_amount', 'square_synced', 'is_any_staff', 'google_event_id', 'consent_accepted', 'consent_signed_at', 'policy_acknowledged', 'policy_acknowledged_at', 'payment_status', 'payment_updated_at', 'deposit_amount', 'deposit_paid', 'created_at', 'created_at_gmt', 'updated_at' ),
			),
			'booking_options' => array(
				'table'    => 'yatoon_booking_options',
				'filename' => 'appointment-options.csv',
				'columns'  => array( 'id', 'booking_id', 'option_id', 'created_at' ),
			),
			'locations' => array(
				'table'    => 'yatoon_locations',
				'filename' => 'locations.csv',
				'columns'  => array( 'id', 'square_id', 'name', 'address', 'phone', 'timezone', 'is_active', 'created_at', 'updated_at' ),
			),
		);
	}

	/**
	 * Download a portable CSV or ZIP export.
	 */
	public static function handle_export() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to export data.', 'yatoon-booking-system' ) );
		}
		check_admin_referer( 'yatoon_export_portable_data' );
		$type     = sanitize_key( wp_unslash( $_POST['export_type'] ?? '' ) );
		$datasets = self::export_datasets();

		if ( 'all' === $type ) {
			self::download_complete_export( $datasets );
		}
		if ( ! isset( $datasets[ $type ] ) || in_array( $type, array( 'service_options', 'staff_services', 'booking_options', 'locations' ), true ) ) {
			wp_die( esc_html__( 'Choose a valid export file.', 'yatoon-booking-system' ) );
		}

		self::send_download_headers( 'yatoon-' . $datasets[ $type ]['filename'], 'text/csv; charset=UTF-8' );
		$handle = fopen( 'php://output', 'wb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		if ( ! $handle ) {
			wp_die( esc_html__( 'The export file could not be created.', 'yatoon-booking-system' ) );
		}
		fwrite( $handle, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
		self::write_dataset_csv( $handle, $datasets[ $type ] );
		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
		exit;
	}

	/**
	 * Build and download the complete portable ZIP.
	 *
	 * @param array<string,array<string,mixed>> $datasets Export definitions.
	 */
	private static function download_complete_export( $datasets ) {
		if ( ! class_exists( 'ZipArchive' ) ) {
			wp_die( esc_html__( 'ZIP export is unavailable on this server. Download the individual CSV files instead.', 'yatoon-booking-system' ) );
		}
		$zip_path = wp_tempnam( 'yatoon-portable-export.zip' );
		if ( ! $zip_path ) {
			wp_die( esc_html__( 'The export file could not be created.', 'yatoon-booking-system' ) );
		}
		$zip       = new ZipArchive();
		$temp_csvs = array();
		if ( true !== $zip->open( $zip_path, ZipArchive::OVERWRITE ) ) {
			wp_delete_file( $zip_path );
			wp_die( esc_html__( 'The export ZIP could not be opened.', 'yatoon-booking-system' ) );
		}

		foreach ( $datasets as $dataset ) {
			$csv_path = wp_tempnam( $dataset['filename'] );
			$handle   = $csv_path ? fopen( $csv_path, 'wb' ) : false; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
			if ( ! $handle ) {
				$zip->close();
				foreach ( $temp_csvs as $temp_csv ) {
					wp_delete_file( $temp_csv );
				}
				wp_delete_file( $zip_path );
				wp_die( esc_html__( 'One of the export CSV files could not be created.', 'yatoon-booking-system' ) );
			}
			fwrite( $handle, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
			self::write_dataset_csv( $handle, $dataset );
			fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			$temp_csvs[] = $csv_path;
			$zip->addFile( $csv_path, $dataset['filename'] );
		}

		$zip->addFromString(
			'README.txt',
			"Yatoon Booking portable export\r\n" .
			'Created: ' . gmdate( 'Y-m-d H:i:s' ) . " UTC\r\n\r\n" .
			"This archive contains UTF-8 CSV files for services, options, staff, staff/service assignments, clients, appointments, appointment options, and locations.\r\n" .
			"Security-sensitive tokens, credentials, payment-provider IDs, signatures, and webhook data are intentionally excluded.\r\n" .
			"Client and appointment files contain personal information. Store this archive securely.\r\n" .
			"For a full restorable Yatoon database snapshot, use Tools > Operations.\r\n"
		);
		$zip->close();
		foreach ( $temp_csvs as $temp_csv ) {
			wp_delete_file( $temp_csv );
		}

		$filename = 'yatoon-booking-portable-' . gmdate( 'Y-m-d-His' ) . '.zip';
		self::send_download_headers( $filename, 'application/zip', filesize( $zip_path ) );
		readfile( $zip_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
		wp_delete_file( $zip_path );
		exit;
	}

	/**
	 * Write one dataset to an open CSV stream in bounded batches.
	 *
	 * @param resource                  $handle  Writable stream.
	 * @param array<string,mixed>       $dataset Export definition.
	 */
	private static function write_dataset_csv( $handle, $dataset ) {
		global $wpdb;
		$table = $wpdb->prefix . $dataset['table'];
		if ( ! self::table_exists( $table ) ) {
			fputcsv( $handle, $dataset['columns'] );
			return;
		}

		// All identifiers originate from the fixed definitions above, never request data.
		$available = $wpdb->get_col( "SHOW COLUMNS FROM `{$table}`", 0 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$columns   = array_values( array_intersect( $dataset['columns'], $available ) );
		if ( empty( $columns ) || ! in_array( 'id', $columns, true ) ) {
			fputcsv( $handle, $dataset['columns'] );
			return;
		}
		fputcsv( $handle, $columns );
		$column_sql = '`' . implode( '`,`', $columns ) . '`';
		$last_id    = 0;
		do {
			$query = $wpdb->prepare( "SELECT {$column_sql} FROM `{$table}` WHERE `id` > %d ORDER BY `id` ASC LIMIT %d", $last_id, self::EXPORT_BATCH ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$rows  = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
			foreach ( $rows as $row ) {
				$last_id = (int) $row['id'];
				fputcsv( $handle, array_map( array( __CLASS__, 'protect_csv_cell' ), array_values( $row ) ) );
			}
		} while ( count( $rows ) === self::EXPORT_BATCH );
	}

	/**
	 * Neutralize spreadsheet formulas in user-entered CSV values.
	 *
	 * @param mixed $value Cell value.
	 * @return string
	 */
	private static function protect_csv_cell( $value ) {
		$value = null === $value ? '' : (string) $value;
		return preg_match( '/^[=+\-@\t\r]/u', $value ) ? "'" . $value : $value;
	}

	/**
	 * Send safe download headers after clearing buffered admin markup.
	 *
	 * @param string   $filename Download name.
	 * @param string   $mime     MIME type.
	 * @param int|null $length   Optional byte count.
	 */
	private static function send_download_headers( $filename, $mime, $length = null ) {
		while ( ob_get_level() ) {
			ob_end_clean();
		}
		nocache_headers();
		header( 'Content-Type: ' . $mime );
		header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
		header( 'X-Content-Type-Options: nosniff' );
		if ( null !== $length ) {
			header( 'Content-Length: ' . (string) (int) $length );
		}
	}

	public static function handle_import() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to import data.', 'yatoon-booking-system' ) );
		}
		check_admin_referer( 'yatoon_run_import' );
		$source  = sanitize_key( wp_unslash( $_POST['source'] ?? '' ) );
		$dry_run = ! empty( $_POST['dry_run'] );
		try {
			if ( 'csv' === $source ) {
				$entity = sanitize_key( wp_unslash( $_POST['entity'] ?? '' ) );
				$rows   = self::read_csv_upload();
				$result = self::import_rows( $source, $entity, $rows, $dry_run );
			} elseif ( in_array( $source, array( 'bookly', 'amelia' ), true ) ) {
				$entities = array_intersect( array_map( 'sanitize_key', (array) wp_unslash( $_POST['entities'] ?? array() ) ), array( 'services', 'staff', 'clients' ) );
				$result   = self::import_native_source( $source, $entities, $dry_run );
			} else {
				throw new RuntimeException( __( 'Unknown import source.', 'yatoon-booking-system' ) );
			}
			$message = sprintf(
				/* translators: 1: mode, 2: rows checked, 3: new rows, 4: updated rows, 5: skipped rows. */
				__( '%1$s complete: %2$d checked, %3$d new, %4$d updated, %5$d skipped.', 'yatoon-booking-system' ),
				$dry_run ? __( 'Preview', 'yatoon-booking-system' ) : __( 'Import', 'yatoon-booking-system' ),
				$result['checked'],
				$result['created'],
				$result['updated'],
				$result['skipped']
			);
			set_transient( 'yatoon_import_result_' . get_current_user_id(), array( 'message' => $message ), MINUTE_IN_SECONDS );
		} catch ( Throwable $error ) {
			set_transient( 'yatoon_import_result_' . get_current_user_id(), array( 'error' => true, 'message' => $error->getMessage() ), MINUTE_IN_SECONDS );
		}
		$redirect_url = admin_url( 'admin.php?page=yatoon-import' );
		if ( ! empty( $_POST['from_wizard'] ) ) {
			$redirect_url = add_query_arg( 'from', 'wizard', $redirect_url );
		}
		wp_safe_redirect( $redirect_url );
		exit;
	}

	private static function read_csv_upload() {
		// Upload paths cannot be text-sanitized; the authenticated caller and the structural, size, MIME, and CSV checks below validate this array.
		$file = isset( $_FILES['import_file'] ) ? $_FILES['import_file'] : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( empty( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
			throw new RuntimeException( __( 'Choose a CSV file to continue.', 'yatoon-booking-system' ) );
		}
		if ( UPLOAD_ERR_OK !== (int) $file['error'] || (int) $file['size'] > self::MAX_FILE_BYTES ) {
			throw new RuntimeException( __( 'The CSV could not be read or is larger than 5 MB.', 'yatoon-booking-system' ) );
		}
		if ( 'csv' !== strtolower( pathinfo( sanitize_file_name( $file['name'] ), PATHINFO_EXTENSION ) ) ) {
			throw new RuntimeException( __( 'Only .csv files are accepted.', 'yatoon-booking-system' ) );
		}
		$checked = wp_check_filetype_and_ext( $file['tmp_name'], sanitize_file_name( $file['name'] ), array( 'csv' => 'text/csv' ) );
		if ( ! empty( $checked['ext'] ) && 'csv' !== $checked['ext'] ) {
			throw new RuntimeException( __( 'The uploaded file is not a valid CSV.', 'yatoon-booking-system' ) );
		}
		$handle = fopen( $file['tmp_name'], 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		if ( ! $handle ) {
			throw new RuntimeException( __( 'The CSV file could not be opened.', 'yatoon-booking-system' ) );
		}
		$headers = fgetcsv( $handle );
		if ( ! is_array( $headers ) ) {
			fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			throw new RuntimeException( __( 'The CSV has no header row.', 'yatoon-booking-system' ) );
		}
		$headers = array_map( array( __CLASS__, 'normalize_header' ), $headers );
		$rows    = array();
		while ( ( $values = fgetcsv( $handle ) ) !== false && count( $rows ) < self::MAX_ROWS ) {
			$values = array_pad( $values, count( $headers ), '' );
			$rows[] = array_combine( $headers, array_slice( $values, 0, count( $headers ) ) );
		}
		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
		return $rows;
	}

	private static function normalize_header( $header ) {
		$header = preg_replace( '/^\xEF\xBB\xBF/', '', (string) $header );
		return sanitize_key( str_replace( array( ' ', '-' ), '_', strtolower( trim( $header ) ) ) );
	}

	private static function value( $row, $aliases, $default = '' ) {
		foreach ( $aliases as $alias ) {
			$key = self::normalize_header( $alias );
			if ( isset( $row[ $key ] ) && '' !== trim( (string) $row[ $key ] ) ) {
				return trim( (string) $row[ $key ] );
			}
		}
		return $default;
	}

	private static function import_native_source( $source, $entities, $dry_run ) {
		global $wpdb;
		$rows_by_entity = array();
		if ( 'bookly' === $source ) {
			if ( in_array( 'services', $entities, true ) && self::table_exists( $wpdb->prefix . 'bookly_services' ) ) {
				$rows_by_entity['services'] = $wpdb->get_results( "SELECT id,title AS name,CEIL(duration/60) AS duration_minutes,price,category_id FROM {$wpdb->prefix}bookly_services", ARRAY_A );
			}
			if ( in_array( 'staff', $entities, true ) && self::table_exists( $wpdb->prefix . 'bookly_staff' ) ) {
				$rows_by_entity['staff'] = $wpdb->get_results( "SELECT id,full_name AS name,email,phone,visibility FROM {$wpdb->prefix}bookly_staff", ARRAY_A );
			}
			if ( in_array( 'clients', $entities, true ) && self::table_exists( $wpdb->prefix . 'bookly_customers' ) ) {
				$rows_by_entity['clients'] = $wpdb->get_results( "SELECT id,full_name AS name,first_name,last_name,email,phone,notes FROM {$wpdb->prefix}bookly_customers", ARRAY_A );
			}
		} else {
			if ( in_array( 'services', $entities, true ) && self::table_exists( $wpdb->prefix . 'amelia_services' ) ) {
				$rows_by_entity['services'] = $wpdb->get_results( "SELECT id,name,CEIL(duration/60) AS duration_minutes,price,status,categoryId AS category_id FROM {$wpdb->prefix}amelia_services", ARRAY_A );
			}
			if ( self::table_exists( $wpdb->prefix . 'amelia_users' ) ) {
				if ( in_array( 'staff', $entities, true ) ) {
					$rows_by_entity['staff'] = $wpdb->get_results( "SELECT id,CONCAT(firstName,' ',lastName) AS name,email,phone,status FROM {$wpdb->prefix}amelia_users WHERE type IN ('provider','employee')", ARRAY_A );
				}
				if ( in_array( 'clients', $entities, true ) ) {
					$rows_by_entity['clients'] = $wpdb->get_results( "SELECT id,CONCAT(firstName,' ',lastName) AS name,firstName AS first_name,lastName AS last_name,email,phone,note AS notes FROM {$wpdb->prefix}amelia_users WHERE type='customer'", ARRAY_A );
				}
			}
		}
		$total = array( 'checked' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0 );
		foreach ( $rows_by_entity as $entity => $rows ) {
			$part = self::import_rows( $source, $entity, $rows, $dry_run );
			foreach ( $total as $key => $unused ) {
				$total[ $key ] += $part[ $key ];
			}
		}
		return $total;
	}

	private static function import_rows( $source, $entity, $rows, $dry_run ) {
		if ( ! in_array( $entity, array( 'services', 'staff', 'clients' ), true ) ) {
			throw new RuntimeException( __( 'Choose services, staff, or clients.', 'yatoon-booking-system' ) );
		}
		$result = array( 'checked' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0 );
		foreach ( array_slice( (array) $rows, 0, self::MAX_ROWS ) as $row ) {
			++$result['checked'];
			$outcome = self::upsert_row( $source, $entity, (array) $row, $dry_run );
			++$result[ $outcome ];
		}
		return $result;
	}

	private static function upsert_row( $source, $entity, $row, $dry_run ) {
		global $wpdb;
		if ( 'services' === $entity ) {
			$name = sanitize_text_field( self::value( $row, array( 'name', 'title', 'service_name' ) ) );
			if ( '' === $name ) { return 'skipped'; }
			$external = sanitize_text_field( self::value( $row, array( 'square_id', 'external_id', 'id' ), md5( $name ) ) );
			$key      = substr( 'import:' . $source . ':' . $external, 0, 255 );
			$existing = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_services WHERE square_id=%s OR name=%s LIMIT 1", $key, $name ) );
			$data = array( 'square_id' => $key, 'name' => $name, 'description' => sanitize_textarea_field( self::value( $row, array( 'description', 'info' ) ) ), 'duration_minutes' => max( 1, (int) self::value( $row, array( 'duration_minutes', 'duration', 'length' ), 30 ) ), 'price' => max( 0, (float) self::value( $row, array( 'price', 'cost' ), 0 ) ), 'category' => sanitize_text_field( self::value( $row, array( 'category', 'category_name', 'category_id' ) ) ), 'is_active' => in_array( strtolower( self::value( $row, array( 'status', 'active', 'is_active' ), 'active' ) ), array( '0', 'inactive', 'disabled', 'hidden' ), true ) ? 0 : 1 );
			if ( ! $dry_run ) { $existing ? $wpdb->update( $wpdb->prefix . 'yatoon_services', $data, array( 'id' => $existing ) ) : $wpdb->insert( $wpdb->prefix . 'yatoon_services', $data ); }
			return $existing ? 'updated' : 'created';
		}
		if ( 'staff' === $entity ) {
			$name = sanitize_text_field( self::value( $row, array( 'name', 'full_name', 'display_name' ) ) );
			if ( '' === $name ) { return 'skipped'; }
			$email    = sanitize_email( self::value( $row, array( 'email', 'email_address' ) ) );
			$external = sanitize_text_field( self::value( $row, array( 'square_id', 'external_id', 'id' ), md5( $email . $name ) ) );
			$key      = substr( 'import:' . $source . ':' . $external, 0, 255 );
			$existing = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_staff WHERE square_id=%s OR (email<>'' AND email=%s) LIMIT 1", $key, $email ) );
			$data = array( 'square_id' => $key, 'name' => $name, 'email' => $email, 'phone' => sanitize_text_field( self::value( $row, array( 'phone', 'phone_number' ) ) ), 'is_active' => in_array( strtolower( self::value( $row, array( 'status', 'active', 'is_active' ), 'active' ) ), array( '0', 'inactive', 'disabled' ), true ) ? 0 : 1, 'is_bookable' => in_array( strtolower( self::value( $row, array( 'visibility', 'bookable', 'is_bookable' ), 'public' ) ), array( '0', 'private', 'disabled', 'hidden' ), true ) ? 0 : 1 );
			if ( ! $dry_run ) { $existing ? $wpdb->update( $wpdb->prefix . 'yatoon_staff', $data, array( 'id' => $existing ) ) : $wpdb->insert( $wpdb->prefix . 'yatoon_staff', $data ); }
			return $existing ? 'updated' : 'created';
		}
		$name = sanitize_text_field( self::value( $row, array( 'name', 'full_name', 'customer_name' ) ) );
		if ( '' === $name ) {
			$name = trim( sanitize_text_field( self::value( $row, array( 'first_name', 'firstname' ) ) ) . ' ' . sanitize_text_field( self::value( $row, array( 'last_name', 'lastname' ) ) ) );
		}
		$email = sanitize_email( self::value( $row, array( 'email', 'customer_email', 'email_address' ) ) );
		$phone = sanitize_text_field( self::value( $row, array( 'phone', 'customer_phone', 'phone_number' ) ) );
		if ( '' === $name || ( '' === $email && '' === $phone ) ) { return 'skipped'; }
		$where = '' !== $phone ? $wpdb->prepare( 'customer_phone=%s', $phone ) : $wpdb->prepare( 'customer_email=%s', $email );
		$existing = (int) $wpdb->get_var( "SELECT id FROM {$wpdb->prefix}yatoon_clients WHERE {$where} LIMIT 1" );
		$data = array( 'customer_name' => $name, 'customer_email' => $email, 'customer_phone' => $phone, 'notes' => sanitize_textarea_field( self::value( $row, array( 'notes', 'note', 'description' ) ) ) );
		if ( ! $dry_run ) { $existing ? $wpdb->update( $wpdb->prefix . 'yatoon_clients', $data, array( 'id' => $existing ) ) : $wpdb->insert( $wpdb->prefix . 'yatoon_clients', $data ); }
		return $existing ? 'updated' : 'created';
	}
}
