<?php

/**
 * Data Import Class
 *
 * Handles importing plugin data from JSON or CSV format.
 *
 * @package    Karate_Club_Manager
 * @subpackage Karate_Club_Manager/includes/classes
 * @since      1.0.271
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
    // Exit if accessed directly.
}
/**
 * MACM Data Import Class
 *
 * Orchestrates data import operations including validation, parsing, and import.
 *
 * @since 1.0.271
 */
class MACM_Data_Import {
    /**
     * Maximum allowed file size in bytes (10MB).
     *
     * @since 1.0.271
     * @var int
     */
    const MAX_FILE_SIZE = 10485760;

    /**
     * Allowed file extensions.
     *
     * @since 1.0.271
     * @var array
     */
    const ALLOWED_EXTENSIONS = array('json', 'csv');

    /**
     * Allowed MIME types.
     *
     * @since 1.0.271
     * @var array
     */
    const ALLOWED_MIME_TYPES = array(
        'application/json',
        'text/json',
        'text/csv',
        'application/csv',
        'text/plain'
    );

    /**
     * Parsed import data.
     *
     * @since 1.0.271
     * @var array
     */
    private $data = array();

    /**
     * Import errors collected during processing.
     *
     * @since 1.0.271
     * @var array
     */
    private $errors = array();

    /**
     * Data mapper instance.
     *
     * @since 1.0.271
     * @var MACM_Data_Mapper|null
     */
    private $mapper = null;

    /**
     * Validate an uploaded import file.
     *
     * Checks file extension, MIME type, size, and upload errors.
     *
     * @since 1.0.271
     * @param array $file The $_FILES array element for the uploaded file.
     * @return true|WP_Error True if valid, WP_Error on failure.
     */
    public function validate_file( $file ) {
        // Check for upload errors.
        if ( !isset( $file['error'] ) || UPLOAD_ERR_OK !== $file['error'] ) {
            $error_message = $this->get_upload_error_message( $file['error'] ?? UPLOAD_ERR_NO_FILE );
            return new WP_Error('upload_error', $error_message);
        }
        // Check file exists.
        if ( empty( $file['tmp_name'] ) || !is_uploaded_file( $file['tmp_name'] ) ) {
            return new WP_Error('no_file', __( 'No file was uploaded.', 'martial-arts-club-manager' ));
        }
        // Check file size.
        if ( $file['size'] > self::MAX_FILE_SIZE ) {
            return new WP_Error('file_too_large', sprintf( 
                /* translators: %s: maximum file size */
                __( 'File exceeds maximum size of %s.', 'martial-arts-club-manager' ),
                size_format( self::MAX_FILE_SIZE )
             ));
        }
        // Check file extension.
        $extension = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
        if ( !in_array( $extension, self::ALLOWED_EXTENSIONS, true ) ) {
            return new WP_Error('invalid_extension', sprintf( 
                /* translators: %s: allowed extensions */
                __( 'Invalid file type. Allowed types: %s.', 'martial-arts-club-manager' ),
                implode( ', ', self::ALLOWED_EXTENSIONS )
             ));
        }
        // Check MIME type using WordPress function.
        $file_type = wp_check_filetype( $file['name'], array(
            'json' => 'application/json',
            'csv'  => 'text/csv',
        ) );
        if ( empty( $file_type['ext'] ) ) {
            return new WP_Error('invalid_type', __( 'File type not allowed.', 'martial-arts-club-manager' ));
        }
        // Validate file content is not empty.
        if ( 0 === $file['size'] ) {
            return new WP_Error('empty_file', __( 'The uploaded file is empty.', 'martial-arts-club-manager' ));
        }
        return true;
    }

    /**
     * Get human-readable upload error message.
     *
     * @since 1.0.271
     * @param int $error_code PHP upload error code.
     * @return string Error message.
     */
    private function get_upload_error_message( $error_code ) {
        $messages = array(
            UPLOAD_ERR_INI_SIZE   => __( 'The uploaded file exceeds the server upload limit.', 'martial-arts-club-manager' ),
            UPLOAD_ERR_FORM_SIZE  => __( 'The uploaded file exceeds the form size limit.', 'martial-arts-club-manager' ),
            UPLOAD_ERR_PARTIAL    => __( 'The file was only partially uploaded.', 'martial-arts-club-manager' ),
            UPLOAD_ERR_NO_FILE    => __( 'No file was uploaded.', 'martial-arts-club-manager' ),
            UPLOAD_ERR_NO_TMP_DIR => __( 'Server missing temporary folder.', 'martial-arts-club-manager' ),
            UPLOAD_ERR_CANT_WRITE => __( 'Failed to write file to disk.', 'martial-arts-club-manager' ),
            UPLOAD_ERR_EXTENSION  => __( 'A PHP extension stopped the file upload.', 'martial-arts-club-manager' ),
        );
        return $messages[$error_code] ?? __( 'Unknown upload error.', 'martial-arts-club-manager' );
    }

    /**
     * Parse a JSON import file.
     *
     * Validates the JSON structure and extracts import data.
     * Uses WP_Filesystem for WordPress Plugin Check compliance.
     *
     * @since 1.0.271
     * @since 1.0.297 Refactored to use WP_Filesystem for WordPress Plugin Check compliance.
     * @param string $file_path Path to the JSON file.
     * @return array|WP_Error Parsed data array or WP_Error on failure.
     */
    public function parse_json( $file_path ) {
        // Verify file is readable.
        if ( !is_readable( $file_path ) ) {
            return new WP_Error('read_error', __( 'Failed to read the import file.', 'martial-arts-club-manager' ));
        }
        // Read file contents using WP_Filesystem for WordPress Plugin Check compliance.
        global $wp_filesystem;
        if ( empty( $wp_filesystem ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }
        $contents = $wp_filesystem->get_contents( $file_path );
        if ( false === $contents ) {
            return new WP_Error('read_error', __( 'Failed to read the import file.', 'martial-arts-club-manager' ));
        }
        // Decode JSON.
        $data = json_decode( $contents, true );
        if ( null === $data && JSON_ERROR_NONE !== json_last_error() ) {
            return new WP_Error('json_error', sprintf( 
                /* translators: %s: JSON error message */
                __( 'Invalid JSON file: %s', 'martial-arts-club-manager' ),
                json_last_error_msg()
             ));
        }
        // Validate it's a MACM export file.
        if ( !isset( $data['macm_export'] ) ) {
            return new WP_Error('invalid_format', __( 'This file does not appear to be a valid Martial Arts Club Manager export.', 'martial-arts-club-manager' ));
        }
        // Validate format version.
        $format_version = $data['macm_export']['format_version'] ?? '0';
        if ( version_compare( $format_version, '1.0', '<' ) ) {
            return new WP_Error('unsupported_version', __( 'This export file format is not supported.', 'martial-arts-club-manager' ));
        }
        $this->data = $data;
        return $data;
    }

    /**
     * Parse a CSV import file.
     *
     * Reads CSV data and converts to entity array format.
     * Uses WP_Filesystem and str_getcsv() to satisfy WordPress Plugin Check
     * requirements for WP_Filesystem-compatible file operations.
     *
     * @since 1.0.271
     * @since 1.0.290 Refactored to use file_get_contents() instead of fopen/fgetcsv.
     * @since 1.0.297 Refactored to use WP_Filesystem for WordPress Plugin Check compliance.
     * @param string $file_path Path to the CSV file.
     * @param string $entity    Target entity type for this CSV (e.g., 'members').
     * @return array|WP_Error Parsed data array or WP_Error on failure.
     */
    public function parse_csv( $file_path, $entity = 'members' ) {
        // Validate file exists and is readable.
        if ( !file_exists( $file_path ) || !is_readable( $file_path ) ) {
            return new WP_Error('read_error', __( 'Failed to open the import file.', 'martial-arts-club-manager' ));
        }
        // Read entire file contents using WP_Filesystem for WordPress Plugin Check compliance.
        global $wp_filesystem;
        if ( empty( $wp_filesystem ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }
        $content = $wp_filesystem->get_contents( $file_path );
        if ( false === $content ) {
            return new WP_Error('read_error', __( 'Failed to read the import file.', 'martial-arts-club-manager' ));
        }
        // Remove UTF-8 BOM if present (first 3 bytes: EF BB BF).
        if ( 0 === strpos( $content, "﻿" ) ) {
            $content = substr( $content, 3 );
        }
        // Normalize line endings to \n for consistent parsing.
        $content = str_replace( array("\r\n", "\r"), "\n", $content );
        // Split into lines.
        $lines = explode( "\n", $content );
        if ( empty( $lines ) ) {
            return new WP_Error('invalid_csv', __( 'Invalid CSV file: no content found.', 'martial-arts-club-manager' ));
        }
        // Parse header row.
        $header_line = array_shift( $lines );
        $headers = str_getcsv( $header_line );
        if ( empty( $headers ) || 1 === count( $headers ) && empty( $headers[0] ) ) {
            return new WP_Error('invalid_csv', __( 'Invalid CSV file: no header row found.', 'martial-arts-club-manager' ));
        }
        // Sanitize headers.
        $headers = array_map( 'sanitize_key', $headers );
        // Read data rows.
        $rows = array();
        $line = 1;
        foreach ( $lines as $line_content ) {
            ++$line;
            // Skip empty lines.
            if ( '' === trim( $line_content ) ) {
                continue;
            }
            // Parse CSV row.
            $row = str_getcsv( $line_content );
            // Skip empty rows.
            if ( empty( $row ) || 1 === count( $row ) && empty( $row[0] ) ) {
                continue;
            }
            // Check column count matches headers.
            if ( count( $row ) !== count( $headers ) ) {
                $this->errors[] = sprintf(
                    /* translators: 1: line number, 2: expected columns, 3: actual columns */
                    __( 'Line %1$d: Expected %2$d columns, found %3$d.', 'martial-arts-club-manager' ),
                    $line,
                    count( $headers ),
                    count( $row )
                );
                continue;
            }
            // Combine headers with values.
            $rows[] = array_combine( $headers, $row );
        }
        if ( empty( $rows ) ) {
            return new WP_Error('empty_csv', __( 'No data rows found in CSV file.', 'martial-arts-club-manager' ));
        }
        // Build data structure similar to JSON format.
        $this->data = array(
            'macm_export' => array(
                'version'           => MACM_VERSION,
                'format_version'    => '1.0',
                'exported_at'       => gmdate( 'c' ),
                'site_url'          => 'csv_import',
                'entities_included' => array($entity),
            ),
            $entity       => $rows,
        );
        return $this->data;
    }

    /**
     * Get import preview with entity counts.
     *
     * Analyzes the parsed data and returns a summary of what will be imported.
     *
     * @since 1.0.271
     * @param array|null $data Optional data array. Uses internal data if not provided.
     * @return array Preview array with counts and metadata.
     */
    public function get_import_preview( $data = null ) {
        if ( null === $data ) {
            $data = $this->data;
        }
        $preview = array(
            'metadata' => array(),
            'entities' => array(),
            'warnings' => array(),
            'errors'   => $this->errors,
        );
        // Extract metadata.
        if ( isset( $data['macm_export'] ) ) {
            $preview['metadata'] = array(
                'version'        => $data['macm_export']['version'] ?? __( 'Unknown', 'martial-arts-club-manager' ),
                'format_version' => $data['macm_export']['format_version'] ?? '1.0',
                'exported_at'    => $data['macm_export']['exported_at'] ?? '',
                'source_site'    => $data['macm_export']['site_url'] ?? '',
            );
        }
        // Count entities.
        $entity_types = array(
            'settings'            => __( 'Settings', 'martial-arts-club-manager' ),
            'belt_colors'         => __( 'Belt Colors', 'martial-arts-club-manager' ),
            'membership_types'    => __( 'Membership Types', 'martial-arts-club-manager' ),
            'members'             => __( 'Members', 'martial-arts-club-manager' ),
            'trial_bookings'      => __( 'Trial Bookings', 'martial-arts-club-manager' ),
            'locations'           => __( 'Locations', 'martial-arts-club-manager' ),
            'groups'              => __( 'Groups', 'martial-arts-club-manager' ),
            'clubs'               => __( 'Clubs', 'martial-arts-club-manager' ),
            'instructors'         => __( 'Instructors', 'martial-arts-club-manager' ),
            'classes'             => __( 'Classes', 'martial-arts-club-manager' ),
            'class_enrollments'   => __( 'Class Enrollments', 'martial-arts-club-manager' ),
            'attendance'          => __( 'Attendance Records', 'martial-arts-club-manager' ),
            'events'              => __( 'Events', 'martial-arts-club-manager' ),
            'event_registrations' => __( 'Event Registrations', 'martial-arts-club-manager' ),
            'training_videos'     => __( 'Training Videos', 'martial-arts-club-manager' ),
            'grading_history'     => __( 'Grading History', 'martial-arts-club-manager' ),
            'member_groups'       => __( 'Member Group Assignments', 'martial-arts-club-manager' ),
            'class_instructors'   => __( 'Class Instructor Assignments', 'martial-arts-club-manager' ),
        );
        foreach ( $entity_types as $key => $label ) {
            if ( isset( $data[$key] ) ) {
                $count = ( 'settings' === $key ? count( $data[$key] ) : count( $data[$key] ) );
                $preview['entities'][$key] = array(
                    'label'   => $label,
                    'count'   => $count,
                    'premium' => $this->is_premium_entity( $key ),
                );
            }
        }
        // Check for user mapping requirements.
        if ( isset( $data['members'] ) && !empty( $data['members'] ) ) {
            $users_to_match = array();
            foreach ( $data['members'] as $member ) {
                if ( !empty( $member['user_email'] ) ) {
                    $users_to_match[] = $member['user_email'];
                }
            }
            if ( !empty( $users_to_match ) ) {
                $matched = $this->count_matching_users( $users_to_match );
                $preview['user_mapping'] = array(
                    'total_members'  => count( $data['members'] ),
                    'emails_found'   => count( $users_to_match ),
                    'matching_users' => $matched,
                    'unmatched'      => count( $users_to_match ) - $matched,
                );
                if ( $matched < count( $users_to_match ) ) {
                    $preview['warnings'][] = sprintf( 
                        /* translators: %d: number of unmatched users */
                        __( '%d members have email addresses that do not match any existing users.', 'martial-arts-club-manager' ),
                        count( $users_to_match ) - $matched
                     );
                }
            }
        }
        // Check for premium entities when user is free.
        $can_use_premium = function_exists( 'macm_fs' ) && macm_fs()->can_use_premium_code();
        if ( !$can_use_premium ) {
            $premium_entities = array();
            foreach ( $preview['entities'] as $key => $entity_data ) {
                if ( $entity_data['premium'] ) {
                    $premium_entities[] = $entity_data['label'];
                }
            }
            if ( !empty( $premium_entities ) ) {
                $preview['warnings'][] = sprintf( 
                    /* translators: %s: comma-separated list of entity types */
                    __( 'The following data types require a premium license and will be skipped: %s', 'martial-arts-club-manager' ),
                    implode( ', ', $premium_entities )
                 );
            }
        }
        return $preview;
    }

    /**
     * Check if an entity is premium-only.
     *
     * @since 1.0.271
     * @param string $entity Entity key.
     * @return bool True if premium, false if free.
     */
    private function is_premium_entity( $entity ) {
        $premium_entities = array(
            'locations',
            'groups',
            'clubs',
            'instructors',
            'classes',
            'class_enrollments',
            'attendance',
            'events',
            'event_registrations',
            'training_videos',
            'grading_history',
            'member_groups',
            'class_instructors'
        );
        return in_array( $entity, $premium_entities, true );
    }

    /**
     * Count how many emails match existing WordPress users.
     *
     * Uses batched queries with fixed-size IN clauses to avoid SQL interpolation
     * issues flagged by WordPress Plugin Check static analysis.
     *
     * @since 1.0.271
     * @since 1.0.295 Refactored to use batched queries with literal SQL strings.
     * @param array $emails Array of email addresses.
     * @return int Number of matching users.
     */
    private function count_matching_users( $emails ) {
        global $wpdb;
        if ( empty( $emails ) ) {
            return 0;
        }
        $total_count = 0;
        $batch_size = 10;
        // Process in batches of 10 to use literal SQL.
        $batches = array_chunk( $emails, $batch_size );
        foreach ( $batches as $batch ) {
            $count = count( $batch );
            $batch_count = $this->count_matching_users_batch( $wpdb, $batch, $count );
            $total_count += $batch_count;
        }
        return $total_count;
    }

    /**
     * Count matching users for a batch of emails.
     *
     * Uses literal SQL strings with fixed placeholder counts to satisfy
     * WordPress Plugin Check static analysis requirements.
     *
     * @since 1.0.295
     * @param wpdb  $wpdb  WordPress database object.
     * @param array $batch Array of email addresses (max 10).
     * @param int   $count Number of emails in batch.
     * @return int Number of matching users in this batch.
     */
    private function count_matching_users_batch( $wpdb, $batch, $count ) {
        // Pad batch to exactly 10 elements with empty strings (won't match any user).
        $padded = array_pad( $batch, 10, '' );
        // Use literal SQL with exactly 10 placeholders - no variable interpolation.
        switch ( $count ) {
            case 1:
                return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE user_email IN (%s)', $wpdb->users, $padded[0] ) );
            case 2:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1]
                ) );
            case 3:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2]
                ) );
            case 4:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3]
                ) );
            case 5:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3],
                    $padded[4]
                ) );
            case 6:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3],
                    $padded[4],
                    $padded[5]
                ) );
            case 7:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3],
                    $padded[4],
                    $padded[5],
                    $padded[6]
                ) );
            case 8:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s,%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3],
                    $padded[4],
                    $padded[5],
                    $padded[6],
                    $padded[7]
                ) );
            case 9:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s,%s,%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3],
                    $padded[4],
                    $padded[5],
                    $padded[6],
                    $padded[7],
                    $padded[8]
                ) );
            case 10:
            default:
                return (int) $wpdb->get_var( $wpdb->prepare(
                    'SELECT COUNT(*) FROM %i WHERE user_email IN (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)',
                    $wpdb->users,
                    $padded[0],
                    $padded[1],
                    $padded[2],
                    $padded[3],
                    $padded[4],
                    $padded[5],
                    $padded[6],
                    $padded[7],
                    $padded[8],
                    $padded[9]
                ) );
        }
    }

    /**
     * Import data to the database.
     *
     * Main import orchestration method. Processes entities in dependency order.
     *
     * @since 1.0.271
     * @param array $data    Import data array.
     * @param array $options Import options (user_mapping, conflict_resolution).
     * @return array|WP_Error Import results or error.
     */
    public function import( $data, $options = array() ) {
        // Set default options.
        $options = wp_parse_args( $options, array(
            'user_mapping'        => 'match_email',
            'conflict_resolution' => 'skip',
        ) );
        // Initialize mapper.
        $this->mapper = new MACM_Data_Mapper($options['user_mapping']);
        $results = array(
            'imported'       => array(),
            'skipped'        => array(),
            'errors'         => array(),
            'entity_results' => array(),
        );
        // Define import order (dependency-based).
        $import_order = array(
            // Level 0: No dependencies.
            'settings',
            'belt_colors',
            'membership_types',
            'locations',
            'groups',
            'clubs',
            'instructors',
            // Level 1: Depends on Level 0.
            'members',
            'classes',
            'events',
            'training_videos',
            // Level 2: Junction tables and dependent.
            'member_groups',
            'class_instructors',
            'class_enrollments',
            'event_registrations',
            'trial_bookings',
            'grading_history',
            // Level 3: Highest dependency.
            'attendance',
        );
        global $wpdb;
        // Start transaction.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query( 'START TRANSACTION' );
        try {
            foreach ( $import_order as $entity ) {
                if ( !isset( $data[$entity] ) || empty( $data[$entity] ) ) {
                    continue;
                }
                // Check premium access.
                if ( $this->is_premium_entity( $entity ) ) {
                    if ( !function_exists( 'macm_fs' ) || !macm_fs()->can_use_premium_code() ) {
                        $results['skipped'][$entity] = __( 'Premium license required.', 'martial-arts-club-manager' );
                        continue;
                    }
                }
                // Import entity.
                $entity_result = $this->import_entity( $entity, $data[$entity], $options );
                if ( is_wp_error( $entity_result ) ) {
                    $results['errors'][$entity] = $entity_result->get_error_message();
                    // Rollback on critical error.
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    $wpdb->query( 'ROLLBACK' );
                    return new WP_Error('import_failed', sprintf( 
                        /* translators: 1: entity name, 2: error message */
                        __( 'Import failed on %1$s: %2$s', 'martial-arts-club-manager' ),
                        $entity,
                        $entity_result->get_error_message()
                     ));
                }
                $results['entity_results'][$entity] = $entity_result;
                $results['imported'][$entity] = $entity_result['imported'] ?? 0;
            }
            // Commit transaction.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->query( 'COMMIT' );
        } catch ( Exception $e ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->query( 'ROLLBACK' );
            return new WP_Error('import_exception', $e->getMessage());
        }
        // Clear caches.
        delete_transient( 'macm_exportable_entities' );
        // Clear plugin-specific cache groups if using persistent object cache.
        if ( function_exists( 'wp_cache_delete_group' ) ) {
            wp_cache_delete_group( 'macm' );
        }
        return $results;
    }

    /**
     * Import a specific entity type.
     *
     * Routes to the appropriate import method for each entity.
     *
     * @since 1.0.271
     * @param string $entity  Entity type key.
     * @param array  $records Records to import.
     * @param array  $options Import options.
     * @return array|WP_Error Import result or error.
     */
    private function import_entity( $entity, $records, $options ) {
        // Map entity to import method.
        $method = 'import_' . $entity;
        // Check for premium method.
        $premium_method = $method . '__premium_only';
        if ( method_exists( $this, $premium_method ) ) {
            return $this->{$premium_method}( $records, $options );
        }
        if ( method_exists( $this, $method ) ) {
            return $this->{$method}( $records, $options );
        }
        // Fallback: generic import (Phase 4 will implement specific methods).
        return array(
            'imported' => 0,
            'skipped'  => count( $records ),
            'message'  => __( 'Import method not yet implemented.', 'martial-arts-club-manager' ),
        );
    }

    /**
     * Get the data mapper instance.
     *
     * @since 1.0.271
     * @return MACM_Data_Mapper|null The mapper instance.
     */
    public function get_mapper() {
        return $this->mapper;
    }

    /**
     * Get collected errors.
     *
     * @since 1.0.271
     * @return array Array of error messages.
     */
    public function get_errors() {
        return $this->errors;
    }

    /**
     * Get parsed data.
     *
     * @since 1.0.271
     * @return array The parsed import data.
     */
    public function get_data() {
        return $this->data;
    }

    // =========================================================================
    // FREE ENTITY IMPORT METHODS
    // =========================================================================
    /**
     * Import plugin settings.
     *
     * @since 1.0.272
     * @param array $records Settings key-value pairs.
     * @param array $options Import options.
     * @return array Import result statistics.
     */
    private function import_settings( $records, $options ) {
        $imported = 0;
        $skipped = 0;
        $updated = 0;
        $conflict_mode = $options['conflict_resolution'] ?? 'skip';
        // List of settings that are safe to import.
        $importable_settings = array(
            'macm_button_gradient_start',
            'macm_button_gradient_end',
            'macm_admin_email',
            'macm_woocommerce_integration'
        );
        /**
         * Filter the list of settings keys that can be imported.
         *
         * @since 1.0.272
         * @param array $importable_settings Array of importable setting keys.
         */
        $importable_settings = apply_filters( 'macm_importable_settings_keys', $importable_settings );
        foreach ( $records as $key => $value ) {
            // Only import allowed settings.
            if ( !in_array( $key, $importable_settings, true ) ) {
                ++$skipped;
                continue;
            }
            $existing = get_option( $key );
            if ( false !== $existing ) {
                // Setting exists.
                if ( 'skip' === $conflict_mode ) {
                    ++$skipped;
                    continue;
                }
                // Update existing.
                update_option( $key, $value );
                ++$updated;
            } else {
                // New setting.
                add_option( $key, $value );
                ++$imported;
            }
        }
        return array(
            'imported' => $imported,
            'updated'  => $updated,
            'skipped'  => $skipped,
        );
    }

    /**
     * Import belt colors.
     *
     * Matches existing records by color_key (unique identifier).
     *
     * @since 1.0.272
     * @param array $records Belt color records.
     * @param array $options Import options.
     * @return array Import result statistics.
     */
    private function import_belt_colors( $records, $options ) {
        global $wpdb;
        $imported = 0;
        $skipped = 0;
        $updated = 0;
        $conflict_mode = $options['conflict_resolution'] ?? 'skip';
        foreach ( $records as $record ) {
            if ( empty( $record['color_key'] ) ) {
                ++$skipped;
                continue;
            }
            $color_key = sanitize_key( $record['color_key'] );
            // Check if exists by color_key.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $existing_id = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE color_key = %s', $wpdb->prefix . 'macm_belt_colors', $color_key ) );
            $old_id = ( isset( $record['id'] ) ? absint( $record['id'] ) : 0 );
            if ( $existing_id ) {
                // Record exists.
                if ( 'skip' === $conflict_mode ) {
                    // Map old ID to existing ID.
                    $this->mapper->map_entity_id( 'belt_colors', $old_id, (int) $existing_id );
                    ++$skipped;
                    continue;
                }
                // Build update data - will be filtered based on actual schema.
                $update_data = array(
                    'color_name' => sanitize_text_field( $record['color_name'] ?? '' ),
                    'sort_order' => absint( $record['sort_order'] ?? 0 ),
                    'is_active'  => absint( $record['is_active'] ?? 1 ),
                );
                $update_data = $this->filter_data_for_schema( $update_data, $wpdb->prefix . 'macm_belt_colors' );
                // Update existing record using inline table name.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    $wpdb->prefix . 'macm_belt_colors',
                    $update_data,
                    array(
                        'id' => $existing_id,
                    ),
                    $this->build_format_array( $update_data ),
                    array('%d')
                );
                $this->mapper->map_entity_id( 'belt_colors', $old_id, (int) $existing_id );
                ++$updated;
            } else {
                // Build insert data - will be filtered based on actual schema.
                $insert_data = array(
                    'color_key'  => $color_key,
                    'color_name' => sanitize_text_field( $record['color_name'] ?? '' ),
                    'sort_order' => absint( $record['sort_order'] ?? 0 ),
                    'is_active'  => absint( $record['is_active'] ?? 1 ),
                    'created_at' => current_time( 'mysql' ),
                );
                $insert_data = $this->filter_data_for_schema( $insert_data, $wpdb->prefix . 'macm_belt_colors' );
                // Insert new record using inline table name.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
                $wpdb->insert( $wpdb->prefix . 'macm_belt_colors', $insert_data, $this->build_format_array( $insert_data ) );
                $new_id = $wpdb->insert_id;
                $this->mapper->map_entity_id( 'belt_colors', $old_id, $new_id );
                ++$imported;
            }
        }
        return array(
            'imported' => $imported,
            'updated'  => $updated,
            'skipped'  => $skipped,
        );
    }

    /**
     * Import membership types.
     *
     * Matches existing records by type_name (unique identifier).
     *
     * @since 1.0.272
     * @param array $records Membership type records.
     * @param array $options Import options.
     * @return array Import result statistics.
     */
    private function import_membership_types( $records, $options ) {
        global $wpdb;
        $imported = 0;
        $skipped = 0;
        $updated = 0;
        $conflict_mode = $options['conflict_resolution'] ?? 'skip';
        foreach ( $records as $record ) {
            if ( empty( $record['type_name'] ) ) {
                ++$skipped;
                continue;
            }
            $type_name = sanitize_text_field( $record['type_name'] );
            // Check if exists by type_name.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $existing_id = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE type_name = %s', $wpdb->prefix . 'macm_membership_types', $type_name ) );
            $old_id = ( isset( $record['id'] ) ? absint( $record['id'] ) : 0 );
            if ( $existing_id ) {
                // Record exists.
                if ( 'skip' === $conflict_mode ) {
                    $this->mapper->map_entity_id( 'membership_types', $old_id, (int) $existing_id );
                    ++$skipped;
                    continue;
                }
                // Build update data - will be filtered based on actual schema.
                $update_data = array(
                    'description' => sanitize_textarea_field( $record['description'] ?? '' ),
                    'sort_order'  => absint( $record['sort_order'] ?? 0 ),
                    'is_active'   => absint( $record['is_active'] ?? 1 ),
                );
                $update_data = $this->filter_data_for_schema( $update_data, $wpdb->prefix . 'macm_membership_types' );
                // Update existing record using inline table name.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    $wpdb->prefix . 'macm_membership_types',
                    $update_data,
                    array(
                        'id' => $existing_id,
                    ),
                    $this->build_format_array( $update_data ),
                    array('%d')
                );
                $this->mapper->map_entity_id( 'membership_types', $old_id, (int) $existing_id );
                ++$updated;
            } else {
                // Build insert data - will be filtered based on actual schema.
                $insert_data = array(
                    'type_name'   => $type_name,
                    'description' => sanitize_textarea_field( $record['description'] ?? '' ),
                    'sort_order'  => absint( $record['sort_order'] ?? 0 ),
                    'is_active'   => absint( $record['is_active'] ?? 1 ),
                    'created_at'  => current_time( 'mysql' ),
                );
                $insert_data = $this->filter_data_for_schema( $insert_data, $wpdb->prefix . 'macm_membership_types' );
                // Insert new record using inline table name.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
                $wpdb->insert( $wpdb->prefix . 'macm_membership_types', $insert_data, $this->build_format_array( $insert_data ) );
                $new_id = $wpdb->insert_id;
                $this->mapper->map_entity_id( 'membership_types', $old_id, $new_id );
                ++$imported;
            }
        }
        return array(
            'imported' => $imported,
            'updated'  => $updated,
            'skipped'  => $skipped,
        );
    }

    /**
     * Import members.
     *
     * Uses mapper for user_id resolution and matches by user_email + full_name.
     *
     * @since 1.0.272
     * @param array $records Member records.
     * @param array $options Import options.
     * @return array Import result statistics.
     */
    private function import_members( $records, $options ) {
        global $wpdb;
        $imported = 0;
        $skipped = 0;
        $updated = 0;
        $conflict_mode = $options['conflict_resolution'] ?? 'skip';
        // Preload user emails for batch processing.
        $emails = array_filter( array_column( $records, 'user_email' ) );
        if ( !empty( $emails ) ) {
            $this->mapper->preload_user_emails( $emails );
        }
        foreach ( $records as $record ) {
            if ( empty( $record['full_name'] ) ) {
                ++$skipped;
                continue;
            }
            $old_id = ( isset( $record['id'] ) ? absint( $record['id'] ) : 0 );
            $old_user_id = ( isset( $record['user_id'] ) ? absint( $record['user_id'] ) : 0 );
            $user_email = sanitize_email( $record['user_email'] ?? '' );
            // Map user ID.
            $new_user_id = $this->mapper->map_user_id( $old_user_id, $user_email );
            // If mapping returns null in skip mode, skip this record.
            if ( null === $new_user_id ) {
                ++$skipped;
                continue;
            }
            // Resolve membership_type_id from name if provided.
            $membership_type_id = null;
            if ( !empty( $record['membership_type_name'] ) ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $membership_type_id = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE type_name = %s', $wpdb->prefix . 'macm_membership_types', sanitize_text_field( $record['membership_type_name'] ) ) );
            } elseif ( isset( $record['membership_type_id'] ) ) {
                // Try to use mapped membership_type_id.
                $mapped_type_id = $this->mapper->get_mapped_id( 'membership_types', absint( $record['membership_type_id'] ) );
                $membership_type_id = ( $mapped_type_id ? $mapped_type_id : absint( $record['membership_type_id'] ) );
            }
            // Check if member exists by user_id + full_name.
            $full_name = sanitize_text_field( $record['full_name'] );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $existing_id = $wpdb->get_var( $wpdb->prepare(
                'SELECT id FROM %i WHERE user_id = %d AND full_name = %s',
                $wpdb->prefix . 'macm_members',
                $new_user_id,
                $full_name
            ) );
            // Prepare member data - will be filtered based on actual schema.
            $member_data = array(
                'user_id'            => $new_user_id,
                'full_name'          => $full_name,
                'date_of_birth'      => $this->sanitize_date( $record['date_of_birth'] ?? '' ),
                'membership_type_id' => $membership_type_id,
                'belt_color'         => sanitize_text_field( $record['belt_color'] ?? '' ),
                'weight'             => floatval( $record['weight'] ?? 0 ),
                'height'             => floatval( $record['height'] ?? 0 ),
                'license_number'     => sanitize_text_field( $record['license_number'] ?? '' ),
                'license_expiration' => $this->sanitize_date( $record['license_expiration'] ?? '' ),
                'status'             => sanitize_key( $record['status'] ?? 'active' ),
            );
            // Filter data to only include columns that exist in the target schema.
            $member_data = $this->filter_data_for_schema( $member_data, $wpdb->prefix . 'macm_members' );
            if ( $existing_id ) {
                // Record exists.
                if ( 'skip' === $conflict_mode ) {
                    $this->mapper->map_entity_id( 'members', $old_id, (int) $existing_id );
                    ++$skipped;
                    continue;
                }
                // Update existing record using inline table name.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    $wpdb->prefix . 'macm_members',
                    $member_data,
                    array(
                        'id' => $existing_id,
                    ),
                    $this->build_format_array( $member_data ),
                    array('%d')
                );
                $this->mapper->map_entity_id( 'members', $old_id, (int) $existing_id );
                ++$updated;
            } else {
                // Insert new record using inline table name.
                $member_data['created_at'] = current_time( 'mysql' );
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
                $wpdb->insert( $wpdb->prefix . 'macm_members', $member_data, $this->build_format_array( $member_data ) );
                $new_id = $wpdb->insert_id;
                $this->mapper->map_entity_id( 'members', $old_id, $new_id );
                ++$imported;
            }
        }
        return array(
            'imported' => $imported,
            'updated'  => $updated,
            'skipped'  => $skipped,
        );
    }

    /**
     * Import trial bookings.
     *
     * @since 1.0.272
     * @param array $records Trial booking records.
     * @param array $options Import options.
     * @return array Import result statistics.
     */
    private function import_trial_bookings( $records, $options ) {
        global $wpdb;
        $imported = 0;
        $skipped = 0;
        $updated = 0;
        $conflict_mode = $options['conflict_resolution'] ?? 'skip';
        foreach ( $records as $record ) {
            if ( empty( $record['email'] ) || empty( $record['full_name'] ) ) {
                ++$skipped;
                continue;
            }
            $old_id = ( isset( $record['id'] ) ? absint( $record['id'] ) : 0 );
            $email = sanitize_email( $record['email'] );
            // Resolve class_id from class_name if provided.
            $class_id = null;
            if ( !empty( $record['class_name'] ) ) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $class_id = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE class_name = %s', $wpdb->prefix . 'macm_classes', sanitize_text_field( $record['class_name'] ) ) );
            } elseif ( isset( $record['class_id'] ) && $record['class_id'] ) {
                $mapped_class_id = $this->mapper->get_mapped_id( 'classes', absint( $record['class_id'] ) );
                $class_id = ( $mapped_class_id ? $mapped_class_id : absint( $record['class_id'] ) );
            }
            // Check if exists by email + booking date (if column exists) or just email + full_name.
            $sanitized_booking_date = $this->sanitize_date( $record['booking_date'] ?? '' );
            $booking_date = ( $sanitized_booking_date ? $sanitized_booking_date : current_time( 'mysql' ) );
            // Get valid columns to check if booking_date exists in target schema.
            $valid_columns = $this->get_table_columns( $wpdb->prefix . 'macm_trial_bookings' );
            $has_booking_date = in_array( 'booking_date', $valid_columns, true );
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            if ( $has_booking_date ) {
                $existing_id = $wpdb->get_var( $wpdb->prepare(
                    'SELECT id FROM %i WHERE email = %s AND DATE(booking_date) = DATE(%s)',
                    $wpdb->prefix . 'macm_trial_bookings',
                    $email,
                    $booking_date
                ) );
            } else {
                // Fallback: check by email + full_name for older schemas without booking_date.
                $existing_id = $wpdb->get_var( $wpdb->prepare(
                    'SELECT id FROM %i WHERE email = %s AND full_name = %s',
                    $wpdb->prefix . 'macm_trial_bookings',
                    $email,
                    sanitize_text_field( $record['full_name'] )
                ) );
            }
            // Build full data array - will be filtered based on actual schema.
            $booking_data = array(
                'full_name'    => sanitize_text_field( $record['full_name'] ),
                'email'        => $email,
                'phone'        => sanitize_text_field( $record['phone'] ?? '' ),
                'class_id'     => $class_id,
                'message'      => sanitize_textarea_field( $record['message'] ?? '' ),
                'booking_date' => $booking_date,
                'status'       => sanitize_key( $record['status'] ?? 'pending' ),
            );
            // Filter data to only include columns that exist in the target schema.
            $booking_data = $this->filter_data_for_schema( $booking_data, $wpdb->prefix . 'macm_trial_bookings' );
            if ( $existing_id ) {
                // Record exists.
                if ( 'skip' === $conflict_mode ) {
                    $this->mapper->map_entity_id( 'trial_bookings', $old_id, (int) $existing_id );
                    ++$skipped;
                    continue;
                }
                // Update existing record using inline table name.
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->update(
                    $wpdb->prefix . 'macm_trial_bookings',
                    $booking_data,
                    array(
                        'id' => $existing_id,
                    ),
                    $this->build_format_array( $booking_data ),
                    array('%d')
                );
                $this->mapper->map_entity_id( 'trial_bookings', $old_id, (int) $existing_id );
                ++$updated;
            } else {
                // Insert new record using inline table name.
                $booking_data['created_at'] = current_time( 'mysql' );
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
                $wpdb->insert( $wpdb->prefix . 'macm_trial_bookings', $booking_data, $this->build_format_array( $booking_data ) );
                $new_id = $wpdb->insert_id;
                $this->mapper->map_entity_id( 'trial_bookings', $old_id, $new_id );
                ++$imported;
            }
        }
        return array(
            'imported' => $imported,
            'updated'  => $updated,
            'skipped'  => $skipped,
        );
    }

    /**
     * Cache of table columns to avoid repeated queries.
     *
     * @since 1.0.283
     * @var array
     */
    private $table_columns_cache = array();

    /**
     * Get the column names for a database table.
     *
     * Queries the database schema to get actual column names,
     * allowing import to handle schema differences between sites.
     *
     * @since 1.0.283
     * @param string $table_name Full table name including prefix.
     * @return array Array of column names.
     */
    private function get_table_columns( $table_name ) {
        // Return from cache if available.
        if ( isset( $this->table_columns_cache[$table_name] ) ) {
            return $this->table_columns_cache[$table_name];
        }
        global $wpdb;
        // Query the information schema to get column names.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $columns = $wpdb->get_col( $wpdb->prepare( 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s', DB_NAME, $table_name ) );
        $this->table_columns_cache[$table_name] = ( $columns ? $columns : array() );
        return $this->table_columns_cache[$table_name];
    }

    /**
     * Filter data array to only include columns that exist in the table.
     *
     * @since 1.0.283
     * @param array  $data       Associative array of column => value pairs.
     * @param string $table_name Full table name including prefix.
     * @return array Filtered data with only valid columns.
     */
    private function filter_data_for_schema( $data, $table_name ) {
        $valid_columns = $this->get_table_columns( $table_name );
        if ( empty( $valid_columns ) ) {
            // If we can't get columns, return original data and let MySQL handle errors.
            return $data;
        }
        return array_intersect_key( $data, array_flip( $valid_columns ) );
    }

    /**
     * Build format array for wpdb insert/update based on data types.
     *
     * @since 1.0.283
     * @param array $data Associative array of column => value pairs.
     * @return array Array of format strings (%s, %d, %f).
     */
    private function build_format_array( $data ) {
        $formats = array();
        foreach ( $data as $key => $value ) {
            if ( is_int( $value ) || is_string( $value ) && preg_match( '/^-?\\d+$/', $value ) && strlen( $value ) < 10 ) {
                $formats[] = '%d';
            } elseif ( is_float( $value ) || is_string( $value ) && is_numeric( $value ) && strpos( $value, '.' ) !== false ) {
                $formats[] = '%f';
            } else {
                $formats[] = '%s';
            }
        }
        return $formats;
    }

    /**
     * Sanitize a date string.
     *
     * @since 1.0.272
     * @param string $date Date string to sanitize.
     * @return string|null Sanitized date in Y-m-d format or null if invalid.
     */
    private function sanitize_date( $date ) {
        if ( empty( $date ) ) {
            return null;
        }
        $timestamp = strtotime( $date );
        if ( false === $timestamp ) {
            return null;
        }
        return gmdate( 'Y-m-d', $timestamp );
    }

    /**
     * Sanitize a datetime string.
     *
     * @since 1.0.272
     * @param string $datetime Datetime string to sanitize.
     * @return string|null Sanitized datetime in Y-m-d H:i:s format or null if invalid.
     */
    private function sanitize_datetime( $datetime ) {
        if ( empty( $datetime ) ) {
            return null;
        }
        $timestamp = strtotime( $datetime );
        if ( false === $timestamp ) {
            return null;
        }
        return gmdate( 'Y-m-d H:i:s', $timestamp );
    }

}
