<?php

namespace WarrantyCart\Admin\Product;

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

/**
 * Admin Product Import Class
 *
 * Handles CSV import functionality for warranty products.
 */
class Import {

	/**
	 * Constructor
	 */
	public function __construct() {
		add_action( 'admin_post_warranty_import_csv', array( $this, 'handle_csv_import' ) );
	}

	/**
	 * Handle CSV import
	 *
	 * Processes the CSV import request from the admin interface.
	 */
	public function handle_csv_import(): void {
		if ( ! isset( $_POST['warranty_import_nonce'] ) ||
			! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['warranty_import_nonce'] ) ), 'warranty_import_csv' ) ) {
			warranty_cart()->log( 'CSV import security check failed - invalid nonce', 'warning' );
			wp_die( esc_html__( 'Security check failed.', 'warranty-cart' ) );
		}

		if ( ! current_user_can( 'import_warranty_products' ) ) {
			warranty_cart()->log( 'CSV import access denied - insufficient permissions for user: ' . get_current_user_id(), 'warning' );
			wp_die( esc_html__( 'Insufficient permissions.', 'warranty-cart' ) );
		}

		// Validate and sanitize file upload
		if ( empty( $_FILES['csv_file']['tmp_name'] ) ) {
			wp_safe_redirect( add_query_arg( 'import_error', 'no_file', wp_get_referer() ) );
			exit;
		}

		// Use WordPress file validation functions
		$file_upload = wp_handle_upload(
			$_FILES['csv_file'],
			array(
				'test_form'            => false,
				'upload_error_handler' => function ( $file, $message ) {
					warranty_cart()->log( 'File upload error: ' . $message, 'error' );
					wp_safe_redirect( add_query_arg( 'import_error', 'upload_failed', wp_get_referer() ) );
					exit;
				},
			)
		);

		if ( isset( $file_upload['error'] ) ) {
			warranty_cart()->log( 'File upload error: ' . $file_upload['error'], 'error' );
			wp_safe_redirect( add_query_arg( 'import_error', 'upload_failed', wp_get_referer() ) );
			exit;
		}

		$file_path = $file_upload['file'];
		$category  = sanitize_text_field( wp_unslash( $_POST['category'] ?? 'warranties' ) );

		// Additional validation for CSV files
		$mime_type      = wp_get_image_mime( $file_path ); // This works for any file type
		$allowed_mimes  = array( 'text/csv', 'application/csv', 'text/plain' );
		$file_extension = strtolower( pathinfo( $file_upload['url'], PATHINFO_EXTENSION ) );

		if ( ! in_array( $mime_type, $allowed_mimes, true ) && 'csv' !== $file_extension ) {
			// Clean up uploaded file
			wp_delete_file( $file_path );
			wp_safe_redirect( add_query_arg( 'import_error', 'invalid_file', wp_get_referer() ) );
			exit;
		}

		$imported = $this->process_csv_import( $file_path, $category );

		// Clean up uploaded file after processing
		wp_delete_file( $file_path );

		wp_safe_redirect( add_query_arg( array( 'imported' => $imported ), wp_get_referer() ) );
		exit;
	}

	/**
	 * Process CSV import
	 *
	 * @param string $file_path CSV file path.
	 * @param string $category  Product category.
	 *
	 * @return int Number of imported products.
	 */
	public function process_csv_import( string $file_path, string $category ): int {
		return $this->import_report( $file_path, $category )['imported'];
	}

	/**
	 * Import a CSV and say what happened to every row.
	 *
	 * `process_csv_import()` returns a count and nothing else, and the REST
	 * endpoint reported that count as `total`, `successful` and a hardcoded
	 * `failed: 0`. A hundred-row file with forty bad rows therefore imported
	 * sixty products and told the shop "Successfully imported 60 warranty
	 * products", with nothing anywhere admitting the other forty existed.
	 *
	 * @param string $file_path Path to the uploaded CSV.
	 * @param string $category  Product category slug for the new products.
	 *
	 * @return array{imported: int, skipped: int, failed: int, errors: array<int, string>}
	 */
	public function import_report( string $file_path, string $category ): array {
		$imported = 0;
		$skipped  = 0;
		$failed   = 0;
		$errors   = array();

		/*
		 * Asked before opening, not discovered by opening.
		 *
		 * `fopen()` on a missing or unreadable path raises a PHP warning before
		 * it returns false. The `false === $handle` check below handles the
		 * return value fine, but the warning has already been emitted — into the
		 * admin page on a site with display_errors on, and into the log on every
		 * other. Checking first means a bad path is answered quietly with 0,
		 * which is what the caller already expects.
		 */
		if ( ! is_readable( $file_path ) ) {
			$errors[] = __( 'The uploaded file could not be read.', 'warranty-cart' );

			return compact( 'imported', 'skipped', 'failed', 'errors' );
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$handle = fopen( $file_path, 'r' );

		if ( false === $handle ) {
			$errors[] = __( 'The uploaded file could not be opened.', 'warranty-cart' );

			return compact( 'imported', 'skipped', 'failed', 'errors' );
		}

		// Get header row
		$header = $this->read_csv_row( $handle );
		if ( ! $header ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			fclose( $handle );

			$errors[] = __( 'The file has no header row.', 'warranty-cart' );

			return compact( 'imported', 'skipped', 'failed', 'errors' );
		}

		// Counted from the header, so a reported line number matches what the
		// shop sees when it opens the file.
		$line = 1;

		// Process each row
		$row = $this->read_csv_row( $handle );
		while ( false !== $row ) {
			++$line;

			if ( count( $row ) < 3 ) {
				++$skipped;
				$errors[] = sprintf(
					/* translators: %d: line number in the CSV. */
					__( 'Line %d: not enough columns.', 'warranty-cart' ),
					$line
				);

				$row = $this->read_csv_row( $handle );
				continue;
			}

			$data = array_combine( $header, $row );

			if ( ! $data || empty( $data['name'] ) || empty( $data['price'] ) ) {
				++$skipped;
				$errors[] = sprintf(
					/* translators: %d: line number in the CSV. */
					__( 'Line %d: a name and a price are required.', 'warranty-cart' ),
					$line
				);

				$row = $this->read_csv_row( $handle );
				continue;
			}

			if ( $this->create_warranty_product( $data, $category ) ) {
				++$imported;
				warranty_cart()->log( "Successfully imported warranty product: {$data['name']}", 'info' );
			} else {
				++$failed;
				$errors[] = sprintf(
					/* translators: %1$d: line number, %2$s: product name. */
					__( 'Line %1$d: “%2$s” could not be saved.', 'warranty-cart' ),
					$line,
					$data['name']
				);

				warranty_cart()->log( "Failed to import warranty product: {$data['name']}", 'warning' );
			}

			$row = $this->read_csv_row( $handle );
		}

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

		return compact( 'imported', 'skipped', 'failed', 'errors' );
	}

	/**
	 * Read one CSV row.
	 *
	 * Every argument spelled out, because the fifth one has to be. PHP 8.4
	 * deprecates calling `fgetcsv()` without `$escape` — it currently defaults
	 * to a backslash, and that default is going away — so four bare calls here
	 * emitted a deprecation notice per row of every import.
	 *
	 * The value passed is `''`, adopting the coming behaviour rather than
	 * pinning the old one. Backslash escaping is a PHP quirk that RFC 4180 does
	 * not have and no spreadsheet emits: Excel, Numbers and Google Sheets all
	 * escape a quote by doubling it. With the old default, a product
	 * description ending in a backslash swallowed the delimiter after it and
	 * shifted every remaining column of that row.
	 *
	 * @param resource $handle An open CSV file handle.
	 *
	 * @return array<int, string|null>|false The row, or false at end of file.
	 */
	private function read_csv_row( $handle ) {
		return fgetcsv( $handle, 0, ',', '"', '' );
	}

	/**
	 * Validate CSV headers.
	 *
	 * @param array $headers CSV header row.
	 * @return bool Whether headers are valid.
	 */
	public function validate_csv_headers( array $headers ): bool {
		$required = array( 'name', 'description', 'price', 'type' );
		foreach ( $required as $field ) {
			if ( ! in_array( $field, $headers, true ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Validate a single CSV data row.
	 *
	 * @param array $data Associative array of row data.
	 * @return bool Whether the row is valid.
	 */
	public function validate_csv_data( array $data ): bool {
		return ! empty( $data['name'] );
	}

	/**
	 * Create a warranty product from CSV row data.
	 *
	 * @param array  $data     Associative product data (name, description, price, type).
	 * @param string $category Product category slug.
	 * @return int Created product ID, or 0 on failure.
	 */
	public function create_product_from_csv( array $data, string $category ): int {
		$product = new \WC_Product_Simple();
		$product->set_name( sanitize_text_field( $data['name'] ) );
		$product->set_description( sanitize_textarea_field( $data['description'] ?? '' ) );
		$product->set_regular_price( floatval( $data['price'] ) );
		$product->set_virtual( true );
		$product->set_catalog_visibility( 'hidden' );

		if ( ! empty( $data['sku'] ) ) {
			$product->set_sku( sanitize_text_field( $data['sku'] ) );
		}

		$product_id = $product->save();
		if ( ! $product_id ) {
			return 0;
		}

		wp_set_object_terms( $product_id, $category, 'product_cat' );

		/*
		 * `type`, or the older `is_main_warranty` that one of the two create
		 * paths used to read. Both are accepted so a file written for either
		 * behaviour imports as its author meant.
		 */
		$is_main = ( isset( $data['type'] ) && 'main_warranty' === $data['type'] )
			|| ( isset( $data['is_main_warranty'] ) && 'yes' === strtolower( (string) $data['is_main_warranty'] ) );

		$type = $is_main ? 'main_warranty' : 'additional_service';

		/*
		 * The term, which the importer never wrote.
		 *
		 * Only name, description, price and type were read, so every imported
		 * plan reached the storefront with no duration of its own and fell back
		 * to the store default when an order completed. A shop importing a
		 * catalogue of one, two and three-year cover got three products that all
		 * ran for whatever the default said — the one thing an imported plan
		 * most needs to carry, silently discarded.
		 *
		 * Both columns are optional. A file without them behaves as it always
		 * did, which is what the fallback is for.
		 */
		$duration = isset( $data['duration'] ) ? absint( $data['duration'] ) : 0;
		$unit     = isset( $data['duration_unit'] ) ? sanitize_text_field( $data['duration_unit'] ) : '';

		if ( $duration > 0 ) {
			$product->update_meta_data( '_warranty_cart_duration', $duration );
		}

		if ( in_array( $unit, array( 'days', 'months', 'years' ), true ) ) {
			$product->update_meta_data( '_warranty_cart_duration_unit', $unit );
		}

		if ( isset( $data['coverage_details'] ) ) {
			$product->update_meta_data(
				'_warranty_cart_coverage_details',
				sanitize_textarea_field( $data['coverage_details'] )
			);
		}

		/*
		 * Both keys, and both are load-bearing.
		 *
		 * The importer wrote `_warranty_product_type` while the product editor
		 * and REST controllers write `_warranty_cart_product_type`, and it set
		 * no status at all — but the shortcode's query requires the status to
		 * be 'enabled'. An imported catalogue was therefore invisible on the
		 * storefront and absent from the admin's warranty list.
		 */
		$product->update_meta_data( '_warranty_cart_product_type', $type );
		$product->update_meta_data( '_warranty_cart_product_status', 'enabled' );
		$product->save();

		return $product_id;
	}

	/**
	 * Create one warranty product from a CSV row.
	 *
	 * Delegates, and used to duplicate. There were two create methods that had
	 * drifted apart, and the drift was not cosmetic: this one decided the type
	 * from an `is_main_warranty` column reading "yes", while
	 * `create_product_from_csv()` and `validate_csv_headers()` both use `type`.
	 * The header check requires `type` and this path ignored it, so **every
	 * imported product became an additional service** — and an additional
	 * service is never offered beside a product, so an imported catalogue sold
	 * nothing and the shop had no way to see why.
	 *
	 * @param array  $data     Associative row data.
	 * @param string $category Product category slug.
	 *
	 * @return bool Whether it was created.
	 */
	private function create_warranty_product( array $data, string $category ): bool {
		return $this->create_product_from_csv( $data, $category ) > 0;
	}
}
