<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Manfiet_Migration_Manager
 *
 * Provides a full Import / Export (Migration) feature for the Manfi Expense Tracker plugin.
 * Both the "Expense Tracker" and "Invoice Manager" admin menus expose a "Migration" submenu
 * that points to this single page.
 *
 * Export: produces a structured JSON file containing any combination of:
 *   expenses, categories, tags, invoices, vendors, clients, financial accounts, settings.
 *
 * Import: accepts a JSON file previously exported by this class, validates it, then
 *   upserts records in dependency order with ID remapping for cross-server migrations.
 */
class Manfiet_Migration_Manager {

	private const VERSION        = '1.0';
	private const EXPORT_ACTION  = 'manfiet_migration_export';
	private const IMPORT_ACTION  = 'manfiet_migration_import';
	private const ROLLBACK_ACTION = 'manfiet_version_rollback';
	private const NONCE_EXPORT   = 'manfiet_migration_export_nonce';
	private const NONCE_IMPORT   = 'manfiet_migration_import_nonce';
	private const NONCE_ROLLBACK  = 'manfiet_migration_rollback_nonce';
	private const PAGE_SLUG      = 'manfiet-migration';

	/** Settings option keys to include in export (excludes internal counters). */
	private const SETTINGS_KEYS = [
		'manfiet_default_budget',
		'manfiet_default_weekly_budget',
		'manfiet_default_yearly_budget',
		'manfiet_export_unknown_user_label',
		'manfiet_enable_gutenberg_blocks',
		'manfiet_enable_elementor_widgets',
		'manfiet_email_sender_address',
		'manfiet_email_sender_name',
		'manfiet_expenses_per_page',
		'manfiet_currency',
		'manfiet_enable_invoice_manager',
	];

	public function __construct() {
		add_action( 'admin_post_' . self::EXPORT_ACTION, [ $this, 'handle_export' ] );
		add_action( 'admin_post_' . self::IMPORT_ACTION, [ $this, 'handle_import' ] );
		add_action( 'admin_post_' . self::ROLLBACK_ACTION, [ $this, 'handle_rollback' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_migration_styles' ] );
	}

	// -------------------------------------------------------------------------
	// Menu / page rendering
	// -------------------------------------------------------------------------

	/**
	 * Called by Manfiet_Expense_Admin_Menu to register the submenu under both parents.
	 * Uses the same slug so WordPress de-duplicates it automatically.
	 */
	public function register_submenu( string $parent_slug ) {
		add_submenu_page(
			$parent_slug,
			esc_html__( 'Migration', 'manfi-expense-tracker' ),
			esc_html__( 'Migration', 'manfi-expense-tracker' ),
			'manage_options',
			self::PAGE_SLUG,
			[ $this, 'render_migration_page' ]
		);
	}

	public function enqueue_migration_styles( string $hook ) {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
		if ( $page !== self::PAGE_SLUG ) {
			return;
		}

		$css_path = MANFIET_EXPENSE_TRACKER_PLUGIN_DIR . 'assets/css/migration.css';
		$version  = file_exists( $css_path ) ? filemtime( $css_path ) : MANFIET_EXPENSE_TRACKER_VERSION;

		wp_enqueue_style(
			'manfiet-migration',
			MANFIET_EXPENSE_TRACKER_PLUGIN_URL . 'assets/css/migration.css',
			[],
			$version
		);
	}

	// -------------------------------------------------------------------------
	// Page render
	// -------------------------------------------------------------------------

	public function render_migration_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'manfi-expense-tracker' ) );
		}

		// Gather dropdown data for granular export options.
		$vendors = $this->get_all_vendors_for_select();
		$clients = $this->get_all_clients_for_select();
		$invoices = $this->get_all_invoices_for_select();

		// Retrieve import results from transient (set after a redirect).
		$import_results = get_transient( 'manfiet_migration_import_results_' . get_current_user_id() );
		if ( $import_results ) {
			delete_transient( 'manfiet_migration_import_results_' . get_current_user_id() );
		}

		$export_url = admin_url( 'admin-post.php' );
		$import_url = admin_url( 'admin-post.php' );
		?>
		<div class="wrap manfiet-migration-wrap">
			<h2 class="wp-header-end" style="display:none;"></h2>
			<div class="manfiet-migration-header">
				<div class="manfiet-migration-header-inner">
					<h1 class="manfiet-migration-title">
						<span class="manfiet-migration-icon">🔄</span>
						<?php esc_html_e( 'Migration — Import &amp; Export', 'manfi-expense-tracker' ); ?>
					</h1>
					<p class="manfiet-migration-subtitle">
						<?php esc_html_e( 'Move your expense tracker data between servers. Export a JSON bundle here, then import it on the target installation.', 'manfi-expense-tracker' ); ?>
					</p>
				</div>
			</div>

			<?php if ( $import_results ) : ?>
				<?php $this->render_import_results( $import_results ); ?>
			<?php endif; ?>

			<div class="manfiet-migration-grid">

				<!-- ================= EXPORT CARD ================= -->
				<div class="manfiet-migration-card manfiet-migration-card--export">
					<div class="manfiet-migration-card-header">
						<div class="manfiet-migration-card-icon manfiet-migration-card-icon--export">⬇</div>
						<h2 class="manfiet-migration-card-title"><?php esc_html_e( 'Export Data', 'manfi-expense-tracker' ); ?></h2>
						<p class="manfiet-migration-card-desc"><?php esc_html_e( 'Select what to include in the exported JSON file.', 'manfi-expense-tracker' ); ?></p>
					</div>

					<form method="post" action="<?php echo esc_url( $export_url ); ?>" id="manfiet-export-form">
						<?php wp_nonce_field( self::NONCE_EXPORT, '_wpnonce' ); ?>
						<input type="hidden" name="action" value="<?php echo esc_attr( self::EXPORT_ACTION ); ?>">

						<div class="manfiet-migration-section">
							<label class="manfiet-migration-label"><?php esc_html_e( 'Scope', 'manfi-expense-tracker' ); ?></label>

							<label class="manfiet-migration-check manfiet-migration-check--all">
								<input type="checkbox" id="export_all" name="export_scope[]" value="all" checked>
								<span><?php esc_html_e( '🗂 Everything (Full Export)', 'manfi-expense-tracker' ); ?></span>
							</label>

							<div class="manfiet-migration-divider">
								<span><?php esc_html_e( '— or choose individual scopes —', 'manfi-expense-tracker' ); ?></span>
							</div>

							<div class="manfiet-migration-checks-grid">
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="expenses" class="scope-individual">
									<span><?php esc_html_e( '💰 Expenses', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="categories" class="scope-individual">
									<span><?php esc_html_e( '🏷 Categories', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="tags" class="scope-individual">
									<span><?php esc_html_e( '🔖 Tags', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="invoices" class="scope-individual">
									<span><?php esc_html_e( '📄 Invoices', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="vendors" class="scope-individual">
									<span><?php esc_html_e( '🏭 Vendors', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="clients" class="scope-individual">
									<span><?php esc_html_e( '👥 Clients', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="financial_accounts" class="scope-individual">
									<span><?php esc_html_e( '🏦 Financial Accounts', 'manfi-expense-tracker' ); ?></span>
								</label>
								<label class="manfiet-migration-check">
									<input type="checkbox" name="export_scope[]" value="settings" class="scope-individual">
									<span><?php esc_html_e( '⚙️ Plugin Settings', 'manfi-expense-tracker' ); ?></span>
								</label>
							</div>
						</div>

						<div class="manfiet-migration-section manfiet-migration-section--granular">
							<label class="manfiet-migration-label"><?php esc_html_e( 'Granular Invoice Export', 'manfi-expense-tracker' ); ?></label>
							<p class="manfiet-migration-hint"><?php esc_html_e( 'Optionally export a specific invoice or all invoices for one vendor/client. Overrides the scope selection above for invoices.', 'manfi-expense-tracker' ); ?></p>

							<div class="manfiet-migration-radio-group">
								<label class="manfiet-migration-radio">
									<input type="radio" name="granular_type" value="" checked>
									<span><?php esc_html_e( 'No granular filter', 'manfi-expense-tracker' ); ?></span>
								</label>

								<label class="manfiet-migration-radio">
									<input type="radio" name="granular_type" value="single_invoice">
									<span><?php esc_html_e( 'Single Invoice:', 'manfi-expense-tracker' ); ?></span>
									<select name="granular_invoice_id" class="manfiet-migration-select" id="granular_invoice_select">
										<option value=""><?php esc_html_e( '— select invoice —', 'manfi-expense-tracker' ); ?></option>
										<?php foreach ( $invoices as $inv ) : ?>
											<option value="<?php echo esc_attr( $inv['id'] ); ?>">
												<?php echo esc_html( $inv['label'] ); ?>
											</option>
										<?php endforeach; ?>
									</select>
								</label>

								<label class="manfiet-migration-radio">
									<input type="radio" name="granular_type" value="by_vendor">
									<span><?php esc_html_e( 'All invoices for Vendor:', 'manfi-expense-tracker' ); ?></span>
									<select name="granular_vendor_id" class="manfiet-migration-select" id="granular_vendor_select">
										<option value=""><?php esc_html_e( '— select vendor —', 'manfi-expense-tracker' ); ?></option>
										<?php foreach ( $vendors as $v ) : ?>
											<option value="<?php echo esc_attr( $v['id'] ); ?>">
												<?php echo esc_html( $v['label'] ); ?>
											</option>
										<?php endforeach; ?>
									</select>
								</label>

								<label class="manfiet-migration-radio">
									<input type="radio" name="granular_type" value="by_client">
									<span><?php esc_html_e( 'All invoices for Client:', 'manfi-expense-tracker' ); ?></span>
									<select name="granular_client_id" class="manfiet-migration-select" id="granular_client_select">
										<option value=""><?php esc_html_e( '— select client —', 'manfi-expense-tracker' ); ?></option>
										<?php foreach ( $clients as $c ) : ?>
											<option value="<?php echo esc_attr( $c['id'] ); ?>">
												<?php echo esc_html( $c['label'] ); ?>
											</option>
										<?php endforeach; ?>
									</select>
								</label>
							</div>
						</div>

						<button type="submit" class="manfiet-migration-btn manfiet-migration-btn--export">
							<span class="manfiet-migration-btn-icon">⬇</span>
							<?php esc_html_e( 'Download Export File', 'manfi-expense-tracker' ); ?>
						</button>
					</form>
				</div><!-- /export card -->

				<!-- ================= IMPORT CARD ================= -->
				<div class="manfiet-migration-card manfiet-migration-card--import">
					<div class="manfiet-migration-card-header">
						<div class="manfiet-migration-card-icon manfiet-migration-card-icon--import">⬆</div>
						<h2 class="manfiet-migration-card-title"><?php esc_html_e( 'Import Data', 'manfi-expense-tracker' ); ?></h2>
						<p class="manfiet-migration-card-desc"><?php esc_html_e( 'Upload a .json file that was exported from this plugin on another server.', 'manfi-expense-tracker' ); ?></p>
					</div>

					<form method="post" action="<?php echo esc_url( $import_url ); ?>" enctype="multipart/form-data" id="manfiet-import-form">
						<?php wp_nonce_field( self::NONCE_IMPORT, '_wpnonce' ); ?>
						<input type="hidden" name="action" value="<?php echo esc_attr( self::IMPORT_ACTION ); ?>">

						<div class="manfiet-migration-section">
							<label class="manfiet-migration-label" for="manfiet_import_file">
								<?php esc_html_e( 'Migration JSON File', 'manfi-expense-tracker' ); ?>
							</label>
							<div class="manfiet-migration-file-drop" id="manfiet-file-drop">
								<input type="file" name="manfiet_import_file" id="manfiet_import_file" accept=".json,application/json" required>
								<div class="manfiet-migration-file-drop-text">
									<span class="manfiet-migration-file-icon">📁</span>
									<span><?php esc_html_e( 'Drop your .json file here, or click to browse', 'manfi-expense-tracker' ); ?></span>
									<span class="manfiet-migration-file-name" id="manfiet-file-name"></span>
								</div>
							</div>
						</div>

						<div class="manfiet-migration-section">
							<label class="manfiet-migration-label"><?php esc_html_e( 'Import Options', 'manfi-expense-tracker' ); ?></label>

							<label class="manfiet-migration-check">
								<input type="checkbox" name="overwrite_existing" value="1">
								<span>
									<?php esc_html_e( 'Overwrite existing records (matched by unique key)', 'manfi-expense-tracker' ); ?>
								</span>
							</label>

							<label class="manfiet-migration-check manfiet-migration-check--warning">
								<input type="checkbox" name="import_settings" value="1" id="import_settings_checkbox">
								<span>
									<?php esc_html_e( 'Also import Plugin Settings', 'manfi-expense-tracker' ); ?>
									<em class="manfiet-migration-warn-note">
										<?php esc_html_e( '⚠ This will overwrite your current site settings (currency, budgets, email settings, etc.)', 'manfi-expense-tracker' ); ?>
									</em>
								</span>
							</label>
						</div>

						<button type="submit" class="manfiet-migration-btn manfiet-migration-btn--import">
							<span class="manfiet-migration-btn-icon">⬆</span>
							<?php esc_html_e( 'Import File', 'manfi-expense-tracker' ); ?>
						</button>
					</form>
				</div><!-- /import card -->

			</div><!-- /grid -->

			<div class="manfiet-migration-info-bar">
				<div class="manfiet-migration-info-item">
					<strong><?php esc_html_e( 'Format:', 'manfi-expense-tracker' ); ?></strong>
					<?php esc_html_e( 'Structured JSON — contains all selected data types in a single file.', 'manfi-expense-tracker' ); ?>
				</div>
				<div class="manfiet-migration-info-item">
					<strong><?php esc_html_e( 'ID Remapping:', 'manfi-expense-tracker' ); ?></strong>
					<?php esc_html_e( 'Vendor / Client / Account IDs are automatically remapped on import when included in the same bundle.', 'manfi-expense-tracker' ); ?>
				</div>
				<div class="manfiet-migration-info-item">
					<strong><?php esc_html_e( 'Duplicates:', 'manfi-expense-tracker' ); ?></strong>
					<?php esc_html_e( 'Existing records matched by their unique key are skipped unless "Overwrite" is checked.', 'manfi-expense-tracker' ); ?>
				</div>
			</div>
		</div><!-- /wrap -->

		<script>
		(function(){
			// "Everything" checkbox toggles individual scopes
			var allBox = document.getElementById('export_all');
			var indivBoxes = document.querySelectorAll('.scope-individual');
			if(allBox){
				allBox.addEventListener('change', function(){
					indivBoxes.forEach(function(b){ b.disabled = allBox.checked; b.checked = false; });
				});
				indivBoxes.forEach(function(b){
					b.addEventListener('change', function(){
						if(b.checked){ allBox.checked = false; }
					});
				});
				// Init
				indivBoxes.forEach(function(b){ b.disabled = allBox.checked; });
			}

			// File drop zone display name
			var fileInput = document.getElementById('manfiet_import_file');
			var fileName  = document.getElementById('manfiet-file-name');
			if(fileInput && fileName){
				fileInput.addEventListener('change', function(){
					fileName.textContent = fileInput.files[0] ? fileInput.files[0].name : '';
				});
			}
		})();
		</script>
		<?php
	}

	// -------------------------------------------------------------------------
	// Render import results
	// -------------------------------------------------------------------------

	private function render_import_results( array $results ) {
		$error = $results['error'] ?? '';
		?>
		<div class="manfiet-migration-results <?php echo $error ? 'manfiet-migration-results--error' : 'manfiet-migration-results--success'; ?>">
			<h3 class="manfiet-migration-results-title">
				<?php echo $error
					? esc_html__( '❌ Import Failed', 'manfi-expense-tracker' )
					: esc_html__( '✅ Import Complete', 'manfi-expense-tracker' ); ?>
			</h3>
			<?php if ( $error ) : ?>
				<p class="manfiet-migration-results-error"><?php echo esc_html( $error ); ?></p>
			<?php else : ?>
				<table class="manfiet-migration-results-table">
					<thead>
						<tr>
							<th><?php esc_html_e( 'Scope', 'manfi-expense-tracker' ); ?></th>
							<th><?php esc_html_e( 'Inserted', 'manfi-expense-tracker' ); ?></th>
							<th><?php esc_html_e( 'Skipped', 'manfi-expense-tracker' ); ?></th>
							<th><?php esc_html_e( 'Errors', 'manfi-expense-tracker' ); ?></th>
						</tr>
					</thead>
					<tbody>
						<?php foreach ( $results['scopes'] ?? [] as $scope => $counts ) : ?>
							<tr>
								<td><?php echo esc_html( ucfirst( str_replace( '_', ' ', $scope ) ) ); ?></td>
								<td class="manfiet-migration-count manfiet-migration-count--inserted"><?php echo (int) ( $counts['inserted'] ?? 0 ); ?></td>
								<td class="manfiet-migration-count manfiet-migration-count--skipped"><?php echo (int) ( $counts['skipped'] ?? 0 ); ?></td>
								<td class="manfiet-migration-count manfiet-migration-count--errors"><?php echo (int) ( $counts['errors'] ?? 0 ); ?></td>
							</tr>
						<?php endforeach; ?>
					</tbody>
				</table>
				<?php if ( ! empty( $results['notices'] ) ) : ?>
					<ul class="manfiet-migration-notices">
						<?php foreach ( $results['notices'] as $notice ) : ?>
							<li><?php echo esc_html( $notice ); ?></li>
						<?php endforeach; ?>
					</ul>
				<?php endif; ?>
			<?php endif; ?>
		</div>
		<?php
	}

	// -------------------------------------------------------------------------
	// Export handler
	// -------------------------------------------------------------------------

	public function handle_export() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'manfi-expense-tracker' ), 403 );
		}

		$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, self::NONCE_EXPORT ) ) {
			wp_die( esc_html__( 'Security check failed.', 'manfi-expense-tracker' ), 403 );
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified above.
		$raw_scopes = isset( $_POST['export_scope'] ) && is_array( $_POST['export_scope'] )
			? array_map( 'sanitize_key', wp_unslash( $_POST['export_scope'] ) )
			: [ 'all' ];

		$granular_type       = isset( $_POST['granular_type'] ) ? sanitize_key( wp_unslash( $_POST['granular_type'] ) ) : '';
		$granular_invoice_id = isset( $_POST['granular_invoice_id'] ) ? absint( wp_unslash( $_POST['granular_invoice_id'] ) ) : 0;
		$granular_vendor_id  = isset( $_POST['granular_vendor_id'] ) ? absint( wp_unslash( $_POST['granular_vendor_id'] ) ) : 0;
		$granular_client_id  = isset( $_POST['granular_client_id'] ) ? absint( wp_unslash( $_POST['granular_client_id'] ) ) : 0;
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$export_all = in_array( 'all', $raw_scopes, true );
		$scopes     = $export_all
			? [ 'expenses', 'categories', 'tags', 'invoices', 'vendors', 'clients', 'financial_accounts', 'settings' ]
			: $raw_scopes;

		$data           = [];
		$included_scopes = [];

		// Granular invoice export overrides the normal "invoices" scope.
		$granular_invoices = null;
		if ( $granular_type === 'single_invoice' && $granular_invoice_id > 0 ) {
			$granular_invoices = $this->export_single_invoice( $granular_invoice_id );
			$included_scopes[] = 'invoices (single: ' . $granular_invoice_id . ')';
		} elseif ( $granular_type === 'by_vendor' && $granular_vendor_id > 0 ) {
			$granular_invoices = $this->export_invoices_by_vendor( $granular_vendor_id );
			$included_scopes[] = 'invoices (vendor: ' . $granular_vendor_id . ')';
		} elseif ( $granular_type === 'by_client' && $granular_client_id > 0 ) {
			$granular_invoices = $this->export_invoices_by_client( $granular_client_id );
			$included_scopes[] = 'invoices (client: ' . $granular_client_id . ')';
		}

		if ( in_array( 'expenses', $scopes, true ) ) {
			$data['expenses']  = $this->export_expenses();
			$included_scopes[] = 'expenses';
		}
		if ( in_array( 'categories', $scopes, true ) ) {
			$data['categories'] = $this->export_categories();
			$included_scopes[]  = 'categories';
		}
		if ( in_array( 'tags', $scopes, true ) ) {
			$data['tags']      = $this->export_tags();
			$included_scopes[] = 'tags';
		}
		if ( in_array( 'vendors', $scopes, true ) ) {
			$data['vendors']   = $this->export_vendors();
			$included_scopes[] = 'vendors';
		}
		if ( in_array( 'clients', $scopes, true ) ) {
			$data['clients']   = $this->export_clients();
			$included_scopes[] = 'clients';
		}
		if ( in_array( 'financial_accounts', $scopes, true ) ) {
			$data['financial_accounts'] = $this->export_financial_accounts();
			$included_scopes[]          = 'financial_accounts';
		}

		// Invoices: granular takes precedence over scope-based.
		if ( $granular_invoices !== null ) {
			$data['invoices'] = $granular_invoices;
		} elseif ( in_array( 'invoices', $scopes, true ) ) {
			$data['invoices']  = $this->export_invoices_all();
			$included_scopes[] = 'invoices';
		}

		if ( in_array( 'settings', $scopes, true ) ) {
			$data['settings']  = $this->export_settings();
			$included_scopes[] = 'settings';
		}

		$bundle = [
			'manfiet_migration_version' => self::VERSION,
			'exported_at'               => gmdate( 'c' ),
			'site_url'                  => get_site_url(),
			'plugin_version'            => MANFIET_EXPENSE_TRACKER_VERSION,
			'scopes'                    => $included_scopes,
			'data'                      => $data,
		];

		$filename = 'manfiet-migration-' . gmdate( 'Y-m-d' ) . '.json';
		$json     = wp_json_encode( $bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );

		while ( ob_get_level() > 0 ) {
			ob_end_clean();
		}

		nocache_headers();
		header( 'Content-Type: application/json; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Content-Length: ' . strlen( $json ) );

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary file download, not HTML.
		echo $json;
		exit;
	}

	// -------------------------------------------------------------------------
	// Import handler
	// -------------------------------------------------------------------------

	public function handle_import() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'manfi-expense-tracker' ), 403 );
		}

		$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, self::NONCE_IMPORT ) ) {
			wp_die( esc_html__( 'Security check failed.', 'manfi-expense-tracker' ), 403 );
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified above.
		$overwrite       = ! empty( $_POST['overwrite_existing'] );
		$import_settings = ! empty( $_POST['import_settings'] );
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$results = $this->process_import( $overwrite, $import_settings );

		// Store results in a transient, then redirect back to the page to display them.
		set_transient( 'manfiet_migration_import_results_' . get_current_user_id(), $results, 60 );

		wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) );
		exit;
	}

	// -------------------------------------------------------------------------
	// Rollback handler
	// -------------------------------------------------------------------------

	public function handle_rollback() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'manfi-expense-tracker' ), 403 );
		}

		$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, self::NONCE_ROLLBACK ) ) {
			wp_die( esc_html__( 'Security check failed.', 'manfi-expense-tracker' ), 403 );
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified above.
		$rollback_version = isset( $_POST['rollback_version'] ) ? sanitize_text_field( wp_unslash( $_POST['rollback_version'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		$allowed_versions = [ '1.0.7', '1.0.6', '1.0.5', '1.0.4', '1.0.3', '1.0.2', '1.0.1', '1.0.0' ];
		if ( ! in_array( $rollback_version, $allowed_versions, true ) ) {
			wp_die( esc_html__( 'Invalid rollback version selected.', 'manfi-expense-tracker' ), 400 );
		}

		$package_url = 'https://downloads.wordpress.org/plugin/manfi-expense-tracker.' . $rollback_version . '.zip';

		// Include WordPress administration core files.
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/misc.php';
		require_once ABSPATH . 'wp-admin/includes/template.php';

		// Set up WP_Upgrader Skin.
		$skin = new Plugin_Upgrader_Skin( [
			'title'  => __( 'Rollback Plugin', 'manfi-expense-tracker' ),
			'nonce'  => self::NONCE_ROLLBACK,
			'url'    => 'admin-post.php?action=' . self::ROLLBACK_ACTION,
			'plugin' => MANFIET_EXPENSE_TRACKER_PLUGIN_BASENAME,
		] );

		$skin->header();

		$upgrader = new Plugin_Upgrader( $skin );
		$result   = $upgrader->install( $package_url, [ 'overwrite_package' => true ] );

		if ( ! is_wp_error( $result ) && $result ) {
			// Reactivate the plugin since overwrite deactivates it.
			activate_plugin( MANFIET_EXPENSE_TRACKER_PLUGIN_BASENAME );
			// translators: %s: target plugin version number
			echo '<div class="notice notice-success is-dismissible" style="margin-top: 15px;"><p>' . sprintf( esc_html__( 'Successfully rolled back to version %s! Reactivating the plugin...', 'manfi-expense-tracker' ), esc_html( $rollback_version ) ) . '</p></div>';
			echo '<p><a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ) . '" class="button button-primary">' . esc_html__( 'Return to Migration Dashboard', 'manfi-expense-tracker' ) . '</a></p>';
		} else {
			echo '<div class="notice notice-error" style="margin-top: 15px;"><p>' . esc_html__( 'Rollback failed. Please check file permissions or try manually.', 'manfi-expense-tracker' ) . '</p></div>';
		}

		$skin->footer();
		exit;
	}

	private function process_import( bool $overwrite, bool $import_settings ): array {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce is verified in handle_import() prior to calling this method.
		// Validate upload.
		if ( empty( $_FILES['manfiet_import_file']['tmp_name'] ) ) {
			return [ 'error' => __( 'No file uploaded.', 'manfi-expense-tracker' ) ];
		}

		$file = $_FILES['manfiet_import_file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		$allowed_types = [ 'application/json', 'text/plain', 'application/octet-stream' ];

		// Read raw content.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$raw = file_get_contents( $file['tmp_name'] );
		if ( $raw === false || $raw === '' ) {
			return [ 'error' => __( 'Could not read the uploaded file.', 'manfi-expense-tracker' ) ];
		}

		$bundle = json_decode( $raw, true );
		if ( ! is_array( $bundle ) || ! isset( $bundle['manfiet_migration_version'], $bundle['data'] ) ) {
			return [ 'error' => __( 'Invalid migration file. Please upload a JSON file exported by this plugin.', 'manfi-expense-tracker' ) ];
		}

		$data    = $bundle['data'];
		$results = [ 'scopes' => [], 'notices' => [] ];

		// --- Dependency order: settings, categories, tags, financial accounts, vendors, clients, expenses, invoices.

		// Settings (optional, requires user confirmation).
		if ( $import_settings && ! empty( $data['settings'] ) ) {
			$r = $this->import_settings( (array) $data['settings'] );
			$results['scopes']['settings'] = $r;
		} elseif ( ! empty( $data['settings'] ) && ! $import_settings ) {
			$results['scopes']['settings'] = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
			$results['notices'][] = __( 'Settings were present in the file but not imported (checkbox was unchecked).', 'manfi-expense-tracker' );
		}

		// Categories.
		$category_remap = [];
		if ( ! empty( $data['categories'] ) ) {
			$r = $this->import_categories( (array) $data['categories'], $overwrite, $category_remap );
			$results['scopes']['categories'] = $r['counts'];
			$category_remap = $r['remap'];
		}

		// Tags.
		$tag_remap = [];
		if ( ! empty( $data['tags'] ) ) {
			$r = $this->import_tags( (array) $data['tags'], $overwrite, $tag_remap );
			$results['scopes']['tags'] = $r['counts'];
			$tag_remap = $r['remap'];
		}

		// Financial accounts.
		$account_remap = [];
		if ( ! empty( $data['financial_accounts'] ) ) {
			$r = $this->import_financial_accounts( (array) $data['financial_accounts'], $overwrite, $account_remap );
			$results['scopes']['financial_accounts'] = $r['counts'];
			$account_remap = $r['remap'];
		}

		// Vendors.
		$vendor_remap = [];
		if ( ! empty( $data['vendors'] ) ) {
			$r = $this->import_vendors( (array) $data['vendors'], $overwrite, $vendor_remap );
			$results['scopes']['vendors'] = $r['counts'];
			$vendor_remap = $r['remap'];
		}

		// Clients.
		$client_remap = [];
		if ( ! empty( $data['clients'] ) ) {
			$r = $this->import_clients( (array) $data['clients'], $overwrite, $client_remap );
			$results['scopes']['clients'] = $r['counts'];
			$client_remap = $r['remap'];
		}

		// Expenses (after categories & tags so remap works).
		if ( ! empty( $data['expenses'] ) ) {
			$r = $this->import_expenses( (array) $data['expenses'], $overwrite, $category_remap, $tag_remap );
			$results['scopes']['expenses'] = $r;
		}

		// Invoices (after vendors & clients & accounts).
		if ( ! empty( $data['invoices'] ) ) {
			$r = $this->import_invoices( (array) $data['invoices'], $overwrite, $vendor_remap, $client_remap, $account_remap );
			$results['scopes']['invoices'] = $r;
		}

		return $results;
		// phpcs:enable WordPress.Security.NonceVerification.Missing
	}

	// =========================================================================
	// EXPORT helpers
	// =========================================================================

	private function export_expenses(): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$table = $wpdb->prefix . 'manfiet_expenses';
		$rows  = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}manfiet_expenses ORDER BY id ASC", ARRAY_A );
		// phpcs:enable
		return is_array( $rows ) ? $rows : [];
	}

	private function export_categories(): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}manfiet_expense_categories ORDER BY id ASC", ARRAY_A );
		// phpcs:enable
		return is_array( $rows ) ? $rows : [];
	}

	private function export_tags(): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}manfiet_expense_tags ORDER BY id ASC", ARRAY_A );
		// phpcs:enable
		return is_array( $rows ) ? $rows : [];
	}

	private function export_invoices_all(): array {
		$posts = get_posts( [
			'post_type'      => 'manfiet_invoice',
			'post_status'    => 'any',
			'numberposts'    => -1,
			'orderby'        => 'ID',
			'order'          => 'ASC',
		] );
		return $this->serialize_posts( $posts, $this->invoice_meta_keys() );
	}

	private function export_single_invoice( int $id ): array {
		$post = get_post( $id );
		if ( ! $post || $post->post_type !== 'manfiet_invoice' ) {
			return [];
		}
		return $this->serialize_posts( [ $post ], $this->invoice_meta_keys() );
	}

	private function export_invoices_by_vendor( int $vendor_id ): array {
		// phpcs:disable WordPress.DB.SlowDBQuery -- Slow DB query is acceptable in admin migration context.
		$posts = get_posts( [
			'post_type'      => 'manfiet_invoice',
			'post_status'    => 'any',
			'numberposts'    => -1,
			'meta_key'       => '_manfiet_invoice_vendor_id',
			'meta_value'     => $vendor_id,
			'orderby'        => 'ID',
			'order'          => 'ASC',
		] );
		// phpcs:enable WordPress.DB.SlowDBQuery
		return $this->serialize_posts( $posts, $this->invoice_meta_keys() );
	}

	private function export_invoices_by_client( int $client_id ): array {
		// phpcs:disable WordPress.DB.SlowDBQuery -- Slow DB query is acceptable in admin migration context.
		$posts = get_posts( [
			'post_type'      => 'manfiet_invoice',
			'post_status'    => 'any',
			'numberposts'    => -1,
			'meta_key'       => '_manfiet_invoice_client_id',
			'meta_value'     => $client_id,
			'orderby'        => 'ID',
			'order'          => 'ASC',
		] );
		// phpcs:enable WordPress.DB.SlowDBQuery
		return $this->serialize_posts( $posts, $this->invoice_meta_keys() );
	}

	private function export_vendors(): array {
		$posts = get_posts( [
			'post_type'   => 'manfiet_vendor',
			'post_status' => 'any',
			'numberposts' => -1,
			'orderby'     => 'ID',
			'order'       => 'ASC',
		] );
		return $this->serialize_posts( $posts, $this->vendor_meta_keys() );
	}

	private function export_clients(): array {
		$posts = get_posts( [
			'post_type'   => 'manfiet_client',
			'post_status' => 'any',
			'numberposts' => -1,
			'orderby'     => 'ID',
			'order'       => 'ASC',
		] );
		return $this->serialize_posts( $posts, $this->client_meta_keys() );
	}

	private function export_financial_accounts(): array {
		$posts = get_posts( [
			'post_type'   => 'manfiet_fin_account',
			'post_status' => 'any',
			'numberposts' => -1,
			'orderby'     => 'ID',
			'order'       => 'ASC',
		] );
		return $this->serialize_posts( $posts, $this->account_meta_keys() );
	}

	private function export_settings(): array {
		$settings = [];
		foreach ( self::SETTINGS_KEYS as $key ) {
			$settings[ $key ] = get_option( $key );
		}
		return $settings;
	}

	// =========================================================================
	// IMPORT helpers
	// =========================================================================

	private function import_settings( array $settings ): array {
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
		foreach ( self::SETTINGS_KEYS as $key ) {
			if ( array_key_exists( $key, $settings ) ) {
				update_option( $key, $settings[ $key ] );
				$counts['inserted']++;
			}
		}
		return $counts;
	}

	private function import_categories( array $categories, bool $overwrite, array &$remap ): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$table  = $wpdb->prefix . 'manfiet_expense_categories';
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
		$remap  = [];

		// First pass: categories without parents.
		foreach ( $categories as $cat ) {
			if ( ! isset( $cat['category_name'] ) && ! isset( $cat['name'] ) ) {
				continue;
			}
			if ( (int) ( $cat['parent_id'] ?? 0 ) !== 0 ) {
				continue;
			}
			$this->upsert_category( $table, $cat, $overwrite, $remap, $counts );
		}

		// Second pass: categories with parents (remap parent_id).
		foreach ( $categories as $cat ) {
			if ( ! isset( $cat['category_name'] ) && ! isset( $cat['name'] ) ) {
				continue;
			}
			if ( (int) ( $cat['parent_id'] ?? 0 ) === 0 ) {
				continue;
			}
			$old_parent       = (int) $cat['parent_id'];
			$cat['parent_id'] = $remap[ $old_parent ] ?? $old_parent;
			$this->upsert_category( $table, $cat, $overwrite, $remap, $counts );
		}
		// phpcs:enable

		return [ 'counts' => $counts, 'remap' => $remap ];
	}

	private function upsert_category( string $table, array $cat, bool $overwrite, array &$remap, array &$counts ) {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$old_id  = (int) ( $cat['id'] ?? 0 );
		// Support both export column name variants.
		$name    = sanitize_text_field( (string) ( $cat['category_name'] ?? $cat['name'] ?? '' ) );
		$user_id = (int) ( $cat['user_id'] ?? 0 );

		$existing_id = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}manfiet_expense_categories WHERE LOWER(category_name) = LOWER(%s) AND user_id = %d LIMIT 1",
				$name,
				$user_id
			)
		);

		if ( $existing_id ) {
			$remap[ $old_id ] = $existing_id;
			if ( $overwrite ) {
				$wpdb->update(
					$table,
					[
						'category_name' => $name,
						'parent_id'     => (int) ( $cat['parent_id'] ?? 0 ),
						'description'   => sanitize_textarea_field( (string) ( $cat['description'] ?? '' ) ),
					],
					[ 'id' => $existing_id ],
					[ '%s', '%d', '%s' ],
					[ '%d' ]
				);
			}
			$counts['skipped']++;
			return;
		}

		$inserted = $wpdb->insert(
			$table,
			[
				'category_name' => $name,
				'user_id'       => $user_id,
				'parent_id'     => (int) ( $cat['parent_id'] ?? 0 ),
				'description'   => sanitize_textarea_field( (string) ( $cat['description'] ?? '' ) ),
			],
			[ '%s', '%d', '%d', '%s' ]
		);

		if ( $inserted ) {
			$remap[ $old_id ] = (int) $wpdb->insert_id;
			$counts['inserted']++;
		} else {
			$counts['errors']++;
		}
		// phpcs:enable
	}

	private function import_tags( array $tags, bool $overwrite, array &$remap ): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$table  = $wpdb->prefix . 'manfiet_expense_tags';
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
		$remap  = [];

		foreach ( $tags as $tag ) {
			// Support both export column name variants.
			if ( ! isset( $tag['tag_name'] ) && ! isset( $tag['name'] ) ) {
				continue;
			}
			$old_id  = (int) ( $tag['id'] ?? 0 );
			$name    = sanitize_text_field( (string) ( $tag['tag_name'] ?? $tag['name'] ?? '' ) );
			$user_id = (int) ( $tag['user_id'] ?? 0 );

			$existing_id = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT id FROM {$wpdb->prefix}manfiet_expense_tags WHERE LOWER(tag_name) = LOWER(%s) AND user_id = %d LIMIT 1",
					$name,
					$user_id
				)
			);

			if ( $existing_id ) {
				$remap[ $old_id ] = $existing_id;
				$counts['skipped']++;
				continue;
			}

			$inserted = $wpdb->insert(
				$table,
				[ 'tag_name' => $name, 'user_id' => $user_id ],
				[ '%s', '%d' ]
			);
			if ( $inserted ) {
				$remap[ $old_id ] = (int) $wpdb->insert_id;
				$counts['inserted']++;
			} else {
				$counts['errors']++;
			}
		}
		// phpcs:enable
		return [ 'counts' => $counts, 'remap' => $remap ];
	}

	private function import_expenses( array $expenses, bool $overwrite, array $category_remap, array $tag_remap ): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$table  = $wpdb->prefix . 'manfiet_expenses';
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];

		// Get actual column list so we never insert non-existent columns.
		$columns = $this->get_table_columns( $table );

		foreach ( $expenses as $exp ) {
			// The expense table stores category as a text name (not an ID).
			// Tags are stored as a JSON array of tag name strings.
			// Export captures the raw DB row so columns are already correct; we just
			// need to sanitize and skip/overwrite duplicates.

			// Unique key: date + amount + category + user_id.
			$existing = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT id FROM {$wpdb->prefix}manfiet_expenses WHERE date = %s AND amount = %f AND category = %s AND user_id = %d LIMIT 1",
					(string) ( $exp['date'] ?? '' ),
					(float) ( $exp['amount'] ?? 0 ),
					(string) ( $exp['category'] ?? '' ),
					(int) ( $exp['user_id'] ?? 0 )
				)
			);

			if ( $existing ) {
				if ( $overwrite ) {
					$wpdb->update(
						$table,
						$this->sanitize_expense_row( $exp, $columns ),
						[ 'id' => (int) $existing ],
						null,
						[ '%d' ]
					);
				}
				$counts['skipped']++;
				continue;
			}

			unset( $exp['id'] ); // Let DB auto-assign.
			$row = $this->sanitize_expense_row( $exp, $columns );
			if ( empty( $row['date'] ) || empty( $row['amount'] ) ) {
				$counts['errors']++;
				continue;
			}
			if ( $wpdb->insert( $table, $row ) ) {
				$counts['inserted']++;
			} else {
				$counts['errors']++;
			}
		}
		// phpcs:enable
		return $counts;
	}

	/**
	 * Sanitizes an expense row, keeping only columns that exist in the current install.
	 * Mirrors the actual schema: user_id, category (text), amount, date, recurring_interval, tags (JSON), remark.
	 */
	private function sanitize_expense_row( array $exp, array $columns ): array {
		$allowed = [
			'user_id'            => (int) ( $exp['user_id'] ?? 0 ),
			'amount'             => (float) ( $exp['amount'] ?? 0 ),
			'category'           => sanitize_text_field( (string) ( $exp['category'] ?? '' ) ),
			'date'               => sanitize_text_field( (string) ( $exp['date'] ?? '' ) ),
			'recurring_interval' => sanitize_key( (string) ( $exp['recurring_interval'] ?? '' ) ) ?: null,
			'tags'               => sanitize_text_field( (string) ( $exp['tags'] ?? '' ) ),
			'remark'             => sanitize_textarea_field( (string) ( $exp['remark'] ?? '' ) ),
		];
		// Keep only columns that actually exist in this install.
		return array_filter(
			array_intersect_key( $allowed, array_flip( $columns ) ),
			static function ( $v ) { return $v !== null && $v !== ''; }
		);
	}

	private function import_vendors( array $vendors, bool $overwrite, array &$remap ): array {
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
		$remap  = [];
		foreach ( $vendors as $v ) {
			$old_id      = (int) ( $v['id'] ?? 0 );
			$company     = sanitize_text_field( (string) ( $v['meta']['_manfiet_vendor_company_name'] ?? $v['company_name'] ?? '' ) );
			$email       = sanitize_email( (string) ( $v['meta']['_manfiet_vendor_email'] ?? $v['email'] ?? '' ) );
			$title       = sanitize_text_field( (string) ( $v['title'] ?? $company ) );

			// Check duplicate by company_name + email.
			$existing_ids = get_posts( [
				'post_type'   => 'manfiet_vendor',
				'post_status' => 'any',
				'numberposts' => 1,
				'meta_query'  => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					[
						'key'   => '_manfiet_vendor_company_name',
						'value' => $company,
					],
				],
				'fields'      => 'ids',
			] );

			if ( ! empty( $existing_ids ) ) {
				$existing_id      = (int) $existing_ids[0];
				$remap[ $old_id ] = $existing_id;
				if ( $overwrite ) {
					wp_update_post( [ 'ID' => $existing_id, 'post_title' => $title ] );
					$this->save_post_meta( $existing_id, $v['meta'] ?? [], $this->vendor_meta_keys() );
				}
				$counts['skipped']++;
				continue;
			}

			$new_id = wp_insert_post( [
				'post_type'   => 'manfiet_vendor',
				'post_status' => 'publish',
				'post_title'  => $title,
			] );

			if ( is_wp_error( $new_id ) ) {
				$counts['errors']++;
				continue;
			}

			$this->save_post_meta( $new_id, $v['meta'] ?? [], $this->vendor_meta_keys() );
			$remap[ $old_id ] = $new_id;
			$counts['inserted']++;
		}
		return [ 'counts' => $counts, 'remap' => $remap ];
	}

	private function import_clients( array $clients, bool $overwrite, array &$remap ): array {
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
		$remap  = [];
		foreach ( $clients as $c ) {
			$old_id  = (int) ( $c['id'] ?? 0 );
			$name    = sanitize_text_field( (string) ( $c['meta']['_manfiet_client_name'] ?? $c['client_name'] ?? '' ) );
			$email   = sanitize_email( (string) ( $c['meta']['_manfiet_client_email'] ?? $c['email'] ?? '' ) );
			$title   = sanitize_text_field( (string) ( $c['title'] ?? $name ) );

			$existing_ids = get_posts( [
				'post_type'   => 'manfiet_client',
				'post_status' => 'any',
				'numberposts' => 1,
				'meta_query'  => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					[
						'key'   => '_manfiet_client_name',
						'value' => $name,
					],
				],
				'fields'      => 'ids',
			] );

			if ( ! empty( $existing_ids ) ) {
				$existing_id      = (int) $existing_ids[0];
				$remap[ $old_id ] = $existing_id;
				if ( $overwrite ) {
					wp_update_post( [ 'ID' => $existing_id, 'post_title' => $title ] );
					$this->save_post_meta( $existing_id, $c['meta'] ?? [], $this->client_meta_keys() );
				}
				$counts['skipped']++;
				continue;
			}

			$new_id = wp_insert_post( [
				'post_type'   => 'manfiet_client',
				'post_status' => 'publish',
				'post_title'  => $title,
			] );

			if ( is_wp_error( $new_id ) ) {
				$counts['errors']++;
				continue;
			}

			$this->save_post_meta( $new_id, $c['meta'] ?? [], $this->client_meta_keys() );
			$remap[ $old_id ] = $new_id;
			$counts['inserted']++;
		}
		return [ 'counts' => $counts, 'remap' => $remap ];
	}

	private function import_financial_accounts( array $accounts, bool $overwrite, array &$remap ): array {
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];
		$remap  = [];
		foreach ( $accounts as $acc ) {
			$old_id   = (int) ( $acc['id'] ?? 0 );
			$provider = sanitize_text_field( (string) ( $acc['meta']['_manfiet_provider_name'] ?? $acc['provider_name'] ?? '' ) );
			$acct_no  = sanitize_text_field( (string) ( $acc['meta']['_manfiet_account_number'] ?? $acc['account_number'] ?? '' ) );
			$title    = sanitize_text_field( (string) ( $acc['title'] ?? $provider ) );

			$existing_ids = get_posts( [
				'post_type'   => 'manfiet_fin_account',
				'post_status' => 'any',
				'numberposts' => 1,
				'meta_query'  => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					'relation' => 'AND',
					[
						'key'   => '_manfiet_provider_name',
						'value' => $provider,
					],
					[
						'key'   => '_manfiet_account_number',
						'value' => $acct_no,
					],
				],
				'fields'      => 'ids',
			] );

			if ( ! empty( $existing_ids ) ) {
				$existing_id      = (int) $existing_ids[0];
				$remap[ $old_id ] = $existing_id;
				if ( $overwrite ) {
					wp_update_post( [ 'ID' => $existing_id, 'post_title' => $title ] );
					$this->save_post_meta( $existing_id, $acc['meta'] ?? [], $this->account_meta_keys() );
				}
				$counts['skipped']++;
				continue;
			}

			$new_id = wp_insert_post( [
				'post_type'   => 'manfiet_fin_account',
				'post_status' => 'publish',
				'post_title'  => $title,
			] );

			if ( is_wp_error( $new_id ) ) {
				$counts['errors']++;
				continue;
			}

			$this->save_post_meta( $new_id, $acc['meta'] ?? [], $this->account_meta_keys() );
			$remap[ $old_id ] = $new_id;
			$counts['inserted']++;
		}
		return [ 'counts' => $counts, 'remap' => $remap ];
	}

	private function import_invoices( array $invoices, bool $overwrite, array $vendor_remap, array $client_remap, array $account_remap ): array {
		$counts = [ 'inserted' => 0, 'skipped' => 0, 'errors' => 0 ];

		foreach ( $invoices as $inv ) {
			$old_id     = (int) ( $inv['id'] ?? 0 );
			$invoice_no = sanitize_text_field( (string) ( $inv['meta']['_manfiet_invoice_no'] ?? $inv['invoice_no'] ?? '' ) );
			$title      = sanitize_text_field( (string) ( $inv['title'] ?? $invoice_no ) );

			// Duplicate check: invoice_no.
			$existing_ids = get_posts( [
				'post_type'   => 'manfiet_invoice',
				'post_status' => 'any',
				'numberposts' => 1,
				'meta_query'  => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
					[
						'key'   => '_manfiet_invoice_no',
						'value' => $invoice_no,
					],
				],
				'fields'      => 'ids',
			] );

			// Remap related IDs using the maps built earlier.
			$meta = $inv['meta'] ?? [];
			if ( ! empty( $meta['_manfiet_invoice_vendor_id'] ) ) {
				$old_vid = (int) $meta['_manfiet_invoice_vendor_id'];
				$meta['_manfiet_invoice_vendor_id'] = $vendor_remap[ $old_vid ] ?? $old_vid;
			}
			if ( ! empty( $meta['_manfiet_invoice_client_id'] ) ) {
				$old_cid = (int) $meta['_manfiet_invoice_client_id'];
				$meta['_manfiet_invoice_client_id'] = $client_remap[ $old_cid ] ?? $old_cid;
			}
			if ( ! empty( $meta['_manfiet_invoice_financial_account_ids'] ) ) {
				$old_acc_ids = json_decode( (string) $meta['_manfiet_invoice_financial_account_ids'], true );
				if ( is_array( $old_acc_ids ) ) {
					$new_acc_ids = array_map( function( $aid ) use ( $account_remap ) {
						return $account_remap[ (int) $aid ] ?? (int) $aid;
					}, $old_acc_ids );
					$meta['_manfiet_invoice_financial_account_ids'] = wp_json_encode( $new_acc_ids );
				}
			}

			if ( ! empty( $existing_ids ) ) {
				$existing_id = (int) $existing_ids[0];
				if ( $overwrite ) {
					wp_update_post( [ 'ID' => $existing_id, 'post_title' => $title ] );
					$this->save_post_meta( $existing_id, $meta, $this->invoice_meta_keys() );
				}
				$counts['skipped']++;
				continue;
			}

			$new_id = wp_insert_post( [
				'post_type'   => 'manfiet_invoice',
				'post_status' => $inv['post_status'] ?? 'publish',
				'post_title'  => $title,
				'post_author' => get_current_user_id(),
			] );

			if ( is_wp_error( $new_id ) ) {
				$counts['errors']++;
				continue;
			}

			$this->save_post_meta( $new_id, $meta, $this->invoice_meta_keys() );
			$counts['inserted']++;
		}
		return $counts;
	}

	// =========================================================================
	// Helpers — serialization / deserialization
	// =========================================================================

	/**
	 * Serializes an array of WP_Post objects into a portable array of [ id, title, post_status, meta => [...] ].
	 */
	private function serialize_posts( array $posts, array $meta_keys ): array {
		$result = [];
		foreach ( $posts as $post ) {
			$meta = [];
			foreach ( $meta_keys as $key ) {
				$raw        = get_post_meta( $post->ID, $key, true );
				$meta[$key] = $raw;
			}
			$result[] = [
				'id'          => $post->ID,
				'title'       => get_the_title( $post ),
				'post_status' => $post->post_status,
				'meta'        => $meta,
			];
		}
		return $result;
	}

	/**
	 * Saves a meta array to a post, restricted to known keys.
	 */
	private function save_post_meta( int $post_id, array $meta, array $allowed_keys ) {
		foreach ( $allowed_keys as $key ) {
			if ( array_key_exists( $key, $meta ) ) {
				update_post_meta( $post_id, $key, $meta[ $key ] );
			}
		}
	}

	/**
	 * Remaps tag IDs stored as a comma-separated string.
	 */
	private function remap_tags_field( $tags_raw, array $tag_remap ): string {
		$raw = (string) $tags_raw;
		if ( $raw === '' ) {
			return '';
		}
		$ids     = array_map( 'intval', explode( ',', $raw ) );
		$new_ids = array_map( function( $id ) use ( $tag_remap ) {
			return $tag_remap[ $id ] ?? $id;
		}, $ids );
		return implode( ',', array_filter( $new_ids ) );
	}

	/**
	 * Gets column names for a table.
	 */
	private function get_table_columns( string $table ): array {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		global $wpdb;
		$rows = $wpdb->get_results( $wpdb->prepare( 'SHOW COLUMNS FROM %i', $table ), ARRAY_A );
		// phpcs:enable
		if ( ! is_array( $rows ) ) {
			return [];
		}
		return array_column( $rows, 'Field' );
	}

	// =========================================================================
	// Meta key manifests
	// =========================================================================

	private function invoice_meta_keys(): array {
		return [
			'_manfiet_invoice_no',
			'_manfiet_invoice_vendor_id',
			'_manfiet_invoice_client_id',
			'_manfiet_invoice_client_name',
			'_manfiet_invoice_date',
			'_manfiet_invoice_due_date',
			'_manfiet_invoice_address',
			'_manfiet_invoice_phone',
			'_manfiet_invoice_email',
			'_manfiet_invoice_email2',
			'_manfiet_invoice_bkash',
			'_manfiet_invoice_financial_account_ids',
			'_manfiet_invoice_bank_name',
			'_manfiet_invoice_bank_account',
			'_manfiet_invoice_bank_title',
			'_manfiet_invoice_bank_branch',
			'_manfiet_invoice_bank_routing',
			'_manfiet_invoice_bank_type',
			'_manfiet_invoice_service_desc',
			'_manfiet_invoice_notes',
			'_manfiet_invoice_exchange_rate',
			'_manfiet_invoice_discount_pct',
			'_manfiet_invoice_vat_pct',
			'_manfiet_invoice_adjustment_usd',
			'_manfiet_invoice_adjustment_bdt',
			'_manfiet_invoice_adjustment_note',
			'_manfiet_invoice_status',
			'_manfiet_invoice_campaigns',
			'_manfiet_invoice_previous_invoice_id',
			'_manfiet_invoice_previous_invoice_no',
			'_manfiet_invoice_previous_due_amount',
			'_manfiet_invoice_payment_received_amount',
			'_manfiet_invoice_share_token',
			'_manfiet_invoice_payment_link',
			'_manfiet_invoice_type',
			'_manfiet_service_type_label',
			'_manfiet_invoice_currency',
		];
	}

	private function vendor_meta_keys(): array {
		return [
			'_manfiet_vendor_company_name',
			'_manfiet_vendor_phone',
			'_manfiet_vendor_email',
			'_manfiet_vendor_address',
			'_manfiet_vendor_logo_url',
			'_manfiet_vendor_website_url',
			'_manfiet_vendor_tagline',
		];
	}

	private function client_meta_keys(): array {
		return [
			'_manfiet_client_name',
			'_manfiet_client_contact_person',
			'_manfiet_client_phone',
			'_manfiet_client_phones',
			'_manfiet_client_email',
			'_manfiet_client_email2',
			'_manfiet_client_emails',
			'_manfiet_client_address',
		];
	}

	private function account_meta_keys(): array {
		return [
			'_manfiet_account_type',
			'_manfiet_provider_name',
			'_manfiet_account_number',
			'_manfiet_holder_name',
			'_manfiet_routing_number',
			'_manfiet_account_notes',
			'_manfiet_swift_code',
			'_manfiet_iban',
			'_manfiet_branch_name',
			'_manfiet_branch_address',
		];
	}

	// =========================================================================
	// Dropdown helpers for the page selects
	// =========================================================================

	private function get_all_invoices_for_select(): array {
		$posts = get_posts( [
			'post_type'   => 'manfiet_invoice',
			'post_status' => 'any',
			'numberposts' => -1,
			'orderby'     => 'ID',
			'order'       => 'DESC',
		] );
		$result = [];
		foreach ( $posts as $p ) {
			$inv_no    = (string) get_post_meta( $p->ID, '_manfiet_invoice_no', true );
			$client    = (string) get_post_meta( $p->ID, '_manfiet_invoice_client_name', true );
			$label     = $inv_no ?: get_the_title( $p );
			if ( $client ) {
				$label .= ' — ' . $client;
			}
			$result[] = [ 'id' => $p->ID, 'label' => $label ];
		}
		return $result;
	}

	private function get_all_vendors_for_select(): array {
		$posts = get_posts( [
			'post_type'   => 'manfiet_vendor',
			'post_status' => 'any',
			'numberposts' => -1,
			'orderby'     => 'title',
			'order'       => 'ASC',
		] );
		$result = [];
		foreach ( $posts as $p ) {
			$company  = (string) get_post_meta( $p->ID, '_manfiet_vendor_company_name', true );
			$label    = $company ?: get_the_title( $p );
			$result[] = [ 'id' => $p->ID, 'label' => $label ];
		}
		return $result;
	}

	private function get_all_clients_for_select(): array {
		$posts = get_posts( [
			'post_type'   => 'manfiet_client',
			'post_status' => 'any',
			'numberposts' => -1,
			'orderby'     => 'title',
			'order'       => 'ASC',
		] );
		$result = [];
		foreach ( $posts as $p ) {
			$name     = (string) get_post_meta( $p->ID, '_manfiet_client_name', true );
			$label    = $name ?: get_the_title( $p );
			$result[] = [ 'id' => $p->ID, 'label' => $label ];
		}
		return $result;
	}
}
