<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
const DROP_PRINT_SKU_MIGRATION_PAGE_SLUG = 'drop-print-migration-wizard';
function drop_print_sku_migration_page_url(): string {
	return add_query_arg( array( 'page' => DROP_PRINT_SKU_MIGRATION_PAGE_SLUG ), admin_url( 'admin.php' ) );
}
function drop_print_sku_migration_is_current(): bool {
	return is_admin() && ! empty( $_GET['page'] ) && DROP_PRINT_SKU_MIGRATION_PAGE_SLUG === sanitize_key( wp_unslash( $_GET['page'] ) );
}
function drop_print_sku_migration_register_page() {
	add_dashboard_page( '', '', 'manage_options', DROP_PRINT_SKU_MIGRATION_PAGE_SLUG, 'drop_print_sku_migration_render_page' );
}
add_action( 'admin_menu', 'drop_print_sku_migration_register_page', 20 );
function drop_print_sku_migration_admin_body_class( string $classes ): string {
	if ( drop_print_sku_migration_is_current() ) {
		$classes .= ' drop-print-sku-migration-page';
	}
	return $classes;
}
add_filter( 'admin_body_class', 'drop_print_sku_migration_admin_body_class' );
function drop_print_sku_migration_suppress_notices() {
	if ( drop_print_sku_migration_is_current() ) {
		remove_all_actions( 'admin_notices' );
		remove_all_actions( 'all_admin_notices' );
	}
}
add_action( 'in_admin_header', 'drop_print_sku_migration_suppress_notices', 0 );
function drop_print_sku_migration_enqueue_assets() {
	if ( ! drop_print_sku_migration_is_current() ) {
		return;
	}
	$path = DROP_PRINT_PLUGIN_PATH . 'assets/css/sku-migration-review.css';
	wp_enqueue_style(
		'drop-print-sku-migration-review',
		DROP_PRINT_PLUGIN_URL . 'assets/css/sku-migration-review.css',
		array(),
		file_exists( $path ) ? filemtime( $path ) : DROP_PRINT_VERSION
	);
}
add_action( 'admin_enqueue_scripts', 'drop_print_sku_migration_enqueue_assets' );
function drop_print_sku_migration_cached_preview() {
	if ( drop_print_sku_migration_is_current() ) {
		return null;
	}
	$cached = get_transient( 'drop_print_sku_migration_cached_preview' );
	if ( false !== $cached ) {
		return $cached;
	}
	if ( ! function_exists( 'drop_print_sku_migration_build_preview' ) ) {
		return null;
	}
	$preview = drop_print_sku_migration_build_preview();
	if ( is_wp_error( $preview ) ) {
		set_transient( 'drop_print_sku_migration_cached_preview', null, 2 * MINUTE_IN_SECONDS );
		return null;
	}
	set_transient( 'drop_print_sku_migration_cached_preview', $preview, 15 * MINUTE_IN_SECONDS );
	return $preview;
}
function drop_print_sku_migration_pending_counts() {
	$preview = drop_print_sku_migration_cached_preview();
	return is_array( $preview ) ? $preview['counts'] : null;
}
function drop_print_sku_migration_admin_notice() {
	if ( ! is_admin() || ! current_user_can( 'manage_options' ) || wp_doing_ajax() ) {
		return;
	}
	if ( ! function_exists( 'drop_print_save_product_meta' ) ) {
		return;
	}
	$counts = drop_print_sku_migration_pending_counts();
	if ( ! is_array( $counts ) || ( $counts['ready'] + $counts['attention'] ) <= 0 ) {
		return;
	}
	$unresolved_families = $counts['unresolved_families'] ?? array();
	$is_blocking = ! empty( $unresolved_families );
	?>
	<div class="notice <?php echo $is_blocking ? 'notice-error' : 'notice-info'; ?>">
		<p>
			<?php
			printf(
				esc_html__( '%1$d Drop Print product variation(s) across %2$d product(s) still use a 1.x paper code. Nothing changes until you review it.', 'drop-print' ),
				(int) ( $counts['ready'] + $counts['attention'] ),
				(int) $counts['products']
			);
			?>
			<a href="<?php echo esc_url( drop_print_sku_migration_page_url() ); ?>" class="button button-secondary" style="margin-left:8px;">
				<?php esc_html_e( 'Review migration', 'drop-print' ); ?>
			</a>
		</p>
		<?php if ( $is_blocking ) : ?>
			<p>
				<strong><?php esc_html_e( 'Drop Print cannot produce a print for these specific paper codes - orders using them will fail at checkout until this is resolved.', 'drop-print' ); ?></strong>
				<?php
				printf(
					esc_html__( '%1$d paper code(s) don\'t match any known paper at all (not just a known one missing a size): %2$s. These need support, not this screen.', 'drop-print' ),
					count( $unresolved_families ),
					esc_html( implode( ', ', $unresolved_families ) )
				);
				?>
			</p>
		<?php endif; ?>
	</div>
	<?php
}
add_action( 'admin_notices', 'drop_print_sku_migration_admin_notice' );
function drop_print_sku_migration_product_edit_notice() {
	if ( ! is_admin() || ! current_user_can( 'manage_options' ) || wp_doing_ajax() ) {
		return;
	}
	if ( ! function_exists( 'drop_print_save_product_meta' ) ) {
		return;
	}
	$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
	if ( ! $screen || 'product' !== $screen->id || 'post' !== $screen->base ) {
		return;
	}
	$product_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
	if ( $product_id <= 0 ) {
		return;
	}
	$preview = drop_print_sku_migration_cached_preview();
	if ( ! is_array( $preview ) || empty( $preview['products'][ $product_id ] ) ) {
		return;
	}
	$product = $preview['products'][ $product_id ];
	$ready     = 0;
	$attention = 0;
	foreach ( $product['variations'] as $row ) {
		if ( 'ready' === $row['status'] ) {
			$ready++;
		} elseif ( 'attention' === $row['status'] ) {
			$attention++;
		}
	}
	if ( ( $ready + $attention ) <= 0 ) {
		return;
	}
	?>
	<div class="notice notice-info">
		<p>
			<?php
			printf(
				esc_html__( 'This product has %d variation(s) still using a 1.x paper code.', 'drop-print' ),
				(int) ( $ready + $attention )
			);
			?>
			<a href="<?php echo esc_url( drop_print_sku_migration_page_url() ); ?>" class="button button-secondary" style="margin-left:8px;">
				<?php esc_html_e( 'Review migration', 'drop-print' ); ?>
			</a>
		</p>
	</div>
	<?php
}
add_action( 'admin_notices', 'drop_print_sku_migration_product_edit_notice' );
function drop_print_sku_migration_ajax_alternatives() {
	check_ajax_referer( 'drop_print_sku_migration_alternatives', 'nonce' );
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( array( 'message' => __( 'You do not have sufficient permissions to do this.', 'drop-print' ) ), 403 );
	}
	if ( ! function_exists( 'drop_print_sku_migration_fetch_alternative_papers' ) ) {
		wp_send_json_error( array( 'message' => __( 'Not available.', 'drop-print' ) ), 500 );
	}
	$papers = drop_print_sku_migration_fetch_alternative_papers();
	if ( is_wp_error( $papers ) ) {
		wp_send_json_error( array( 'message' => $papers->get_error_message() ), 502 );
	}
	wp_send_json_success( array( 'papers' => $papers ) );
}
add_action( 'wp_ajax_drop_print_sku_migration_alternatives', 'drop_print_sku_migration_ajax_alternatives' );
function drop_print_sku_migration_ajax_resolve_override() {
	check_ajax_referer( 'drop_print_sku_migration_alternatives', 'nonce' );
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( array( 'message' => __( 'You do not have sufficient permissions to do this.', 'drop-print' ) ), 403 );
	}
	$sku          = isset( $_POST['sku'] ) ? sanitize_text_field( wp_unslash( $_POST['sku'] ) ) : '';
	$canonical_id = isset( $_POST['canonical_id'] ) ? absint( $_POST['canonical_id'] ) : 0;
	if ( '' === $sku || $canonical_id <= 0 ) {
		wp_send_json_error( array( 'message' => __( 'A Drop Print code and a paper are both required.', 'drop-print' ) ), 400 );
	}
	if ( ! function_exists( 'drop_print_sku_migration_resolve_with_override' ) ) {
		wp_send_json_error( array( 'message' => __( 'Not available.', 'drop-print' ) ), 500 );
	}
	$result = drop_print_sku_migration_resolve_with_override( $sku, $canonical_id );
	if ( is_wp_error( $result ) ) {
		wp_send_json_error( array( 'message' => $result->get_error_message() ), 502 );
	}
	wp_send_json_success( $result );
}
add_action( 'wp_ajax_drop_print_sku_migration_resolve_override', 'drop_print_sku_migration_ajax_resolve_override' );
function drop_print_sku_migration_ajax_resolve_override_batch() {
	check_ajax_referer( 'drop_print_sku_migration_alternatives', 'nonce' );
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( array( 'message' => __( 'You do not have sufficient permissions to do this.', 'drop-print' ) ), 403 );
	}
	if ( isset( $_POST['skus_json'] ) ) {
		$decoded = json_decode( wp_unslash( $_POST['skus_json'] ), true );
		$skus    = is_array( $decoded ) ? array_map( 'sanitize_text_field', $decoded ) : array();
	} else {
		$skus = isset( $_POST['skus'] ) && is_array( $_POST['skus'] )
			? array_map( 'sanitize_text_field', wp_unslash( $_POST['skus'] ) )
			: array();
	}
	$canonical_id = isset( $_POST['canonical_id'] ) ? absint( $_POST['canonical_id'] ) : 0;
	if ( empty( $skus ) || $canonical_id <= 0 ) {
		wp_send_json_error( array( 'message' => __( 'At least one Drop Print code and a paper are both required.', 'drop-print' ) ), 400 );
	}
	if ( ! function_exists( 'drop_print_sku_migration_resolve_with_override_batch' ) ) {
		wp_send_json_error( array( 'message' => __( 'Not available.', 'drop-print' ) ), 500 );
	}
	$result = drop_print_sku_migration_resolve_with_override_batch( $skus, $canonical_id );
	if ( is_wp_error( $result ) ) {
		wp_send_json_error( array( 'message' => $result->get_error_message() ), 502 );
	}
	wp_send_json_success( array( 'resolved' => $result ) );
}
add_action( 'wp_ajax_drop_print_sku_migration_resolve_override_batch', 'drop_print_sku_migration_ajax_resolve_override_batch' );
function drop_print_sku_migration_ajax_apply_chunk() {
	check_ajax_referer( 'drop_print_sku_migration_run' );
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( array( 'message' => __( 'You do not have sufficient permissions to do this.', 'drop-print' ) ), 403 );
	}
	if ( ! function_exists( 'drop_print_sku_migration_apply_chunk' ) ) {
		wp_send_json_error( array( 'message' => __( 'Not available.', 'drop-print' ) ), 500 );
	}
	$payload = isset( $_POST['payload'] ) ? json_decode( wp_unslash( $_POST['payload'] ), true ) : null;
	if ( ! is_array( $payload ) || empty( $payload['variation_ids'] ) || ! is_array( $payload['variation_ids'] ) ) {
		wp_send_json_error( array( 'message' => __( 'Malformed chunk payload.', 'drop-print' ) ), 400 );
	}
	$variation_ids = array_map( 'absint', $payload['variation_ids'] );
	$selected      = isset( $payload['selected_variation_ids'] ) && is_array( $payload['selected_variation_ids'] )
		? array_map( 'absint', $payload['selected_variation_ids'] )
		: array();
	$normalise_map = static function ( $raw, $cast ) {
		$out = array();
		if ( is_array( $raw ) ) {
			foreach ( $raw as $k => $v ) {
				$out[ absint( $k ) ] = $cast( $v );
			}
		}
		return $out;
	};
	$expected_old  = $normalise_map( $payload['expected_old_sku'] ?? array(), 'sanitize_text_field' );
	$expected_new  = $normalise_map( $payload['expected_new_sku'] ?? array(), 'sanitize_text_field' );
	$override      = $normalise_map( $payload['override_canonical_id'] ?? array(), 'absint' );
	$result = drop_print_sku_migration_apply_chunk( $variation_ids, $selected, $expected_old, $expected_new, $override );
	if ( is_wp_error( $result ) ) {
		wp_send_json_error( array( 'message' => $result->get_error_message() ), 502 );
	}
	wp_send_json_success( $result );
}
add_action( 'wp_ajax_drop_print_sku_migration_apply_chunk', 'drop_print_sku_migration_ajax_apply_chunk' );
function drop_print_sku_migration_ajax_finalize_run() {
	check_ajax_referer( 'drop_print_sku_migration_run' );
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_send_json_error( array( 'message' => __( 'You do not have sufficient permissions to do this.', 'drop-print' ) ), 403 );
	}
	$summary = isset( $_POST['summary'] ) ? json_decode( wp_unslash( $_POST['summary'] ), true ) : null;
	if ( ! is_array( $summary ) ) {
		wp_send_json_error( array( 'message' => __( 'Malformed run summary.', 'drop-print' ) ), 400 );
	}
	delete_transient( 'drop_print_sku_migration_cached_preview' );
	set_transient( 'drop_print_sku_migration_run_result', $summary, 15 * MINUTE_IN_SECONDS );
	wp_send_json_success( array( 'redirect' => add_query_arg( 'applied', '1', drop_print_sku_migration_page_url() ) ) );
}
add_action( 'wp_ajax_drop_print_sku_migration_finalize_run', 'drop_print_sku_migration_ajax_finalize_run' );
function drop_print_sku_migration_handle_download_preview_csv() {
	if ( ! drop_print_sku_migration_is_current() || empty( $_GET['dp_download_preview'] ) ) {
		return;
	}
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You do not have sufficient permissions to do this.', 'drop-print' ) );
	}
	check_admin_referer( 'drop_print_sku_migration_download_preview' );
	if ( ! function_exists( 'drop_print_sku_migration_build_preview' ) ) {
		wp_die( esc_html__( 'Not available.', 'drop-print' ) );
	}
	$preview = drop_print_sku_migration_build_preview();
	if ( is_wp_error( $preview ) ) {
		wp_die( esc_html( $preview->get_error_message() ) );
	}
	nocache_headers();
	header( 'Content-Type: text/csv; charset=utf-8' );
	header( 'Content-Disposition: attachment; filename="drop-print-migration-preview-' . gmdate( 'Y-m-d-His' ) . '.csv"' );
	$out = fopen( 'php://output', 'w' ); 
	fputcsv( $out, array( 'product', 'size', 'old_drop_print_code', 'new_drop_print_code', 'target_paper', 'status' ) );
	foreach ( $preview['products'] as $product ) {
		foreach ( $product['variations'] as $row ) {
			fputcsv(
				$out,
				array(
					$product['product_name'],
					function_exists( 'drop_print_sku_migration_display_size_label' )
						? drop_print_sku_migration_display_size_label( $row['size_label'] )
						: $row['size_label'],
					$row['old_sku'],
					$row['new_sku'] ?? '',
					$row['canonical_name'] ?? '',
					$row['status'],
				)
			);
		}
	}
	fclose( $out ); 
	exit;
}
add_action( 'admin_init', 'drop_print_sku_migration_handle_download_preview_csv' );
function drop_print_sku_migration_handle_post() {
	if ( ! drop_print_sku_migration_is_current() || 'POST' !== ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) {
		return;
	}
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You do not have sufficient permissions to do this.', 'drop-print' ) );
	}
	check_admin_referer( 'drop_print_sku_migration_run' );
	$preview = drop_print_sku_migration_build_preview();
	if ( is_wp_error( $preview ) ) {
		set_transient( 'drop_print_sku_migration_run_result', array( 'error' => $preview->get_error_message() ), 5 * MINUTE_IN_SECONDS );
		wp_safe_redirect( drop_print_sku_migration_page_url() );
		exit;
	}
	$selected = isset( $_POST['variation_ids'] ) && is_array( $_POST['variation_ids'] )
		? array_map( 'absint', wp_unslash( $_POST['variation_ids'] ) )
		: array();
	$expected_old_sku = isset( $_POST['variation_expected_old_sku'] ) && is_array( $_POST['variation_expected_old_sku'] )
		? array_map( 'sanitize_text_field', wp_unslash( $_POST['variation_expected_old_sku'] ) )
		: array();
	$expected_new_sku = isset( $_POST['variation_expected_new_sku'] ) && is_array( $_POST['variation_expected_new_sku'] )
		? array_map( 'sanitize_text_field', wp_unslash( $_POST['variation_expected_new_sku'] ) )
		: array();
	$override_canonical_id = isset( $_POST['variation_override_canonical_id'] ) && is_array( $_POST['variation_override_canonical_id'] )
		? array_map( 'absint', wp_unslash( $_POST['variation_override_canonical_id'] ) )
		: array();
	list( $selected, $dropped_stale ) = drop_print_sku_migration_filter_stale_selections(
		$preview,
		$selected,
		$expected_old_sku,
		$expected_new_sku,
		$override_canonical_id
	);
	$result                       = drop_print_sku_migration_apply( $preview, $selected );
	$result['dropped_stale']      = $dropped_stale;
	$result['unresolved_families'] = $preview['counts']['unresolved_families'] ?? array();
	delete_transient( 'drop_print_sku_migration_cached_preview' );
	set_transient( 'drop_print_sku_migration_run_result', $result, 15 * MINUTE_IN_SECONDS );
	wp_safe_redirect( add_query_arg( 'applied', '1', drop_print_sku_migration_page_url() ) );
	exit;
}
add_action( 'admin_init', 'drop_print_sku_migration_handle_post' );
function drop_print_sku_migration_display_size_label( string $size_label ): string {
	$separators = array( '&#8211;', "\xe2\x80\x93" ); 
	foreach ( $separators as $sep ) {
		$needle = ' ' . $sep . ' ';
		$first  = strpos( $size_label, $needle );
		$last   = strrpos( $size_label, $needle );
		if ( false !== $first && false !== $last && $first !== $last ) {
			$start = $first + strlen( $needle );
			return trim( substr( $size_label, $start, $last - $start ) );
		}
	}
	return $size_label; 
}
function drop_print_sku_migration_status_pill( string $status ): string {
	$map = array(
		'ready'     => array( 'ready', __( 'Ready', 'drop-print' ) ),
		'attention' => array( 'attention', __( 'Needs attention', 'drop-print' ) ),
		'current'   => array( 'current', __( 'Already current', 'drop-print' ) ),
	);
	list( $class, $label ) = $map[ $status ] ?? array( 'attention', $status );
	return '<span class="pill ' . esc_attr( $class ) . '">' . esc_html( $label ) . '</span>';
}
function drop_print_sku_migration_render_page() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'drop-print' ) );
	}
	if ( ! empty( $_GET['applied'] ) ) {
		drop_print_sku_migration_render_done_screen();
		return;
	}
	$preview = drop_print_sku_migration_build_preview();
	?>
	<div class="dp-skumig">
		<header class="dp-skumig__head">
			<div>
				<p class="dp-skumig__kicker"><?php esc_html_e( 'Drop Print · SKU Migration · Review before running', 'drop-print' ); ?></p>
				<h1><?php esc_html_e( 'Drop Print migration review', 'drop-print' ); ?></h1>
				<p class="dp-skumig__sub"><?php esc_html_e( "Every product below still carries a 1.x paper code. Nothing changes until you press Run — this is a preview of exactly what each product's Drop Print code will become.", 'drop-print' ); ?></p>
			</div>
			<a href="<?php echo esc_url( admin_url() ); ?>" class="dp-skumig__close" aria-label="<?php esc_attr_e( 'Close', 'drop-print' ); ?>">&times;</a>
		</header>
		<?php if ( is_wp_error( $preview ) ) : ?>
			<div class="dp-skumig__error">
				<p><strong><?php esc_html_e( 'Could not reach the server to check your products.', 'drop-print' ); ?></strong></p>
				<p><?php echo esc_html( $preview->get_error_message() ); ?></p>
			</div>
			<?php return; ?>
		<?php endif; ?>
		<?php
		$counts = $preview['counts'];
		if ( 0 === $counts['variations'] ) :
			?>
			<div class="dp-skumig__empty">
				<p><?php esc_html_e( 'Nothing to migrate — every Drop Print product already uses a current-format Drop Print code.', 'drop-print' ); ?></p>
			</div>
			<?php
			return;
		endif;
		?>
		<div class="stats">
			<div class="stat"><div class="n"><?php echo esc_html( $counts['products'] ); ?></div><div class="l"><?php esc_html_e( 'Products scanned', 'drop-print' ); ?></div></div>
			<div class="stat"><div class="n"><?php echo esc_html( $counts['variations'] ); ?></div><div class="l"><?php esc_html_e( 'Variations found', 'drop-print' ); ?></div></div>
			<div class="stat ready"><div class="n"><?php echo esc_html( $counts['ready'] ); ?></div><div class="l"><?php esc_html_e( 'Ready to migrate', 'drop-print' ); ?></div></div>
			<div class="stat attention"><div class="n"><?php echo esc_html( $counts['attention'] ); ?></div><div class="l"><?php esc_html_e( 'Needs attention', 'drop-print' ); ?></div></div>
			<div class="stat"><div class="n"><?php echo esc_html( $counts['current'] ); ?></div><div class="l"><?php esc_html_e( 'Already current', 'drop-print' ); ?></div></div>
		</div>
		<div class="dp-skumig__notice">
			<p>
				<?php esc_html_e( 'Approving any row on a product rebuilds ALL of that product\'s Drop Print variations, not just the ones you check — this is how Drop Print has always saved a product\'s variations, the migration doesn\'t change it. Every variation on an affected product gets a new internal ID. Unchecking a row keeps its Drop Print code, but the variation itself is still rebuilt: sale prices, stock settings, per-variation images, or anything else set outside Drop Print are not carried over.', 'drop-print' ); ?>
			</p>
		</div>
		<form method="post" action="<?php echo esc_url( drop_print_sku_migration_page_url() ); ?>" id="dp-skumig-form">
			<?php wp_nonce_field( 'drop_print_sku_migration_run' ); ?>
			<?php ?>
			<input type="hidden" id="dp-skumig-alt-nonce" value="<?php echo esc_attr( wp_create_nonce( 'drop_print_sku_migration_alternatives' ) ); ?>">
			<section class="card">
				<div class="toolbar">
					<label class="select-all">
						<input type="checkbox" id="dp-skumig-select-all" checked>
						<?php esc_html_e( 'Select all ready rows', 'drop-print' ); ?>
					</label>
					<?php  ?>
					<label class="select-all dp-skumig-sku-toggle">
						<input type="checkbox" id="dp-skumig-show-skus">
						<?php esc_html_e( 'Show Drop Print codes', 'drop-print' ); ?>
					</label>
				</div>
				<div class="table-scroll">
					<table id="dp-skumig-table">
						<thead>
							<tr>
								<th class="check"></th>
								<th><?php esc_html_e( 'Product', 'drop-print' ); ?></th>
								<th><?php esc_html_e( 'Size', 'drop-print' ); ?></th>
								<th class="dp-skumig-sku-col"><?php esc_html_e( 'Drop Print code', 'drop-print' ); ?></th>
								<th><?php esc_html_e( 'Target paper', 'drop-print' ); ?></th>
								<th><?php esc_html_e( 'Status', 'drop-print' ); ?></th>
							</tr>
						</thead>
						<tbody>
							<?php foreach ( $preview['products'] as $product ) : ?>
								<?php
								$shown_paper_trigger_for_product = false;
								?>
								<?php foreach ( $product['variations'] as $i => $row ) : ?>
									<tr class="<?php echo 0 === $i ? 'group-start' : ''; ?> <?php echo 'ready' !== $row['status'] ? 'dim' : ''; ?>" data-product-id="<?php echo esc_attr( $product['product_id'] ); ?>" data-variation-row="<?php echo esc_attr( $row['variation_id'] ); ?>">
										<td class="check">
											<?php if ( 'ready' === $row['status'] ) : ?>
												<input type="checkbox" class="dp-skumig-row-check" name="variation_ids[]" value="<?php echo esc_attr( $row['variation_id'] ); ?>" checked>
												<input type="hidden" class="dp-skumig-expected-old" name="variation_expected_old_sku[<?php echo esc_attr( $row['variation_id'] ); ?>]" value="<?php echo esc_attr( $row['old_sku'] ); ?>">
												<input type="hidden" class="dp-skumig-expected-new" name="variation_expected_new_sku[<?php echo esc_attr( $row['variation_id'] ); ?>]" value="<?php echo esc_attr( $row['new_sku'] ); ?>">
												<?php ?>
												<input type="hidden" class="dp-skumig-override-id" name="variation_override_canonical_id[<?php echo esc_attr( $row['variation_id'] ); ?>]" value="">
											<?php endif; ?>
										</td>
										<td class="product-cell">
											<span class="product-name"><a href="<?php echo esc_url( $product['edit_url'] ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $product['product_name'] ); ?></a></span>
										</td>
										<td class="size-pill"><?php echo esc_html( drop_print_sku_migration_display_size_label( $row['size_label'] ) ); ?></td>
										<td class="dp-skumig-sku-col">
											<span class="sku-flow">
												<span class="sku-old <?php echo 'attention' === $row['status'] ? 'unresolved' : ''; ?>"><?php echo esc_html( $row['old_sku'] ); ?></span>
												<?php if ( $row['new_sku'] ) : ?>
													<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
													<span class="sku-new"><?php echo esc_html( $row['new_sku'] ); ?></span>
												<?php endif; ?>
											</span>
										</td>
										<td class="target-paper dp-skumig-target-paper" data-variation-id="<?php echo esc_attr( $row['variation_id'] ); ?>" data-product-id="<?php echo esc_attr( $product['product_id'] ); ?>" data-default-canonical-id="<?php echo esc_attr( $row['canonical_id'] ); ?>" data-default-canonical-name="<?php echo esc_attr( $row['canonical_name'] ); ?>" data-default-new-sku="<?php echo esc_attr( $row['new_sku'] ); ?>">
											<?php if ( $row['canonical_name'] ) : ?>
												<span class="dp-skumig-target-paper-name"><?php echo esc_html( $row['canonical_name'] ); ?></span> <span class="id">#<?php echo esc_html( $row['canonical_id'] ); ?></span>
											<?php else : ?>
												&mdash;
											<?php endif; ?>
											<?php if ( ! empty( $row['substitution_reason'] ) && ! $shown_paper_trigger_for_product ) : ?>
												<?php
												$shown_paper_trigger_for_product = true;
												?>
												<button
													type="button"
													class="dp-skumig-choose-paper"
													data-variation-id="<?php echo esc_attr( $row['variation_id'] ); ?>"
													data-product-id="<?php echo esc_attr( $product['product_id'] ); ?>"
													data-old-sku="<?php echo esc_attr( $row['old_sku'] ); ?>"
													data-default-canonical-id="<?php echo esc_attr( $row['canonical_id'] ); ?>"
													data-default-canonical-name="<?php echo esc_attr( $row['canonical_name'] ); ?>"
													data-default-new-sku="<?php echo esc_attr( $row['new_sku'] ); ?>"
													data-reason="<?php echo esc_attr( $row['substitution_reason'] ); ?>"
													aria-label="<?php esc_attr_e( 'Why this paper? Choose a different one for this product.', 'drop-print' ); ?>"
												>i</button>
											<?php endif; ?>
										</td>
										<td><?php echo wp_kses_post( drop_print_sku_migration_status_pill( $row['status'] ) ); ?></td>
									</tr>
									<?php if ( $row['note'] ) : ?>
										<tr><td colspan="6" class="row-note"><?php echo esc_html( $row['note'] ); ?></td></tr>
									<?php endif; ?>
								<?php endforeach; ?>
							<?php endforeach; ?>
						</tbody>
					</table>
				</div>
			</section>
			<?php 
			?>
			<input type="hidden" id="dp-skumig-unresolved-families" value="<?php echo esc_attr( wp_json_encode( $counts['unresolved_families'] ?? array() ) ); ?>">
			<div class="action-bar">
				<div class="action-inner" id="dp-skumig-action-idle">
					<div class="action-text">
						<span id="dp-skumig-selected-count"><?php echo esc_html( $counts['ready'] ); ?></span> <?php esc_html_e( 'of', 'drop-print' ); ?> <?php echo esc_html( $counts['ready'] ); ?> <?php esc_html_e( 'selected · a backup is written before each batch changes', 'drop-print' ); ?>
					</div>
					<div class="action-buttons">
						<?php
						$download_url = wp_nonce_url(
							add_query_arg( 'dp_download_preview', '1', drop_print_sku_migration_page_url() ),
							'drop_print_sku_migration_download_preview'
						);
						?>
						<a href="<?php echo esc_url( $download_url ); ?>" class="button-plain dp-skumig-download" title="<?php esc_attr_e( 'Download a CSV of this preview', 'drop-print' ); ?>" aria-label="<?php esc_attr_e( 'Download a CSV of this preview', 'drop-print' ); ?>">
							<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3v12m0 0l-4-4m4 4l4-4M4 19h16"/></svg>
						</a>
						<a href="<?php echo esc_url( admin_url() ); ?>" class="button-plain"><?php esc_html_e( 'Cancel', 'drop-print' ); ?></a>
						<button type="submit" class="primary" id="dp-skumig-run-button" <?php disabled( 0 === $counts['ready'] ); ?>>
							<?php esc_html_e( 'Run migration', 'drop-print' ); ?>
						</button>
					</div>
				</div>
			</div>
		</form>
		<?php 
		?>
		<div id="dp-skumig-run-progress-overlay" class="dp-skumig-modal-overlay" hidden>
			<div class="dp-skumig-modal dp-skumig-run-progress-modal" role="alertdialog" aria-modal="true" aria-labelledby="dp-skumig-run-progress-title">
				<h2 id="dp-skumig-run-progress-title"><?php esc_html_e( 'Migrating your products', 'drop-print' ); ?></h2>
				<p id="dp-skumig-progress-text"><?php esc_html_e( 'Starting…', 'drop-print' ); ?></p>
				<div class="dp-skumig-progress-bar"><div class="dp-skumig-progress-fill" id="dp-skumig-progress-fill"></div></div>
				<p class="dp-skumig-run-progress-note"><?php esc_html_e( 'Please keep this browser window open until this finishes - closing it now would leave the run part-way done.', 'drop-print' ); ?></p>
			</div>
		</div>
		<?php  ?>
		<div id="dp-skumig-substitution-modal" class="dp-skumig-modal-overlay" hidden>
			<div class="dp-skumig-modal" role="dialog" aria-modal="true" aria-labelledby="dp-skumig-modal-title">
				<button type="button" class="dp-skumig-modal-close" id="dp-skumig-modal-close" aria-label="<?php esc_attr_e( 'Close', 'drop-print' ); ?>">&times;</button>
				<h2 id="dp-skumig-modal-title"><?php esc_html_e( 'This product is changing paper', 'drop-print' ); ?></h2>
				<p id="dp-skumig-modal-reason"></p>
				<div class="dp-skumig-modal-actions">
					<button type="button" class="primary" id="dp-skumig-modal-accept"></button>
					<button type="button" class="button-plain" id="dp-skumig-modal-show-alternatives"><?php esc_html_e( 'Choose a different paper', 'drop-print' ); ?></button>
				</div>
				<div id="dp-skumig-modal-alternatives" class="dp-skumig-modal-alternatives" hidden>
					<p class="dp-skumig-modal-loading" id="dp-skumig-modal-loading"><?php esc_html_e( 'Loading papers…', 'drop-print' ); ?></p>
					<ul id="dp-skumig-modal-paper-list"></ul>
				</div>
				<?php  ?>
				<div id="dp-skumig-modal-confirm" class="dp-skumig-modal-alternatives" hidden>
					<p id="dp-skumig-modal-confirm-text"></p>
					<div class="dp-skumig-modal-actions">
						<button type="button" class="primary" id="dp-skumig-modal-confirm-one"></button>
						<button type="button" class="button-plain" id="dp-skumig-modal-confirm-bulk" hidden></button>
						<button type="button" class="button-plain" id="dp-skumig-modal-confirm-back"><?php esc_html_e( 'Back', 'drop-print' ); ?></button>
					</div>
				</div>
			</div>
		</div>
	</div>
	<script>
	(function () {
		var selectAll = document.getElementById('dp-skumig-select-all');
		var count = document.getElementById('dp-skumig-selected-count');
		var boxes = function () { return document.querySelectorAll('.dp-skumig-row-check'); };
		function updateCount() {
			var n = 0;
			boxes().forEach(function (b) { if (b.checked) n++; });
			if (count) { count.textContent = n; }
		}
		if (selectAll) {
			selectAll.addEventListener('change', function () {
				boxes().forEach(function (b) { b.checked = selectAll.checked; });
				updateCount();
			});
		}
		boxes().forEach(function (b) { b.addEventListener('change', updateCount); });
		// doc 47 B1 - off by default; the toggle only ever adds a class, the
		// underlying data (CSV/JSON backups, the hidden expected-mapping
		// fields the TOCTOU guard reads) is unaffected either way.
		var table = document.getElementById('dp-skumig-table');
		var showSkus = document.getElementById('dp-skumig-show-skus');
		if (table && showSkus) {
			showSkus.addEventListener('change', function () {
				table.classList.toggle('dp-skumig-show-skus', showSkus.checked);
			});
		}
		// doc client/47 §3.4 - the substitution modal: explain the swap, then
		// let the merchant accept it or pick a different paper. One shared
		// modal element, populated per row from that row's own data-*
		// attributes (no round trip needed just to open it).
		var modal = document.getElementById('dp-skumig-substitution-modal');
		var modalReason = document.getElementById('dp-skumig-modal-reason');
		var modalAccept = document.getElementById('dp-skumig-modal-accept');
		var modalShowAlt = document.getElementById('dp-skumig-modal-show-alternatives');
		var modalAltWrap = document.getElementById('dp-skumig-modal-alternatives');
		var modalLoading = document.getElementById('dp-skumig-modal-loading');
		var modalList = document.getElementById('dp-skumig-modal-paper-list');
		var modalClose = document.getElementById('dp-skumig-modal-close');
		var altNonce = document.getElementById('dp-skumig-alt-nonce');
		var currentRow = null;
		var papersCache = null; // fetched once per page load, reused across rows
		// Restored 2026-09-01 - bulk-apply ACROSS PRODUCTS site-wide is a
		// separate capability from "one paper per product" (which is now the
		// default, unconditional behaviour of applyPaperToProduct() below).
		var confirmWrap = document.getElementById('dp-skumig-modal-confirm');
		var confirmText = document.getElementById('dp-skumig-modal-confirm-text');
		var confirmOne = document.getElementById('dp-skumig-modal-confirm-one');
		var confirmBulk = document.getElementById('dp-skumig-modal-confirm-bulk');
		var confirmBack = document.getElementById('dp-skumig-modal-confirm-back');
		var pendingPaper = null;
		var pendingMatchingProductIds = [];
		function closeModal() {
			if (modal) { modal.hidden = true; }
			currentRow = null;
			pendingPaper = null;
			pendingMatchingProductIds = [];
		}
		// Owner correction, 2026-09-01: ONE paper per PRODUCT, never per size -
		// every 'ready' row belonging to the SAME product as the trigger,
		// regardless of what its own default canonical happens to be (they
		// should all already share it, but this doesn't assume that).
		function findProductRows(productId) {
			var rows = [];
			document.querySelectorAll('.dp-skumig-row-check').forEach(function (cb) {
				var tr = cb.closest('tr');
				if (!tr || tr.getAttribute('data-product-id') !== String(productId)) { return; }
				rows.push({ variationId: tr.getAttribute('data-variation-row'), oldSku: tr.querySelector('.dp-skumig-expected-old') ? tr.querySelector('.dp-skumig-expected-old').value : null });
			});
			return rows;
		}
		// Every OTHER product whose paper-choice trigger still shows the SAME
		// default canonical as the current one (site-wide, not just this
		// page's visible rows - the table already lists every Drop Print
		// product on load) - the unit here is the PRODUCT, matching the "one
		// paper per product" model, not the variation.
		function findMatchingProducts(defaultCanonicalId) {
			var ids = [];
			document.querySelectorAll('.dp-skumig-choose-paper').forEach(function (btn) {
				if (btn.getAttribute('data-default-canonical-id') !== String(defaultCanonicalId)) { return; }
				var productId = btn.getAttribute('data-product-id');
				if (productId && ids.indexOf(productId) === -1) { ids.push(productId); }
			});
			return ids;
		}
		function openModalForRow(trigger) {
			currentRow = {
				productId: trigger.getAttribute('data-product-id'),
				variationId: trigger.getAttribute('data-variation-id'),
				oldSku: trigger.getAttribute('data-old-sku'),
				defaultCanonicalId: trigger.getAttribute('data-default-canonical-id'),
				defaultCanonicalName: trigger.getAttribute('data-default-canonical-name'),
				defaultNewSku: trigger.getAttribute('data-default-new-sku'),
				reason: trigger.getAttribute('data-reason')
			};
			if (modalReason) { modalReason.textContent = currentRow.reason; }
			if (modalAccept) { modalAccept.textContent = 'Keep ' + currentRow.defaultCanonicalName; }
			if (modalAltWrap) { modalAltWrap.hidden = true; }
			if (modalList) { modalList.innerHTML = ''; }
			if (confirmWrap) { confirmWrap.hidden = true; }
			pendingPaper = null;
			pendingMatchingProductIds = [];
			if (modal) { modal.hidden = false; }
		}
		document.querySelectorAll('.dp-skumig-choose-paper').forEach(function (btn) {
			btn.addEventListener('click', function () { openModalForRow(btn); });
		});
		if (modalClose) { modalClose.addEventListener('click', closeModal); }
		if (modal) {
			modal.addEventListener('click', function (e) {
				if (e.target === modal) { closeModal(); } // overlay itself, not the box
			});
		}
		if (modalAccept) {
			modalAccept.addEventListener('click', function () {
				// "Keep the default" must be idempotent - if this PRODUCT
				// already has an override from an earlier visit to this modal
				// (open, choose, reopen, change your mind), Accept has to CLEAR
				// it on EVERY one of the product's rows, not just close the
				// modal leaving stale override data behind. Each row carries
				// its OWN default (data-default-* on .dp-skumig-target-paper,
				// not just the trigger's) since sizes differ row to row even
				// though the paper is the same.
				if (currentRow) {
					document.querySelectorAll('.dp-skumig-target-paper[data-product-id="' + currentRow.productId + '"]').forEach(function (cell) {
						var variationId = cell.getAttribute('data-variation-id');
						var canonicalId = cell.getAttribute('data-default-canonical-id');
						var canonicalName = cell.getAttribute('data-default-canonical-name');
						var newSku = cell.getAttribute('data-default-new-sku');
						if (variationId) { updateRowDisplay(variationId, canonicalId, canonicalName, newSku, false); }
					});
				}
				closeModal();
			});
		}
		function fetchAlternatives(onDone) {
			if (papersCache) { onDone(papersCache); return; }
			if (modalLoading) { modalLoading.hidden = false; }
			var body = new URLSearchParams();
			body.set('action', 'drop_print_sku_migration_alternatives');
			body.set('nonce', altNonce ? altNonce.value : '');
			fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: body })
				.then(function (r) { return r.json(); })
				.then(function (data) {
					if (modalLoading) { modalLoading.hidden = true; }
					if (data && data.success && data.data && Array.isArray(data.data.papers)) {
						papersCache = data.data.papers;
						onDone(papersCache);
					} else if (modalList) {
						modalList.innerHTML = '<li>' + ((data && data.data && data.data.message) || 'Could not load papers.') + '</li>';
					}
				})
				.catch(function () {
					if (modalLoading) { modalLoading.hidden = true; }
					if (modalList) { modalList.innerHTML = '<li>Could not load papers.</li>'; }
				});
		}
		function renderPaperList(papers) {
			if (!modalList) { return; }
			modalList.innerHTML = '';
			papers.forEach(function (paper) {
				var li = document.createElement('li');
				var btn = document.createElement('button');
				btn.type = 'button';
				btn.className = 'dp-skumig-modal-paper-option';
				var bits = [paper.name];
				if (paper.gsm) { bits.push(paper.gsm + 'gsm'); }
				if (paper.finish) { bits.push(paper.finish); }
				btn.textContent = bits.join(' · ');
				if (String(paper.canonical_id) === String(currentRow.defaultCanonicalId)) {
					btn.disabled = true;
					btn.textContent += ' (current)';
				} else {
					btn.addEventListener('click', function () { showConfirmStep(paper); });
				}
				li.appendChild(btn);
				modalList.appendChild(li);
			});
		}
		if (modalShowAlt) {
			modalShowAlt.addEventListener('click', function () {
				if (modalAltWrap) { modalAltWrap.hidden = false; }
				fetchAlternatives(renderPaperList);
			});
		}
		// Restored 2026-09-01: bulk-apply is still real, just re-scoped to
		// the PRODUCT as its unit instead of the variation - "this product"
		// (the default, one paper for all its sizes) vs "every OTHER product
		// still defaulting to the same paper, site-wide".
		function showConfirmStep(paper) {
			if (!currentRow) { return; }
			pendingPaper = paper;
			pendingMatchingProductIds = findMatchingProducts(currentRow.defaultCanonicalId);
			if (modalAltWrap) { modalAltWrap.hidden = true; }
			if (confirmWrap) { confirmWrap.hidden = false; }
			if (confirmText) { confirmText.textContent = 'Use ' + paper.name + ' for:'; }
			if (confirmOne) { confirmOne.textContent = 'This product only'; }
			if (confirmBulk) {
				if (pendingMatchingProductIds.length > 1) {
					confirmBulk.hidden = false;
					confirmBulk.textContent = 'All ' + pendingMatchingProductIds.length + ' products still on ' + currentRow.defaultCanonicalName;
				} else {
					confirmBulk.hidden = true;
				}
			}
		}
		if (confirmBack) {
			confirmBack.addEventListener('click', function () {
				if (confirmWrap) { confirmWrap.hidden = true; }
				if (modalAltWrap) { modalAltWrap.hidden = false; }
			});
		}
		if (confirmOne) {
			confirmOne.addEventListener('click', function () {
				if (pendingPaper && currentRow) { applyPaperToProduct(pendingPaper, [currentRow.productId]); }
			});
		}
		if (confirmBulk) {
			confirmBulk.addEventListener('click', function () {
				if (pendingPaper) { applyPaperToProduct(pendingPaper, pendingMatchingProductIds); }
			});
		}
		// A size the chosen paper genuinely cannot cover is NOT a failure to
		// work around here; it gets a plain inline warning on that row
		// instead (see markRowUnavailable() below) - there is no per-row
		// fix, only a flag for the merchant to see (owner's own instruction).
		function applyPaperToProduct(paper, productIds) {
			if (!productIds || !productIds.length) { return; }
			var rows = [];
			productIds.forEach(function (productId) { rows = rows.concat(findProductRows(productId)); });
			if (!rows.length) { return; }
			var body = new URLSearchParams();
			body.set('action', 'drop_print_sku_migration_resolve_override_batch');
			body.set('nonce', altNonce ? altNonce.value : '');
			var skus = [];
			rows.forEach(function (row) { if (row.oldSku) { skus.push(row.oldSku); } });
			body.set('skus_json', JSON.stringify(skus));
			body.set('canonical_id', paper.canonical_id);
			fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: body })
				.then(function (r) { return r.json(); })
				.then(function (data) {
					if (!data || !data.success || !data.data || !data.data.resolved) {
						window.alert((data && data.data && data.data.message) || 'Could not change the paper - try again.');
						return;
					}
					var resolved = data.data.resolved;
					var unavailable = 0;
					rows.forEach(function (row) {
						var r = row.oldSku ? resolved[row.oldSku] : null;
						if (r && r.new_sku) {
							updateRowDisplay(row.variationId, r.canonical_id || paper.canonical_id, r.canonical_name || paper.name, r.new_sku, true);
						} else {
							// No per-row fix - just flag it, per the owner's own
							// instruction: warn, don't try to solve it here.
							markRowUnavailable(row.variationId, paper.name);
							unavailable++;
						}
					});
					closeModal();
					var productWord = productIds.length > 1 ? (productIds.length + ' products') : 'this product';
					if (unavailable > 0) {
						window.alert(unavailable + ' size(s) across ' + productWord + ' are not available on ' + paper.name + ' - flagged on those rows, left on their previous Drop Print code. Everything else now targets ' + paper.name + '.');
					}
				})
				.catch(function () {
					window.alert('Could not reach the server - try again.');
				});
		}
		// A row whose CHOSEN paper cannot produce that size: no override is
		// written for it (its hidden fields are left exactly as rendered -
		// still eligible under whatever it resolved to before), it just gets
		// a visible flag so the merchant sees it rather than it silently
		// staying on the old default.
		function markRowUnavailable(variationId, paperName) {
			var cell = document.querySelector('.dp-skumig-target-paper[data-variation-id="' + variationId + '"]');
			if (!cell) { return; }
			var mark = cell.querySelector('.dp-skumig-unavailable');
			if (!mark) {
				mark = document.createElement('div');
				mark.className = 'dp-skumig-unavailable';
				cell.appendChild(mark);
			}
			mark.textContent = 'Not available on ' + paperName + ' for this size';
		}
		// Shared by both directions - choosing an override (isOverride=true)
		// and Accept resetting back to the default (isOverride=false, called
		// with the row's OWN default values). One function so the two paths
		// can never drift into updating different fields/DOM.
		function updateRowDisplay(variationId, canonicalId, canonicalName, newSku, isOverride) {
			// doc client/47 §3.4 - these are the SAME hidden fields the TOCTOU
			// guard (client/40 W2.2) reads at submit time; writing the FRESH
			// value here (override OR default) is what lets that guard verify
			// on submit rather than reject a since-changed mapping.
			var overrideField = document.querySelector('.dp-skumig-override-id[name="variation_override_canonical_id[' + variationId + ']"]');
			var expectedNewField = document.querySelector('.dp-skumig-expected-new[name="variation_expected_new_sku[' + variationId + ']"]');
			if (overrideField) { overrideField.value = isOverride ? canonicalId : ''; }
			if (expectedNewField) { expectedNewField.value = newSku; }
			var cell = document.querySelector('.dp-skumig-target-paper[data-variation-id="' + variationId + '"]');
			if (cell) {
				var nameEl = cell.querySelector('.dp-skumig-target-paper-name');
				if (nameEl) { nameEl.textContent = canonicalName; }
				var idEl = cell.querySelector('.id');
				if (idEl) { idEl.textContent = '#' + canonicalId; }
				var mark = cell.querySelector('.dp-skumig-your-choice');
				if (isOverride) {
					if (!mark) {
						mark = document.createElement('span');
						mark.className = 'dp-skumig-your-choice';
						cell.insertBefore(mark, cell.querySelector('.dp-skumig-choose-paper'));
					}
					mark.textContent = 'your choice';
				} else if (mark) {
					mark.remove();
				}
				// A row previously flagged unavailable on a DIFFERENT chosen
				// paper is no longer stale once this row successfully resolves
				// on the current one - clear it rather than leaving an old
				// warning next to a now-correct mapping.
				var staleWarning = cell.querySelector('.dp-skumig-unavailable');
				if (staleWarning) { staleWarning.remove(); }
			}
			// The SKU-change column (behind the B1 toggle) reflects whichever
			// mapping is currently live for this row, override or default.
			var row = cell ? cell.closest('tr') : null;
			var newSkuEl = row ? row.querySelector('.sku-new') : null;
			if (newSkuEl) { newSkuEl.textContent = newSku; }
		}
		// doc client/47 §5 - chunked "Run migration". Replaces the single
		// giant form POST: verified live that PHP's max_input_vars (500 on
		// the real host, not the 1000 often assumed) silently truncates a
		// request once row-count x 4 fields passes it - 357 rows was already
		// 3x over. Runs in product-grouped batches instead; a batch never
		// splits one product's variations (see apply_chunk()'s own
		// caller-contract comment for why that would silently delete a
		// sibling variation the writer didn't know to keep).
		var CHUNK_TARGET_SIZE = 100;
		var form = document.getElementById('dp-skumig-form');
		var runButton = document.getElementById('dp-skumig-run-button');
		var actionIdle = document.getElementById('dp-skumig-action-idle');
		var runProgressOverlay = document.getElementById('dp-skumig-run-progress-overlay');
		var progressText = document.getElementById('dp-skumig-progress-text');
		var progressFill = document.getElementById('dp-skumig-progress-fill');
		var runInProgress = false;
		// A run part-way done is exactly the "silently incomplete" outcome
		// this whole chunked rebuild exists to prevent - back the polite ask
		// in the overlay with a real guard, not just wording.
		window.addEventListener('beforeunload', function (e) {
			if (!runInProgress) { return; }
			e.preventDefault();
			e.returnValue = '';
		});
		var unresolvedFamiliesField = document.getElementById('dp-skumig-unresolved-families');
		var wpNonceField = document.getElementById('_wpnonce');
		function fieldIdFromName(name) {
			var m = name.match(/\[(\d+)\]/);
			return m ? m[1] : null;
		}
		function pickForIds(map, ids) {
			var out = {};
			ids.forEach(function (id) { if (Object.prototype.hasOwnProperty.call(map, id)) { out[id] = map[id]; } });
			return out;
		}
		function collectRunPlan() {
			// Group EVERY row (checked or not) by product - a chunk touching
			// one variation of a product must carry every sibling too.
			var byProduct = {};
			document.querySelectorAll('tr[data-product-id]').forEach(function (tr) {
				var productId = tr.getAttribute('data-product-id');
				var variationId = tr.getAttribute('data-variation-row');
				if (!productId || !variationId) { return; }
				if (!byProduct[productId]) { byProduct[productId] = []; }
				byProduct[productId].push(variationId);
			});
			var selectedSet = {};
			document.querySelectorAll('.dp-skumig-row-check:checked').forEach(function (cb) { selectedSet[cb.value] = true; });
			var expectedOld = {};
			document.querySelectorAll('.dp-skumig-expected-old').forEach(function (el) {
				var id = fieldIdFromName(el.name);
				if (id) { expectedOld[id] = el.value; }
			});
			var expectedNew = {};
			document.querySelectorAll('.dp-skumig-expected-new').forEach(function (el) {
				var id = fieldIdFromName(el.name);
				if (id) { expectedNew[id] = el.value; }
			});
			var overrideMap = {};
			document.querySelectorAll('.dp-skumig-override-id').forEach(function (el) {
				var id = fieldIdFromName(el.name);
				if (id && el.value) { overrideMap[id] = el.value; }
			});
			var chunks = [];
			var current = { variationIds: [], selected: [] };
			Object.keys(byProduct).forEach(function (productId) {
				var ids = byProduct[productId];
				if (current.variationIds.length > 0 && current.variationIds.length + ids.length > CHUNK_TARGET_SIZE) {
					chunks.push(current);
					current = { variationIds: [], selected: [] };
				}
				ids.forEach(function (id) {
					current.variationIds.push(id);
					if (selectedSet[id]) { current.selected.push(id); }
				});
			});
			if (current.variationIds.length > 0) { chunks.push(current); }
			return { chunks: chunks, expectedOld: expectedOld, expectedNew: expectedNew, overrideMap: overrideMap };
		}
		function runChunk(chunk, plan) {
			var body = new URLSearchParams();
			body.set('action', 'drop_print_sku_migration_apply_chunk');
			body.set('_wpnonce', wpNonceField ? wpNonceField.value : '');
			body.set('payload', JSON.stringify({
				variation_ids: chunk.variationIds,
				selected_variation_ids: chunk.selected,
				expected_old_sku: pickForIds(plan.expectedOld, chunk.variationIds),
				expected_new_sku: pickForIds(plan.expectedNew, chunk.variationIds),
				override_canonical_id: pickForIds(plan.overrideMap, chunk.variationIds)
			}));
			return fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: body }).then(function (r) { return r.json(); });
		}
		function finalizeRun(summary) {
			var body = new URLSearchParams();
			body.set('action', 'drop_print_sku_migration_finalize_run');
			body.set('_wpnonce', wpNonceField ? wpNonceField.value : '');
			body.set('summary', JSON.stringify(summary));
			return fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: body }).then(function (r) { return r.json(); });
		}
		if (form) {
			form.addEventListener('submit', function (e) {
				e.preventDefault();
				var plan = collectRunPlan();
				var totalChunks = plan.chunks.length;
				if (totalChunks === 0) { return; }
				runInProgress = true;
				if (runButton) { runButton.disabled = true; }
				if (progressFill) { progressFill.classList.remove('is-error'); progressFill.style.width = '0%'; }
				if (runProgressOverlay) { runProgressOverlay.hidden = false; }
				var summary = {
					products_migrated: 0,
					variations_migrated: 0,
					errors: [],
					dropped_stale: [],
					backup_paths: [],
					unresolved_families: (function () {
						try { return JSON.parse((unresolvedFamiliesField && unresolvedFamiliesField.value) || '[]'); }
						catch (err) { return []; }
					})()
				};
				function progressSoFarText() {
					return 'Completed so far: ' + summary.products_migrated + ' product(s), ' + summary.variations_migrated + ' variation(s) - nothing further was run.';
				}
				function showRunError(message) {
					if (progressFill) { progressFill.classList.add('is-error'); }
					if (progressText) { progressText.textContent = message; }
					window.alert(message);
					// The run is genuinely stopped here, not paused - let the
					// merchant leave the page or retry rather than trapping
					// them behind the "keep this open" guard indefinitely.
					runInProgress = false;
					if (runButton) { runButton.disabled = false; }
					if (runProgressOverlay) { runProgressOverlay.hidden = true; }
				}
				var i = 0;
				function next() {
					if (i >= totalChunks) {
						if (progressText) { progressText.textContent = 'Finishing up…'; }
						if (progressFill) { progressFill.style.width = '100%'; }
						finalizeRun(summary).then(function (data) {
							if (data && data.success && data.data && data.data.redirect) {
								runInProgress = false;
								window.location = data.data.redirect;
							} else {
								showRunError('Could not finish the run - ' + ((data && data.data && data.data.message) || 'unknown error') + '. ' + progressSoFarText());
							}
						}).catch(function () {
							showRunError('Could not reach the server to finish the run. ' + progressSoFarText());
						});
						return;
					}
					if (progressText) { progressText.textContent = 'Migrating batch ' + (i + 1) + ' of ' + totalChunks + '…'; }
					if (progressFill) { progressFill.style.width = Math.round((i / totalChunks) * 100) + '%'; }
					runChunk(plan.chunks[i], plan).then(function (data) {
						if (!data || !data.success || !data.data) {
							// Stop, don't skip and continue - a silently
							// incomplete run is exactly what this rebuild exists
							// to prevent.
							showRunError('Batch ' + (i + 1) + ' of ' + totalChunks + ' failed: ' + ((data && data.data && data.data.message) || 'unknown error') + '. ' + progressSoFarText());
							return;
						}
						var chunkResult = data.data.apply || {};
						summary.products_migrated += chunkResult.products_migrated || 0;
						summary.variations_migrated += chunkResult.variations_migrated || 0;
						if (Array.isArray(chunkResult.errors)) { summary.errors = summary.errors.concat(chunkResult.errors); }
						if (Array.isArray(data.data.dropped_stale)) { summary.dropped_stale = summary.dropped_stale.concat(data.data.dropped_stale); }
						if (chunkResult.backup_path) {
							summary.backup_paths.push({ csv: chunkResult.backup_path, json: chunkResult.backup_json_path || null });
						}
						i++;
						next();
					}).catch(function () {
						showRunError('Batch ' + (i + 1) + ' of ' + totalChunks + ' could not reach the server. ' + progressSoFarText());
					});
				}
				next();
			});
		}
	})();
	</script>
	<?php
}
function drop_print_sku_migration_render_done_screen() {
	$result = get_transient( 'drop_print_sku_migration_run_result' );
	delete_transient( 'drop_print_sku_migration_run_result' );
	?>
	<div class="dp-skumig">
		<header class="dp-skumig__head">
			<div>
				<p class="dp-skumig__kicker"><?php esc_html_e( 'Drop Print · SKU Migration', 'drop-print' ); ?></p>
				<h1><?php esc_html_e( 'Migration complete', 'drop-print' ); ?></h1>
			</div>
			<a href="<?php echo esc_url( admin_url() ); ?>" class="dp-skumig__close" aria-label="<?php esc_attr_e( 'Close', 'drop-print' ); ?>">&times;</a>
		</header>
		<?php if ( ! is_array( $result ) ) : ?>
			<div class="dp-skumig__empty"><p><?php esc_html_e( 'Nothing to show — this page was reloaded after the run already completed.', 'drop-print' ); ?></p></div>
		<?php elseif ( ! empty( $result['error'] ) ) : ?>
			<div class="dp-skumig__error"><p><?php echo esc_html( $result['error'] ); ?></p></div>
		<?php else : ?>
			<div class="stats">
				<div class="stat ready"><div class="n"><?php echo esc_html( $result['products_migrated'] ); ?></div><div class="l"><?php esc_html_e( 'Products updated', 'drop-print' ); ?></div></div>
				<div class="stat ready"><div class="n"><?php echo esc_html( $result['variations_migrated'] ); ?></div><div class="l"><?php esc_html_e( 'Variations migrated', 'drop-print' ); ?></div></div>
			</div>
			<?php 
			?>
			<?php if ( ! empty( $result['backup_paths'] ) && is_array( $result['backup_paths'] ) ) : ?>
				<div class="dp-skumig__empty">
					<p><?php esc_html_e( 'This ran in batches; each batch wrote its own backup before it changed anything:', 'drop-print' ); ?></p>
					<?php foreach ( $result['backup_paths'] as $pair ) : ?>
						<p>
							<code><?php echo esc_html( $pair['csv'] ?? '' ); ?></code><br>
							<?php if ( ! empty( $pair['json'] ) ) : ?>
								<code><?php echo esc_html( $pair['json'] ); ?></code>
							<?php endif; ?>
						</p>
					<?php endforeach; ?>
				</div>
			<?php elseif ( ! empty( $result['backup_path'] ) ) : ?>
				<div class="dp-skumig__empty">
					<p>
						<?php esc_html_e( 'A backup of the previous Drop Print codes was saved to this server at:', 'drop-print' ); ?><br>
						<code><?php echo esc_html( $result['backup_path'] ); ?></code>
					</p>
					<?php if ( ! empty( $result['backup_json_path'] ) ) : ?>
						<p>
							<?php esc_html_e( 'A full restore snapshot (prices, stock, images) was saved alongside it at:', 'drop-print' ); ?><br>
							<code><?php echo esc_html( $result['backup_json_path'] ); ?></code>
						</p>
					<?php endif; ?>
				</div>
			<?php endif; ?>
			<?php if ( ! empty( $result['errors'] ) ) : ?>
				<div class="dp-skumig__error">
					<p><strong><?php esc_html_e( 'Some rows could not be saved:', 'drop-print' ); ?></strong></p>
					<ul><?php foreach ( $result['errors'] as $err ) : ?><li><?php echo esc_html( $err ); ?></li><?php endforeach; ?></ul>
				</div>
			<?php endif; ?>
			<?php if ( ! empty( $result['dropped_stale'] ) ) : ?>
				<div class="dp-skumig__error">
					<p><strong><?php esc_html_e( 'Some approved rows were skipped because the mapping changed since you reviewed them:', 'drop-print' ); ?></strong></p>
					<ul>
						<?php foreach ( $result['dropped_stale'] as $drop ) : ?>
							<li>
								<?php
								echo esc_html(
									sprintf(
										__( 'Variation #%1$d - %2$s', 'drop-print' ),
										$drop['variation_id'],
										$drop['reason']
									)
								);
								?>
							</li>
						<?php endforeach; ?>
					</ul>
					<p><?php esc_html_e( 'Nothing for these rows was changed. Re-open the review screen to see their current mapping.', 'drop-print' ); ?></p>
				</div>
			<?php endif; ?>
			<?php if ( ! empty( $result['unresolved_families'] ) ) : ?>
				<div class="dp-skumig__notice">
					<p>
						<?php
						printf(
							esc_html__( '%1$d paper code(s) on this site don\'t match any known paper at all: %2$s. This migration can\'t do anything with these — they need a support ticket, not another run.', 'drop-print' ),
							count( $result['unresolved_families'] ),
							esc_html( implode( ', ', $result['unresolved_families'] ) )
						);
						?>
					</p>
				</div>
			<?php endif; ?>
		<?php endif; ?>
		<div class="action-bar">
			<div class="action-inner">
				<div class="action-text"></div>
				<div class="action-buttons">
					<a href="<?php echo esc_url( drop_print_sku_migration_page_url() ); ?>" class="button-plain"><?php esc_html_e( 'Back to review', 'drop-print' ); ?></a>
					<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=product' ) ); ?>" class="primary" style="text-decoration:none;display:inline-flex;align-items:center;"><?php esc_html_e( 'Go to products', 'drop-print' ); ?></a>
				</div>
			</div>
		</div>
	</div>
	<?php
}
