<?php
/**
 * Tools admin controller.
 *
 * @package Awesomemotive\Rewardswp\Controllers\Admin
 * @since 1.0.0
 */

namespace Awesomemotive\Rewardswp\Controllers\Admin;

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

use Awesomemotive\Rewardswp\Factories\ViewsFactory;
use Awesomemotive\Rewardswp\Interfaces\ControllerInterface;
use Awesomemotive\Rewardswp\Interfaces\ViewInterface;
use Awesomemotive\Rewardswp\Services\DataImportService;
use Awesomemotive\Rewardswp\Services\ColumnMappingService;
use Awesomemotive\Rewardswp\Views\Admin\ToolsView;
use function Awesomemotive\Rewardswp\Helpers\is_rewardswp_admin_page;
use function Awesomemotive\Rewardswp\Helpers\get_member_by;
use function Awesomemotive\Rewardswp\Helpers\create_member;
use function Awesomemotive\Rewardswp\Helpers\create_point_entry;
use function Awesomemotive\Rewardswp\Helpers\rewardswp_log_error;


/**
 * Tools Admin Controller.
 *
 * @since 1.0.0
 */
class ToolsAdminController implements ControllerInterface {
	/**
	 * Admin submenu position.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const MENU_POSITION = 60;

	/**
	 * Admin submenu slug.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const MENU_SLUG = 'rewardswp-tools';

	/**
	 * AJAX nonce for tools actions.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const AJAX_NONCE = 'rewardswp-tools-nonce';

	/**
	 * CSV headers for points import.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	const CSV_HEADERS = [
		'member_id',
		'user_id',
		'email',
		'first_name',
		'last_name',
		'referral_code',
		'status',
		'points',
		'type',
		'source_type',
		'source_id',
		'status_points',
		'created_at',
		'expires_at',
		'note_public',
		'note_internal',
	];

	/**
	 * Views factory instance.
	 *
	 * @since 1.0.0
	 *
	 * @var ViewsFactory
	 */
	private ViewsFactory $views_factory;

	/**
	 * Data import service instance.
	 *
	 * @since 1.0.0
	 *
	 * @var DataImportService
	 */
	private DataImportService $data_import_service;

	/**
	 * Column mapping service instance.
	 *
	 * @since 1.0.0
	 *
	 * @var ColumnMappingService
	 */
	private ColumnMappingService $column_mapping_service;

	/**
	 * Construct.
	 *
	 * @since 1.0.0
	 *
	 * @param ViewsFactory         $views_factory
	 * @param DataImportService    $data_import_service
	 * @param ColumnMappingService $column_mapping_service
	 */
	public function __construct( ViewsFactory $views_factory, DataImportService $data_import_service, ColumnMappingService $column_mapping_service ) {
		$this->views_factory          = $views_factory;
		$this->data_import_service    = $data_import_service;
		$this->column_mapping_service = $column_mapping_service;
	}

	/**
	 * Get page title.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	public function get_page_title(): string {
		return esc_html__( 'Tools', 'rewardswp' );
	}

	/**
	 * Init.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'admin_menu', [ $this, 'add_admin_menu' ], self::MENU_POSITION );

		// AJAX actions.
		add_action( 'wp_ajax_rewardswp_download_csv_template', [ $this, 'ajax_download_csv_template' ] );
		add_action( 'wp_ajax_rewardswp_upload_csv_file', [ $this, 'ajax_upload_csv_file' ] );
		add_action( 'wp_ajax_rewardswp_load_csv_for_mapping', [ $this, 'ajax_load_csv_for_mapping' ] );
		add_action( 'wp_ajax_rewardswp_validate_column_mappings', [ $this, 'ajax_validate_column_mappings' ] );
		add_action( 'wp_ajax_rewardswp_process_csv_batch', [ $this, 'ajax_process_csv_batch' ] );
		add_action( 'wp_ajax_rewardswp_download_error_report', [ $this, 'ajax_download_error_report' ] );
	}

	/**
	 * Register the menu.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function add_admin_menu() {
		add_submenu_page(
			MainAdminController::MAIN_MENU_SLUG,
			$this->get_page_title(),
			$this->get_page_title(),
			'manage_options',
			self::MENU_SLUG,
			[ $this, 'render_page' ],
			self::MENU_POSITION
		);
	}

	/**
	 * Render the page.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function render_page(): void {
		/**
		 * Tools view instance.
		 *
		 * @var ViewInterface $view
		 */
		$view = $this->views_factory->get_view( ToolsView::class );
		$view->render();
	}

	/**
	 * Handle AJAX request to download CSV template.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function ajax_download_csv_template(): void {
		check_ajax_referer( self::AJAX_NONCE, '_ajax_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ),
				]
			);
		}

		$csv_content = $this->generate_csv_template();
		$filename    = sanitize_file_name( 'rewardswp-points-import-template-' . gmdate( 'Y-m-d' ) . '.csv' );

		// Set headers for file download.
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Cache-Control: no-cache, must-revalidate' );
		header( 'Pragma: no-cache' );

		// Output CSV content (streaming).
		try {
			$tmp = new \SplTempFileObject();
			$tmp->fwrite( $csv_content );
			$tmp->rewind();
			$tmp->fpassthru();
		} catch ( \RuntimeException $e ) {
			wp_die( esc_html__( 'Unable to generate CSV template.', 'rewardswp' ), 500 );
		}
		exit;
	}

	/**
	 * Get CSV headers for points import.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public static function get_csv_headers(): array {
		return self::CSV_HEADERS;
	}

	/**
	 * Generate CSV template content.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function generate_csv_template(): string {
		$headers = self::get_csv_headers();

		$sample_data = [
			[
				'',         // member_id
				'1',        // user_id
				'john@example.com',  // email
				'John',     // first_name
				'Doe',      // last_name
				'',         // referral_code
				'active',   // status
				'100',      // points
				'earned',   // type
				'import',   // source_type
				'',         // source_id
				'approved', // status_points
				'2024-01-01 10:00:00',  // created_at
				'',         // expires_at
				'Welcome bonus',  // note_public
				'Imported from Old Platform',  // note_internal
			],
			[
				'',         // member_id
				'',         // user_id
				'jane@example.com',  // email
				'Jane',     // first_name
				'Smith',    // last_name
				'',         // referral_code
				'active',   // status
				'50',       // points
				'earned',   // type
				'import',   // source_type
				'',         // source_id
				'approved', // status_points
				'2024-01-02 14:30:00',  // created_at
				'2024-12-31 23:59:59',  // expires_at
				'Purchase reward',  // note_public
				'Order #123',  // note_internal
			],
			[
				'',         // member_id
				'2',        // user_id
				'bob@example.com',  // email
				'Bob',      // first_name
				'Johnson',  // last_name
				'',         // referral_code
				'active',   // status
				'-25',      // points
				'spent',    // type
				'import',   // source_type
				'',         // source_id
				'approved', // status_points
				'2024-01-15 14:30:00',  // created_at
				'',         // expires_at
				'Discount redemption',  // note_public
				'Imported from Old Platform',  // note_internal
			],
		];

		$csv_content = '';

		// Add headers.
		$csv_content .= implode( ',', $headers ) . "\n";

		// Add sample data.
		foreach ( $sample_data as $row ) {
			$csv_content .= '"' . implode( '","', $row ) . '"' . "\n";
		}

		return $csv_content;
	}

	/**
	 * Handle AJAX request to upload CSV file.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function ajax_upload_csv_file(): void {
		check_ajax_referer( self::AJAX_NONCE, '_ajax_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ),
				]
			);
		}

		// Check if file was uploaded.
		if ( empty( $_FILES['csv_file'] ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'No file uploaded.', 'rewardswp' ),
				]
			);
		}

		// File is validated by DataImportService->validate_uploaded_file() before use.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$file = $_FILES['csv_file'];

		// Validate file using DataImportService.
		$validation = $this->data_import_service->validate_uploaded_file( $file, [ 'csv' ] );
		if ( is_wp_error( $validation ) ) {
			wp_send_json_error(
				[
					'message' => $validation->get_error_message(),
				]
			);
		}

		// Parse CSV file using DataImportService.
		$csv_data = $this->data_import_service->parse_csv_file(
			$file['tmp_name'],
			self::get_csv_headers()
		);

		if ( is_wp_error( $csv_data ) ) {
			wp_send_json_error(
				[
					'message' => $csv_data->get_error_message(),
				]
			);
		}

		wp_send_json_success(
			[
				'message' => esc_html__( 'CSV file processed successfully.', 'rewardswp' ),
				'data'    => $csv_data,
			]
		);
	}

	/**
	 * Handle AJAX request to load CSV file for column mapping.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function ajax_load_csv_for_mapping(): void {
		check_ajax_referer( self::AJAX_NONCE, '_ajax_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ),
				]
			);
		}

		// Validate file upload.
		if ( empty( $_FILES['csv_file'] ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'No file uploaded.', 'rewardswp' ),
				]
			);
		}

		// File is validated by DataImportService->validate_uploaded_file() before use.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$file = $_FILES['csv_file'];

		// Validate file using DataImportService.
		$validation = $this->data_import_service->validate_uploaded_file( $file, [ 'csv' ] );
		if ( is_wp_error( $validation ) ) {
			wp_send_json_error(
				[
					'message' => $validation->get_error_message(),
				]
			);
		}

		// Parse CSV file without requiring specific headers.
		$csv_data = $this->data_import_service->parse_csv_file(
			$file['tmp_name'],
			[], // No required headers for mapping
			10  // Preview limit
		);

		if ( is_wp_error( $csv_data ) ) {
			wp_send_json_error(
				[
					'message' => $csv_data->get_error_message(),
				]
			);
		}

		// Auto-detect column mappings.
		$auto_mappings = $this->column_mapping_service->auto_detect_mappings( $csv_data['headers'] );

		wp_send_json_success(
			[
				'headers'       => $csv_data['headers'],
				'preview'       => $csv_data['preview'],
				'total_rows'    => $csv_data['total_rows'],
				'auto_mappings' => $auto_mappings,
			]
		);
	}

	/**
	 * Handle AJAX request to validate column mappings.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function ajax_validate_column_mappings(): void {
		check_ajax_referer( self::AJAX_NONCE, '_ajax_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ),
				]
			);
		}

		$mappings_json = sanitize_text_field( wp_unslash( $_POST['mappings'] ?? '' ) );
		$mappings      = json_decode( $mappings_json, true );

		if ( ! is_array( $mappings ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Invalid mappings data.', 'rewardswp' ),
				]
			);
		}

		// Filter out empty mappings - include both empty strings and 'skip' values.
		$filtered_mappings = array_filter(
			$mappings,
			function ( $value ) {
				return ! empty( $value ) && 'skip' !== $value && trim( $value ) !== '';
			}
		);

		// Validate mappings using ColumnMappingService.
		$validation = $this->column_mapping_service->validate_mappings( $filtered_mappings );

		if ( is_wp_error( $validation ) ) {
			wp_send_json_error(
				[
					'message' => $validation->get_error_message(),
					'errors'  => $validation->get_error_data() ?: [ $validation->get_error_message() ],
				]
			);
		}

		wp_send_json_success(
			[
				'message' => esc_html__( 'Column mappings are valid.', 'rewardswp' ),
			]
		);
	}

	/**
	 * Get maximum upload size from DataImportService.
	 *
	 * @since 1.0.0
	 *
	 * @return int Maximum file size in MB.
	 */
	public function get_max_upload_size_mb(): int {
		return $this->data_import_service->get_max_upload_size_mb();
	}

	/**
	 * Handle AJAX request to process CSV batch import.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function ajax_process_csv_batch(): void {
		check_ajax_referer( self::AJAX_NONCE, '_ajax_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ),
				],
				403
			);
		}

		// Get parameters.
		$batch_size = absint( wp_unslash( $_POST['batch_size'] ?? 50 ) );
		$offset     = absint( wp_unslash( $_POST['offset'] ?? 0 ) );

		// Don't sanitize JSON string with sanitize_text_field as it corrupts JSON.
		// JSON is validated by json_decode() and subsequent validation logic.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$mappings_json   = wp_unslash( $_POST['column_mappings'] ?? '' );
		$column_mappings = json_decode( $mappings_json, true );

		if ( ! is_array( $column_mappings ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Invalid column mappings data.', 'rewardswp' ),
				],
				400
			);
		}

		// Filter out empty mappings (same as frontend does).
		$column_mappings = array_filter(
			$column_mappings,
			function ( $value ) {
				return ! empty( $value ) && 'skip' !== $value && trim( $value ) !== '';
			}
		);

		// Validate column mappings.
		$mapping_validation = $this->column_mapping_service->validate_mappings( $column_mappings );
		if ( is_wp_error( $mapping_validation ) ) {
			wp_send_json_error(
				[
					'message' => $mapping_validation->get_error_message(),
				],
				400
			);
		}

		// Validate file upload.
		if ( empty( $_FILES['csv_file'] ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'No file uploaded.', 'rewardswp' ),
				],
				400
			);
		}

		// File is validated by DataImportService->validate_uploaded_file() before use.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$file = $_FILES['csv_file'];

		// Validate file using DataImportService.
		$validation = $this->data_import_service->validate_uploaded_file( $file, [ 'csv' ] );
		if ( is_wp_error( $validation ) ) {
			wp_send_json_error(
				[
					'message' => $validation->get_error_message(),
				],
				400
			);
		}

		// Process CSV batch using DataImportService.
		$batch_generator = $this->data_import_service->process_csv_in_batches(
			$file['tmp_name'],
			$batch_size,
			$offset
		);

		if ( is_wp_error( $batch_generator ) ) {
			wp_send_json_error(
				[
					'message' => $batch_generator->get_error_message(),
				],
				400
			);
		}

		$processed_count   = 0;
		$created_members   = 0;
		$created_points    = 0;
		$errors            = [];
		$current_row_index = $offset + 1; // Start from 1 (header not counted)
		$total_batch_items = 0;

		// Process the current batch.
		foreach ( $batch_generator as $batch ) {
			$total_batch_items = count( $batch );
			foreach ( $batch as $row ) {
				++$current_row_index;

				// Apply column mappings to transform the row.
				$mapped_row = $this->column_mapping_service->apply_mappings( $row, $column_mappings );
				$result     = $this->process_csv_row( $mapped_row );

				if ( is_wp_error( $result ) ) {
					// Not escaped here — rendered via Alpine x-text, which escapes on output
					// (and the message may contain member names/emails with apostrophes).
					$errors[] = sprintf(
						/* translators: %1$d: row number, %2$s: error message */
						__( 'Row %1$d: %2$s', 'rewardswp' ),
						$current_row_index,
						$result->get_error_message()
					);
					// Continue processing even with errors.
					continue;
				}

				++$processed_count;
				if ( $result['member_created'] ) {
					++$created_members;
				}
				if ( $result['point_created'] ) {
					++$created_points;
				}
			}
			break; // Only process one batch per request
		}

		wp_send_json_success(
			[
				'message'         => sprintf(
					/* translators: %1$d: successful rows, %2$d: total rows attempted */
					esc_html__( 'Processed %1$d of %2$d rows in this batch.', 'rewardswp' ),
					$processed_count,
					$total_batch_items
				),
				'batch_processed' => $processed_count,
				'batch_attempted' => $total_batch_items,
				'total_processed' => $offset + $total_batch_items,
				'created_members' => $created_members,
				'created_points'  => $created_points,
				'errors'          => $errors,
				'has_more'        => $total_batch_items === $batch_size, // More batches if we got a full batch
			]
		);
	}

	/**
	 * Create a WordPress user for import.
	 *
	 * @since 1.0.0
	 *
	 * @param string $email      User email.
	 * @param string $first_name User first name.
	 * @param string $last_name  User last name.
	 *
	 * @return int|\WP_Error User ID on success, WP_Error on failure.
	 */
	private function create_wp_user_for_import( string $email, string $first_name = '', string $last_name = '' ) {
		// Check if user already exists.
		$existing_user = get_user_by( 'email', $email );
		if ( $existing_user ) {
			return new \WP_Error(
				'user_already_exists',
				sprintf(
					/* translators: %s: email address */
					esc_html__( 'User with email %s already exists.', 'rewardswp' ),
					$email
				)
			);
		}

		// Generate username from email.
		$username          = sanitize_user( current( explode( '@', $email ) ) );
		$original_username = $username;
		$counter           = 1;

		// Ensure username is unique.
		while ( username_exists( $username ) ) {
			$username = $original_username . $counter;
			++$counter;
		}

		// Generate random password.
		$password = wp_generate_password( 12, false );

		// Create user data.
		$user_data = [
			'user_login'   => $username,
			'user_email'   => $email,
			'user_pass'    => $password,
			'first_name'   => $first_name,
			'last_name'    => $last_name,
			'display_name' => trim( $first_name . ' ' . $last_name ) ?: $username,
			'role'         => 'subscriber', // Default role for imported users
		];

		$user_id = wp_insert_user( $user_data );

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

		return $user_id;
	}

	/**
	 * Process a single CSV row to create member and points.
	 *
	 * @since 1.0.0
	 *
	 * @param array $row CSV row data.
	 *
	 * @return array|\WP_Error Processing result or error.
	 */
	private function process_csv_row( array $row ) {
		$member_created = false;
		$point_created  = false;

		// Member resolution with precedence: member_id → user_id → email + first_name.
		$member = null;

		// 1. Try to get member by member_id (highest precedence).
		if ( ! empty( $row['member_id'] ) && is_numeric( $row['member_id'] ) ) {
			$member = get_member_by( 'id', (int) $row['member_id'] );
		}

		// 2. Try to get member by User ID.
		if ( ! $member && ! empty( $row['user_id'] ) && is_numeric( $row['user_id'] ) ) {
			$member = get_member_by( 'user_id', (int) $row['user_id'] );

			// If no member exists for this WP user, create one.
			$wp_user = get_user_by( 'ID', (int) $row['user_id'] );
			if ( ! $member && $wp_user && $wp_user instanceof \WP_User ) {
				$member_data = [
					'user_id'    => $wp_user->ID,
					'email'      => $wp_user->user_email,
					'first_name' => sanitize_text_field( $row['first_name'] ?? get_user_meta( $wp_user->ID, 'first_name', true ) ),
					'last_name'  => sanitize_text_field( $row['last_name'] ?? get_user_meta( $wp_user->ID, 'last_name', true ) ),
					'status'     => sanitize_text_field( $row['status'] ?? 'active' ),
				];

				// Add additional member fields if provided.
				if ( ! empty( $row['referral_code'] ) ) {
					$referral_code = sanitize_text_field( $row['referral_code'] );

					// Skip numeric-only referral codes as they're not allowed.
					// Let the system generate a new one automatically.
					if ( ! is_numeric( $referral_code ) ) {
						$member_data['referral_code'] = $referral_code;
					}
					// If referral_code is numeric, don't include it so system generates a new one.
				}

				$member = create_member( $member_data );
				if ( is_wp_error( $member ) ) {
					return new \WP_Error(
						'member_creation_failed',
						sprintf(
							/* translators: %1$s: user ID, %2$s: error message */
							esc_html__( 'Failed to create member for user ID %1$s: %2$s', 'rewardswp' ),
							$row['user_id'],
							$member->get_error_message()
						)
					);
				}
				$member_created = true;
			}
		}

		// 3. Try to get member by email and create if necessary.
		if ( ! $member && ! empty( $row['email'] ) ) {
			$email      = sanitize_email( $row['email'] );
			$first_name = sanitize_text_field( $row['first_name'] ?? '' );
			$last_name  = sanitize_text_field( $row['last_name'] ?? '' );

			// Validate required fields for email-based member creation.
			if ( empty( $first_name ) ) {
				return new \WP_Error(
					'missing_first_name',
					esc_html__( 'First name is required when creating members by email.', 'rewardswp' )
				);
			}

			// Try to get existing member by email first.
			$member = get_member_by( 'email', $email );

			if ( ! $member ) {
				// Check if WP user exists with this email.
				$wp_user    = get_user_by( 'email', $email );
				$wp_user_id = null;

				if ( $wp_user ) {
					$wp_user_id = $wp_user->ID;
				} else {
					// Create WP User first.
					$wp_user_id = $this->create_wp_user_for_import( $email, $first_name, $last_name );
					if ( is_wp_error( $wp_user_id ) ) {
						return new \WP_Error(
							'wp_user_creation_failed',
							sprintf(
								/* translators: %1$s: email, %2$s: error message */
								esc_html__( 'Failed to create WP User for %1$s: %2$s', 'rewardswp' ),
								$email,
								$wp_user_id->get_error_message()
							)
						);
					}
				}

				// Create member associated with WP User.
				$member_data = [
					'user_id'    => $wp_user_id,
					'email'      => $email,
					'first_name' => $first_name,
					'last_name'  => $last_name,
					'status'     => sanitize_text_field( $row['status'] ?? 'active' ),
				];

				// Add additional member fields if provided.
				if ( ! empty( $row['referral_code'] ) ) {
					$referral_code = sanitize_text_field( $row['referral_code'] );

					// Skip numeric-only referral codes as they're not allowed.
					// Let the system generate a new one automatically.
					if ( ! is_numeric( $referral_code ) ) {
						$member_data['referral_code'] = $referral_code;
					}
					// If referral_code is numeric, don't include it so system generates a new one.
				}

				$member = create_member( $member_data );
				if ( is_wp_error( $member ) ) {
					// Clean up WP User if member creation fails and we created it.
					if ( ! $wp_user ) {
						wp_delete_user( $wp_user_id );
					}
					return new \WP_Error(
						'member_creation_failed',
						sprintf(
							/* translators: %1$s: email, %2$s: error message */
							esc_html__( 'Failed to create member for %1$s: %2$s', 'rewardswp' ),
							$email,
							$member->get_error_message()
						)
					);
				}
				$member_created = true;
			}
		}

		if ( ! $member ) {
			return new \WP_Error(
				'no_member',
				esc_html__( 'Could not find or create member. Please provide either member_id, user_id, or email with first_name.', 'rewardswp' )
			);
		}

		// Create point entry.
		$points = absint( $row['points'] ?? 0 );
		if ( 0 === $points ) {
			return new \WP_Error(
				'invalid_points',
				esc_html__( 'Points value is required and must be non-zero.', 'rewardswp' )
			);
		}

		$point_data = [
			'member_id'     => $member->id,
			'points'        => $points,
			'type'          => sanitize_text_field( $row['type'] ?? 'earned' ),
			'source_type'   => sanitize_text_field( $row['source_type'] ?? 'import' ),
			'source_id'     => absint( $row['source_id'] ?? 0 ),
			'status'        => sanitize_text_field( $row['status_points'] ?? 'approved' ),
			'note_public'   => sanitize_text_field( $row['note_public'] ?? '' ),
			'note_internal' => sanitize_text_field( $row['note_internal'] ?? '' ),
		];

		// Handle created_at date.
		if ( ! empty( $row['created_at'] ) ) {
			$created_at = sanitize_text_field( $row['created_at'] );
			// Validate date format.
			$date = \DateTime::createFromFormat( 'Y-m-d H:i:s', $created_at );
			if ( $date ) {
				$point_data['created_at'] = $created_at;
			}
		}

		// Handle expires_at date.
		if ( ! empty( $row['expires_at'] ) ) {
			$expires_at = sanitize_text_field( $row['expires_at'] );
			// Validate date format.
			$date = \DateTime::createFromFormat( 'Y-m-d H:i:s', $expires_at );
			if ( $date ) {
				$point_data['expires_at'] = $expires_at;
			}
		}

		$point = create_point_entry( $point_data, false ); // Don't send notification emails for imports
		if ( is_wp_error( $point ) ) {
			return $point;
		}

		$point_created = true;

		// Update member's points balance.
		try {
			if ( 'earned' === $point_data['type'] ) {
				$member->increase_points_balance( $points );
			} elseif ( 'spent' === $point_data['type'] && $points < 0 ) {
				// For negative points (spending), convert to positive for deduction.

				// Allow negative balance during import to handle historical data.
				$member->deduct_points_balance( abs( $points ), true, true );
			} elseif ( 'spent' === $point_data['type'] && $points > 0 ) {
				// For positive points with 'spent' type, treat as earning back points.
				$member->increase_points_balance( $points );
			}
		} catch ( \Exception $e ) {
			// Log the balance update error but don't fail the import.
			rewardswp_log_error(
				'Import failed to update balance for member {member_id}: {message}',
				[
					'member_id' => $member->id ?? 0,
					'message'   => $e->getMessage(),
				]
			);
		}

		return [
			'member_created' => $member_created,
			'point_created'  => $point_created,
			'member'         => $member,
			'point'          => $point,
		];
	}

	/**
	 * Handle AJAX request to download error report CSV.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function ajax_download_error_report(): void {
		check_ajax_referer( self::AJAX_NONCE, '_ajax_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'Insufficient permissions.', 'rewardswp' ),
				],
				403
			);
		}

		// Get errors from POST data.
		// Errors array is JSON-encoded, validate with json_decode().
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$errors = json_decode( wp_unslash( $_POST['errors'] ?? '[]' ), true );

		if ( empty( $errors ) ) {
			wp_send_json_error(
				[
					'message' => esc_html__( 'No errors to export.', 'rewardswp' ),
				],
				400
			);
		}

		$filename = 'rewardswp-import-errors-' . gmdate( 'Y-m-d-H-i-s' ) . '.csv';

		// Set headers for file download.
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename=' . $filename );
		header( 'Pragma: no-cache' );
		header( 'Expires: 0' );

		// Create file handle.
		$output = fopen( 'php://output', 'w' );

		// Write CSV headers.
		fputcsv( $output, [ 'Row', 'Error Message', 'Timestamp' ] );

		// Write error data.
		foreach ( $errors as $error ) {
			// Extract row number and message from error string.
			if ( preg_match( '/^Row (\d+): (.+)$/', $error, $matches ) ) {
				$row_number    = $matches[1];
				$error_message = $matches[2];
			} else {
				$row_number    = 'Unknown';
				$error_message = $error;
			}

			fputcsv(
				$output,
				[
					$row_number,
					$error_message,
					gmdate( 'Y-m-d H:i:s' ),
				]
			);
		}

		// Stream will be closed automatically when script exits.
		exit;
	}
}
