<?php
/**
 * File Path: includes/import/csv/class-mbs-child-importer-csv-helpers.php
 *
 * CSV Importer – Shared Helpers (Child Class)
 *
 * Contains all private/shared helper methods and constants that were
 * previously inside the monolithic class-mbs-importer-csv.php.
 *
 * This class is loaded by the parent orchestrator and the other child classes.
 * It keeps the individual child files small and prevents truncation issues.
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

/**
 * Shared helper methods and constants for the CSV importer.
 *
 * All methods are static and private where appropriate.
 * Public getters are provided for the parent orchestrator.
 */
class MBS_Child_Importer_CSV_Helpers { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/** Maximum upload file size: 10 MB. */
	const MAX_FILE_SIZE = 10485760;

	/**
	 * Canonical column names this importer accepts.
	 * Used by Tab 5 column-mapping UI and by import_candidates().
	 *
	 * @var string[]
	 */
	const ACCEPTED_COLUMNS = array(
		'title', 'teaser', 'tagline', 'publisher', 'pub_date', 'coming_soon',
		'isbn13', 'asin', 'format', 'format_label', 'price', 'look_inside_text',
		'author', 'series', 'series_number', 'genre', 'tags',
		'page_length', 'cover_image_url', 'cover_image_file',
		// F-020 Phase 2 -- BISAC + Audience + Thema + BIC
		'bisac_codes', 'thema_codes', 'bic_codes', 'bowker_codes', 'audience', 'age_range_from', 'age_range_to',
		'grade_range_from', 'grade_range_to',
		// F-020 Phase 3 -- Book Details + Publication Time
		'synopsis', 'toc', 'lccn', 'illustration_count', 'illustration_types',
		'edition_number', 'edition_type', 'edition_statement',
		'publication_time', 'featured',
		// F-020 Phase 4 -- Publisher + Imprint taxonomies
		'imprint',
		// F-020 Phase 5 -- Contributor taxonomy
		'contributor',
		// F-020 Phase 8 -- Related Products
		'related_products',
		// PREWORK-1 / PREWORK-2 -- Tagline and Bowker added above in canonical positions.
	);

	/**
	 * Get the canonical list of accepted columns (for UI mapping).
	 *
	 * @return string[]
	 */
	public static function get_accepted_columns(): array {
		return self::ACCEPTED_COLUMNS;
	}

	/**
	 * Get the maximum allowed file size in bytes.
	 *
	 * @return int
	 */
	public static function get_max_file_size(): int {
		return self::MAX_FILE_SIZE;
	}

	/**
	 * Auto-detect the column delimiter used in a CSV file.
	 *
	 * Samples the first 5 lines of the file and counts occurrences of the
	 * three most common delimiters: comma (,), semicolon (;), and tab (\t).
	 * The delimiter with the highest count wins. Ties break in favor of comma.
	 *
	 * This is called transparently at both the parse_headers and import steps
	 * so the admin never needs to specify a delimiter manually.
	 *
	 * @param string $file_path Absolute path to the CSV file.
	 * @return string           One of ',', ';', or "\t".
	 */
	public static function detect_delimiter( string $file_path ): string {
		$counts = array( ',' => 0, ';' => 0, "\t" => 0 );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$fh = fopen( $file_path, 'r' );
		if ( ! $fh ) {
			return ',';
		}

		$lines = 0;
		while ( $lines < 5 && ( $line = fgets( $fh ) ) !== false ) {
			$counts[','] += substr_count( $line, ',' );
			$counts[';'] += substr_count( $line, ';' );
			$counts["\t"] += substr_count( $line, "\t" );
			$lines++;
		}

		fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- WP_Filesystem migration deferred to PC-FILESYS sprint; PHP native file ops are safe here (temp files in get_temp_dir(), never inside wp-content)

		// Pick the delimiter with the highest occurrence count.
		// arsort() preserves key association; first key after sort = winner.
		arsort( $counts );
		reset( $counts );
		$winner = key( $counts );

		// Return comma as default if nothing was detected (all counts are 0).
		return $winner ?? ',';
	}

	/**
	 * Validate the uploaded file (size, existence, readability).
	 *
	 * @param string $file_path Absolute path to the uploaded temporary file.
	 * @return true|string True on success, error message string on failure.
	 */
	public static function validate_file( string $file_path ): true|string {
		if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
			return 'File not found or not readable.';
		}
		if ( filesize( $file_path ) > self::MAX_FILE_SIZE ) {
			return 'File exceeds the maximum allowed size of 10 MB.';
		}
		return true;
	}

	/**
	 * Build the field map from CSV headers and optional user column map.
	 *
	 * @param array $headers    Lowercased CSV header row.
	 * @param array $column_map User-provided column → MBS field map.
	 * @return array            Index → field name map.
	 */
	public static function build_field_map( array $headers, array $column_map ): array {
		$field_map      = array();
		$normalized_map = array();

		foreach ( $column_map as $csv_col => $mbs_field ) {
			$normalized_map[ strtolower( trim( $csv_col ) ) ] = sanitize_key( $mbs_field );
		}

		foreach ( $headers as $index => $header ) {
			if ( str_starts_with( $header, '_' ) ) {
				// Private passthrough columns (e.g. _buy_links, _endorsements).
				// These always passthrough regardless of any column_map entry.
				// Checked first so a stale column_map submission (e.g. cached browser
				// JS sending _buy_links => '') cannot suppress the passthrough.
				$field_map[ $index ] = $header;
			} elseif ( isset( $normalized_map[ $header ] ) && '' !== $normalized_map[ $header ] ) {
				$field_map[ $index ] = $normalized_map[ $header ];
			} elseif ( in_array( $header, self::ACCEPTED_COLUMNS, true ) ) {
				$field_map[ $index ] = $header;
			}
		}

		return $field_map;
	}

	/**
	 * Map a single CSV row into a candidate array.
	 *
	 * @param array $row       One data row from CSV.
	 * @param array $field_map Index → field name map.
	 * @param int   $line      Current line number (for error reporting).
	 * @return ?array          Candidate array or null if no title.
	 */
	public static function map_row( array $row, array $field_map, int $line ): ?array {
		$candidate = array( '_line' => $line );

		foreach ( $field_map as $index => $field ) {
			$raw_value           = isset( $row[ $index ] ) ? trim( $row[ $index ] ) : '';
			$candidate[ $field ] = $raw_value;
		}

		if ( empty( $candidate['title'] ) ) {
			return null;
		}

		if ( ! empty( $candidate['isbn13'] ) ) {
			$normalized            = MBS_ISBN_Helper::normalize_isbn( $candidate['isbn13'] );
			$candidate['isbn13']   = $normalized ?: '';
		}

		return $candidate;
	}

	/**
	 * Process a list of candidate arrays (duplicate detection + insertion).
	 *
	 * @param array $candidates       Candidate arrays.
	 * @param array $force_insert_keys Keys approved for force-insert.
	 * @param array $result           Result accumulator.
	 * @return array                  Final import result.
	 */
	public static function process_candidates(
		array $candidates,
		array $force_insert_keys,
		array $result
	): array {
		$detection            = MBS_Duplicate_Detector::check( $candidates );
		$clean                = $detection['clean'];
		$detected_dupes       = $detection['duplicates'];
		$result['duplicates'] = $detected_dupes;

		foreach ( $detected_dupes as $dupe ) {
			$key = self::force_key( $dupe['data'] ?? $dupe );
			if ( in_array( $key, $force_insert_keys, true ) ) {
				$clean[] = $dupe['data'] ?? $dupe;
			} else {
				$result['skipped']++;
			}
		}

		foreach ( $clean as $candidate ) {
			$insert_result = MBS_Child_Importer_CSV_Insert::insert_book( $candidate );
			if ( true === $insert_result ) {
				$result['inserted']++;
			} else {
				$result['errors'][] = $insert_result;
			}
		}

		$result['success'] = true;
		return $result;
	}

	/**
	 * Generate a unique force-key for duplicate detection.
	 *
	 * @param array $candidate Candidate array.
	 * @return string           Unique key (isbn13 or title).
	 */
	public static function force_key( array $candidate ): string {
		if ( ! empty( $candidate['isbn13'] ) ) {
			return 'isbn13:' . $candidate['isbn13'];
		}
		return 'title:' . sanitize_title( $candidate['title'] ?? '' );
	}
}
