<?php
/**
 * GGM Commerce Taxonomy CSV Importer
 *
 * Handles CSV import functionality for categories and tags
 *
 * @package GGM_Commerce
 * @since 1.0.0
 */

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter

/**
 * Class GGM_Commerce_Taxonomy_CSV_Importer
 *
 * Imports categories and tags from CSV format
 */
class GGM_Commerce_Taxonomy_CSV_Importer {

	/**
	 * Import session data transient key
	 *
	 * @var string
	 */
	private $session_key;

	/**
	 * Import type: 'categories' or 'tags'
	 *
	 * @var string
	 */
	private $import_type;

	/**
	 * Current import progress
	 *
	 * @var array
	 */
	private $progress = array(
		'total'      => 0,
		'processed'  => 0,
		'created'    => 0,
		'updated'    => 0,
		'skipped'    => 0,
		'errors'     => array(),
		'warnings'   => array(),
	);

	/**
	 * Lookup maps for relationship resolution
	 *
	 * @var array
	 */
	private $lookup_maps = array(
		'categories' => array(), // slug => id.
	);

	/**
	 * Constructor
	 *
	 * @param string $import_type Import type ('categories' or 'tags').
	 */
	public function __construct( $import_type = 'categories' ) {
		$this->import_type = $import_type;
		$this->session_key = 'ggmc_' . $import_type . '_import_' . get_current_user_id();
	}

	/**
	 * Parse CSV file and return preview data
	 *
	 * @param string $file_path Path to CSV file.
	 * @return array|WP_Error Parsed data or error.
	 */
	public function parse_csv( $file_path ) {
		if ( ! file_exists( $file_path ) ) {
			return new WP_Error( 'file_not_found', __( 'CSV file not found', 'ggm-commerce' ) );
		}

		$handle = fopen( $file_path, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		if ( ! $handle ) {
			return new WP_Error( 'file_read_error', __( 'Could not read CSV file', 'ggm-commerce' ) );
		}

		// Skip BOM if present.
		$bom = fread( $handle, 3 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
		if ( "\xEF\xBB\xBF" !== $bom ) {
			rewind( $handle );
		}

		// Read headers.
		$headers = fgetcsv( $handle );
		if ( empty( $headers ) ) {
			fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			return new WP_Error( 'empty_file', __( 'CSV file is empty or has no headers', 'ggm-commerce' ) );
		}

		// Clean headers.
		$headers = array_map( 'trim', $headers );
		$headers = array_map(
			function ( $h ) {
				return preg_replace( '/^\xEF\xBB\xBF/', '', $h );
			},
			$headers
		);

		// Count rows and get sample.
		$row_count   = 0;
		$sample_rows = array();

		while ( ( $row = fgetcsv( $handle ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
			$row_count++;
			if ( count( $sample_rows ) < 5 ) {
				$row_data = array();
				foreach ( $headers as $i => $header ) {
					$row_data[ $header ] = isset( $row[ $i ] ) ? $row[ $i ] : '';
				}
				$sample_rows[] = $row_data;
			}
		}

		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose

		return array(
			'headers'     => $headers,
			'row_count'   => $row_count,
			'sample_rows' => $sample_rows,
			'file_path'   => $file_path,
		);
	}

	/**
	 * Initialize import session
	 *
	 * @param string $file_path Path to CSV file.
	 * @param array  $options Import options.
	 * @return array|WP_Error Initial state or error.
	 */
	public function init_import( $file_path, $options = array() ) {
		$defaults = array(
			'duplicate_handling' => 'update', // skip, update, create.
			'download_images'    => true,
			'batch_size'         => 50,
		);

		$options = wp_parse_args( $options, $defaults );

		// Parse CSV to get headers and count.
		$parse_result = $this->parse_csv( $file_path );
		if ( is_wp_error( $parse_result ) ) {
			return $parse_result;
		}

		// Pre-load lookup maps for relationship resolution.
		$this->preload_lookup_maps();

		// For categories, read all rows and sort by hierarchy.
		$all_rows = array();
		if ( 'categories' === $this->import_type ) {
			$all_rows = $this->read_all_rows( $file_path, $parse_result['headers'] );
			$all_rows = $this->sort_categories_by_hierarchy( $all_rows );
		}

		// Initialize progress.
		$this->progress = array(
			'total'           => $parse_result['row_count'],
			'processed'       => 0,
			'created'         => 0,
			'updated'         => 0,
			'skipped'         => 0,
			'errors'          => array(),
			'warnings'        => array(),
			'current_item'    => '',
			'status'          => 'running',
			'file_path'       => $file_path,
			'headers'         => $parse_result['headers'],
			'options'         => $options,
			'current_row'     => 0,
			'lookup_maps'     => $this->lookup_maps,
			'sorted_rows'     => $all_rows, // Pre-sorted for categories.
		);

		$this->save_progress();

		return array(
			'total'      => $parse_result['row_count'],
			'batch_size' => $options['batch_size'],
			'status'     => 'initialized',
		);
	}

	/**
	 * Process the next batch of imports
	 *
	 * @return array|WP_Error Batch result with progress info.
	 */
	public function process_batch() {
		// Load current progress.
		$this->progress = $this->get_progress();

		if ( empty( $this->progress ) || 'running' !== $this->progress['status'] ) {
			return new WP_Error( 'no_session', __( 'No active import session', 'ggm-commerce' ) );
		}

		$file_path  = $this->progress['file_path'];
		$headers    = $this->progress['headers'];
		$options    = $this->progress['options'];
		$start_row  = $this->progress['current_row'];
		$batch_size = isset( $options['batch_size'] ) ? $options['batch_size'] : 50;

		// Restore lookup maps from session.
		$this->lookup_maps = $this->progress['lookup_maps'];

		// For categories, use pre-sorted rows.
		if ( 'categories' === $this->import_type && ! empty( $this->progress['sorted_rows'] ) ) {
			$rows = array_slice( $this->progress['sorted_rows'], $start_row, $batch_size );
			$processed_in_batch = 0;

			foreach ( $rows as $row_data ) {
				$this->progress['current_item'] = $row_data['name'] ?? '';

				$result = $this->import_single_category( $row_data, $options );

				$this->handle_import_result( $result, $row_data['name'] ?? 'Unknown' );

				$this->progress['processed']++;
				$this->progress['current_row']++;
				$processed_in_batch++;
			}
		} else {
			// Tags - read from file sequentially.
			$handle = fopen( $file_path, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
			if ( ! $handle ) {
				$this->progress['status'] = 'error';
				$this->save_progress();
				return new WP_Error( 'file_read_error', __( 'Could not read CSV file', 'ggm-commerce' ) );
			}

			// Skip BOM.
			$bom = fread( $handle, 3 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
			if ( "\xEF\xBB\xBF" !== $bom ) {
				rewind( $handle );
			}

			// Skip header row.
			fgetcsv( $handle );

			// Skip to current position.
			for ( $i = 0; $i < $start_row; $i++ ) {
				fgetcsv( $handle );
			}

			// Process batch.
			$processed_in_batch = 0;
			while ( $processed_in_batch < $batch_size && ( $row = fgetcsv( $handle ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
				$row_data = array();
				foreach ( $headers as $idx => $header ) {
					$row_data[ $header ] = isset( $row[ $idx ] ) ? $row[ $idx ] : '';
				}

				$this->progress['current_item'] = $row_data['name'] ?? '';

				$result = $this->import_single_tag( $row_data, $options );

				$this->handle_import_result( $result, $row_data['name'] ?? 'Unknown' );

				$this->progress['processed']++;
				$this->progress['current_row']++;
				$processed_in_batch++;
			}

			fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
		}

		// Check if complete.
		if ( $this->progress['processed'] >= $this->progress['total'] ) {
			$this->progress['status']       = 'complete';
			$this->progress['current_item'] = '';
		}

		// Update lookup maps in progress for next batch.
		$this->progress['lookup_maps'] = $this->lookup_maps;
		$this->save_progress();

		return array(
			'total'           => $this->progress['total'],
			'processed'       => $this->progress['processed'],
			'created'         => $this->progress['created'],
			'updated'         => $this->progress['updated'],
			'skipped'         => $this->progress['skipped'],
			'errors'          => $this->progress['errors'],
			'warnings'        => $this->progress['warnings'],
			'current_item'    => $this->progress['current_item'],
			'status'          => $this->progress['status'],
			'batch_processed' => $processed_in_batch,
		);
	}

	/**
	 * Handle import result
	 *
	 * @param string|WP_Error $result Import result.
	 * @param string          $item_name Item name for error reporting.
	 */
	private function handle_import_result( $result, $item_name ) {
		if ( is_wp_error( $result ) ) {
			$this->progress['errors'][] = array(
				'row'     => $this->progress['processed'] + 1,
				'message' => $result->get_error_message(),
				'data'    => $item_name,
			);
		} elseif ( 'created' === $result ) {
			$this->progress['created']++;
		} elseif ( 'updated' === $result ) {
			$this->progress['updated']++;
		} else {
			$this->progress['skipped']++;
		}
	}

	/**
	 * Pre-load lookup maps for efficient relationship resolution
	 */
	private function preload_lookup_maps() {
		global $wpdb;

		// Load existing categories.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$categories = $wpdb->get_results(
			"SELECT id, slug FROM {$wpdb->prefix}ggmc_categories",
			ARRAY_A
		);
		foreach ( $categories as $cat ) {
			$this->lookup_maps['categories'][ $cat['slug'] ] = $cat['id'];
		}
	}

	/**
	 * Read all rows from CSV file
	 *
	 * @param string $file_path Path to CSV file.
	 * @param array  $headers CSV headers.
	 * @return array All rows.
	 */
	private function read_all_rows( $file_path, $headers ) {
		$rows   = array();
		$handle = fopen( $file_path, 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen

		if ( ! $handle ) {
			return $rows;
		}

		// Skip BOM.
		$bom = fread( $handle, 3 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
		if ( "\xEF\xBB\xBF" !== $bom ) {
			rewind( $handle );
		}

		// Skip header row.
		fgetcsv( $handle );

		while ( ( $row = fgetcsv( $handle ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
			$row_data = array();
			foreach ( $headers as $idx => $header ) {
				$row_data[ $header ] = isset( $row[ $idx ] ) ? $row[ $idx ] : '';
			}
			$rows[] = $row_data;
		}

		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose

		return $rows;
	}

	/**
	 * Sort categories so parents are imported before children
	 *
	 * @param array $rows Category rows.
	 * @return array Sorted rows.
	 */
	private function sort_categories_by_hierarchy( $rows ) {
		$sorted          = array();
		$pending         = $rows;
		$processed_slugs = array( '' ); // Empty string means "root".

		$max_iterations = count( $rows ) * 2;
		$iteration      = 0;

		while ( ! empty( $pending ) && $iteration < $max_iterations ) {
			$iteration++;

			foreach ( $pending as $key => $row ) {
				$parent_slug = $row['parent_slug'] ?? '';

				// If parent already processed (or is root), we can process this one.
				if ( in_array( $parent_slug, $processed_slugs, true ) ) {
					$sorted[]          = $row;
					$processed_slugs[] = $row['slug'] ?? '';
					unset( $pending[ $key ] );
				}
			}
		}

		// Any remaining items have invalid parent references - add with warning.
		foreach ( $pending as $row ) {
			$sorted[] = $row;
			$this->progress['warnings'][] = array(
				'message' => sprintf(
					/* translators: 1: category name, 2: parent slug */
					__( 'Category "%1$s" has invalid parent "%2$s" - imported as root', 'ggm-commerce' ),
					$row['name'] ?? 'Unknown',
					$row['parent_slug'] ?? ''
				),
			);
		}

		return $sorted;
	}

	/**
	 * Import single category
	 *
	 * @param array $row_data Row data from CSV.
	 * @param array $options Import options.
	 * @return string|WP_Error Result: 'created', 'updated', 'skipped', or WP_Error.
	 */
	private function import_single_category( $row_data, $options ) {
		global $wpdb;

		// Validate required fields.
		if ( empty( $row_data['name'] ) ) {
			return new WP_Error( 'missing_name', __( 'Category name is required', 'ggm-commerce' ) );
		}

		$name        = sanitize_text_field( $row_data['name'] );
		$slug        = ! empty( $row_data['slug'] ) ? sanitize_title( $row_data['slug'] ) : sanitize_title( $name );
		$parent_slug = $row_data['parent_slug'] ?? '';

		// Resolve parent ID from lookup map.
		$parent_id = 0;
		if ( ! empty( $parent_slug ) ) {
			if ( isset( $this->lookup_maps['categories'][ $parent_slug ] ) ) {
				$parent_id = $this->lookup_maps['categories'][ $parent_slug ];
			} else {
				// Parent not found - import as root with warning.
				$this->progress['warnings'][] = array(
					'message' => sprintf(
						/* translators: 1: parent slug, 2: category name */
						__( 'Parent category "%1$s" not found for "%2$s"', 'ggm-commerce' ),
						$parent_slug,
						$name
					),
				);
			}
		}

		// Check for existing by slug.
		$existing_id = $this->lookup_maps['categories'][ $slug ] ?? null;

		if ( $existing_id ) {
			// Handle duplicate.
			switch ( $options['duplicate_handling'] ) {
				case 'skip':
					return 'skipped';

				case 'update':
					return $this->update_category( $existing_id, $row_data, $parent_id, $options );

				case 'create':
					$slug = $this->ensure_unique_slug( $slug, 'categories' );
					break;
			}
		}

		// Create new category.
		$insert_data = array(
			'name'          => $name,
			'slug'          => $slug,
			'description'   => sanitize_textarea_field( $row_data['description'] ?? '' ),
			'parent_id'     => $parent_id,
			'display_order' => intval( $row_data['display_order'] ?? 0 ),
			'status'        => sanitize_text_field( $row_data['status'] ?? 'active' ),
			'created_at'    => current_time( 'mysql' ),
			'updated_at'    => current_time( 'mysql' ),
		);

		// Handle image.
		if ( ! empty( $row_data['image_url'] ) && $options['download_images'] ) {
			$image_id = $this->download_image( $row_data['image_url'] );
			if ( $image_id ) {
				$insert_data['image_id'] = $image_id;
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert( "{$wpdb->prefix}ggmc_categories", $insert_data );

		if ( $result ) {
			$new_id = $wpdb->insert_id;
			// Update lookup map for subsequent items.
			$this->lookup_maps['categories'][ $slug ] = $new_id;
			return 'created';
		}

		return new WP_Error( 'insert_failed', __( 'Failed to create category', 'ggm-commerce' ) );
	}

	/**
	 * Update existing category
	 *
	 * @param int   $category_id Existing category ID.
	 * @param array $row_data Row data from CSV.
	 * @param int   $parent_id Resolved parent ID.
	 * @param array $options Import options.
	 * @return string|WP_Error Result.
	 */
	private function update_category( $category_id, $row_data, $parent_id, $options ) {
		global $wpdb;

		$update_data = array(
			'name'          => sanitize_text_field( $row_data['name'] ),
			'description'   => sanitize_textarea_field( $row_data['description'] ?? '' ),
			'parent_id'     => $parent_id,
			'display_order' => intval( $row_data['display_order'] ?? 0 ),
			'status'        => sanitize_text_field( $row_data['status'] ?? 'active' ),
			'updated_at'    => current_time( 'mysql' ),
		);

		// Handle image.
		if ( ! empty( $row_data['image_url'] ) && $options['download_images'] ) {
			$image_id = $this->download_image( $row_data['image_url'] );
			if ( $image_id ) {
				$update_data['image_id'] = $image_id;
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->update(
			"{$wpdb->prefix}ggmc_categories",
			$update_data,
			array( 'id' => $category_id )
		);

		if ( false !== $result ) {
			return 'updated';
		}

		return new WP_Error( 'update_failed', __( 'Failed to update category', 'ggm-commerce' ) );
	}

	/**
	 * Import single tag
	 *
	 * @param array $row_data Row data from CSV.
	 * @param array $options Import options.
	 * @return string|WP_Error Result: 'created', 'updated', 'skipped', or WP_Error.
	 */
	private function import_single_tag( $row_data, $options ) {
		global $wpdb;

		// Validate required fields.
		if ( empty( $row_data['name'] ) ) {
			return new WP_Error( 'missing_name', __( 'Tag name is required', 'ggm-commerce' ) );
		}

		$name = sanitize_text_field( $row_data['name'] );
		$slug = ! empty( $row_data['slug'] ) ? sanitize_title( $row_data['slug'] ) : sanitize_title( $name );

		// Check for existing by slug.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$existing = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id FROM {$wpdb->prefix}ggmc_tags WHERE slug = %s",
				$slug
			)
		);

		if ( $existing ) {
			// Handle duplicate.
			switch ( $options['duplicate_handling'] ) {
				case 'skip':
					return 'skipped';

				case 'update':
					return $this->update_tag( $existing->id, $row_data );

				case 'create':
					$slug = $this->ensure_unique_slug( $slug, 'tags' );
					break;
			}
		}

		// Create new tag.
		$insert_data = array(
			'name'        => $name,
			'slug'        => $slug,
			'description' => sanitize_textarea_field( $row_data['description'] ?? '' ),
			'status'      => sanitize_text_field( $row_data['status'] ?? 'active' ),
			'created_at'  => current_time( 'mysql' ),
			'updated_at'  => current_time( 'mysql' ),
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->insert( "{$wpdb->prefix}ggmc_tags", $insert_data );

		if ( $result ) {
			return 'created';
		}

		return new WP_Error( 'insert_failed', __( 'Failed to create tag', 'ggm-commerce' ) );
	}

	/**
	 * Update existing tag
	 *
	 * @param int   $tag_id Existing tag ID.
	 * @param array $row_data Row data from CSV.
	 * @return string|WP_Error Result.
	 */
	private function update_tag( $tag_id, $row_data ) {
		global $wpdb;

		$update_data = array(
			'name'        => sanitize_text_field( $row_data['name'] ),
			'description' => sanitize_textarea_field( $row_data['description'] ?? '' ),
			'status'      => sanitize_text_field( $row_data['status'] ?? 'active' ),
			'updated_at'  => current_time( 'mysql' ),
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$result = $wpdb->update(
			"{$wpdb->prefix}ggmc_tags",
			$update_data,
			array( 'id' => $tag_id )
		);

		if ( false !== $result ) {
			return 'updated';
		}

		return new WP_Error( 'update_failed', __( 'Failed to update tag', 'ggm-commerce' ) );
	}

	/**
	 * Ensure slug is unique
	 *
	 * @param string $slug Original slug.
	 * @param string $type Type: 'categories' or 'tags'.
	 * @return string Unique slug.
	 */
	private function ensure_unique_slug( $slug, $type ) {
		global $wpdb;

		$table    = "{$wpdb->prefix}ggmc_" . $type;
		$original = $slug;
		$counter  = 1;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		while ( $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE slug = %s", $slug ) ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$slug = $original . '-' . $counter;
			$counter++;
		}

		return $slug;
	}

	/**
	 * Download image from URL
	 *
	 * @param string $url Image URL.
	 * @return int|null Attachment ID or null.
	 */
	private function download_image( $url ) {
		if ( empty( $url ) || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
			return null;
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';

		// Download to temp file.
		$temp_file = download_url( $url, 30 ); // 30 second timeout.

		if ( is_wp_error( $temp_file ) ) {
			$this->progress['warnings'][] = array(
				'message' => sprintf(
					/* translators: %s: image URL */
					__( 'Failed to download image: %s', 'ggm-commerce' ),
					$url
				),
			);
			return null;
		}

		// Prepare file array.
		$filename = basename( wp_parse_url( $url, PHP_URL_PATH ) );
		if ( empty( $filename ) ) {
			$filename = 'imported-image-' . time() . '.jpg';
		}

		$file_array = array(
			'name'     => $filename,
			'tmp_name' => $temp_file,
		);

		// Upload to media library.
		$attachment_id = media_handle_sideload( $file_array, 0 );

		if ( is_wp_error( $attachment_id ) ) {
			@unlink( $temp_file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			$this->progress['warnings'][] = array(
				'message' => sprintf(
					/* translators: %s: image URL */
					__( 'Failed to import image: %s', 'ggm-commerce' ),
					$url
				),
			);
			return null;
		}

		return $attachment_id;
	}

	/**
	 * Save import progress to transient
	 */
	private function save_progress() {
		set_transient( $this->session_key, $this->progress, HOUR_IN_SECONDS );
	}

	/**
	 * Get current import progress
	 *
	 * @return array Progress data.
	 */
	public function get_progress() {
		$progress = get_transient( $this->session_key );
		return $progress ? $progress : $this->progress;
	}

	/**
	 * Clear import session
	 */
	public function clear_session() {
		delete_transient( $this->session_key );
	}

	/**
	 * Get upload directory for imports
	 *
	 * @return string Upload directory path.
	 */
	public function get_upload_directory() {
		$upload_dir = wp_upload_dir();
		$import_dir = $upload_dir['basedir'] . '/ggm-commerce-imports/';

		if ( ! file_exists( $import_dir ) ) {
			wp_mkdir_p( $import_dir );
			file_put_contents( $import_dir . 'index.php', '<?php // Silence is golden' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			file_put_contents( $import_dir . '.htaccess', "deny from all\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		}

		return $import_dir;
	}
}
