<?php
/**
 * CSV import/export for BW Map Magnet.
 *
 * Export: stream a CSV of every map item.
 * Import: upload a CSV; create or upsert items, auto-creating categories as needed.
 */

defined( 'ABSPATH' ) || exit;

class BW_Map_Magnet_Import_Export {

	const COLUMNS = [ 'title', 'category', 'latitude', 'longitude', 'excerpt', 'content', 'image_url' ];

	/** @var array|null Last import result, surfaced as an admin notice on the next page load. */
	private $last_import = null;

	private static $instance = null;

	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function boot() {
		add_action( 'admin_menu', [ $this, 'register_menu' ] );
		add_action( 'admin_init', [ $this, 'handle_export' ] );
		add_action( 'admin_init', [ $this, 'handle_import' ] );
	}

	public function register_menu() {
		add_submenu_page(
			'edit.php?post_type=' . BW_MAP_MAGNET_CPT,
			__( 'Import / Export', 'bw-map-magnet' ),
			__( 'Import / Export', 'bw-map-magnet' ),
			'manage_options',
			'bw-map-magnet-io',
			[ $this, 'render_page' ]
		);
	}

	public function render_page() {
		?>
		<div class="wrap">
			<h1><?php esc_html_e( 'BW Map Magnet — Import / Export', 'bw-map-magnet' ); ?></h1>

			<?php if ( $this->last_import ) : ?>
				<div class="notice <?php echo $this->last_import['failed'] ? 'notice-warning' : 'notice-success'; ?>">
					<p>
						<strong><?php esc_html_e( 'Import complete.', 'bw-map-magnet' ); ?></strong>
						<?php
						printf(
							/* translators: %1$d created, %2$d updated, %3$d skipped, %4$d failed */
							esc_html__( 'Created: %1$d &middot; Updated: %2$d &middot; Skipped: %3$d &middot; Failed: %4$d', 'bw-map-magnet' ),
							(int) $this->last_import['created'],
							(int) $this->last_import['updated'],
							(int) $this->last_import['skipped'],
							(int) $this->last_import['failed']
						);
						?>
					</p>
					<?php if ( ! empty( $this->last_import['images_downloaded'] ) || ! empty( $this->last_import['images_failed'] ) ) : ?>
						<p>
							<?php
							printf(
								/* translators: %1$d images downloaded, %2$d image downloads failed */
								esc_html__( 'Images downloaded: %1$d &middot; Image downloads failed: %2$d', 'bw-map-magnet' ),
								(int) $this->last_import['images_downloaded'],
								(int) $this->last_import['images_failed']
							);
							?>
						</p>
					<?php endif; ?>
					<?php if ( ! empty( $this->last_import['errors'] ) ) : ?>
						<ul style="margin: 4px 0 8px 20px; list-style: disc;">
							<?php foreach ( $this->last_import['errors'] as $err ) : ?>
								<li><?php echo esc_html( $err ); ?></li>
							<?php endforeach; ?>
						</ul>
					<?php endif; ?>
				</div>
			<?php endif; ?>

			<h2><?php esc_html_e( 'Export', 'bw-map-magnet' ); ?></h2>
			<p><?php esc_html_e( 'Download every map item as a CSV file. The columns map directly to the importer so you can edit in a spreadsheet and re-upload.', 'bw-map-magnet' ); ?></p>
			<form method="post">
				<?php wp_nonce_field( 'bw_map_magnet_export', 'bw_map_magnet_export_nonce' ); ?>
				<input type="hidden" name="bw_map_magnet_action" value="export" />
				<?php submit_button( __( 'Download CSV', 'bw-map-magnet' ), 'primary', 'submit', false ); ?>
			</form>

			<hr style="margin: 28px 0;" />

			<h2><?php esc_html_e( 'Import', 'bw-map-magnet' ); ?></h2>
			<p><?php esc_html_e( 'Upload a CSV file to bulk-create or update map items. The first row must be a header row.', 'bw-map-magnet' ); ?></p>

			<form method="post" enctype="multipart/form-data">
				<?php wp_nonce_field( 'bw_map_magnet_import', 'bw_map_magnet_import_nonce' ); ?>
				<input type="hidden" name="bw_map_magnet_action" value="import" />
				<p>
					<label for="bw_map_csv"><strong><?php esc_html_e( 'CSV file', 'bw-map-magnet' ); ?></strong></label><br />
					<input type="file" name="bw_map_csv" id="bw_map_csv" accept=".csv,text/csv" required />
				</p>
				<p>
					<label>
						<input type="checkbox" name="bw_map_update_existing" value="1" />
						<?php esc_html_e( 'Update existing items (match by title). Otherwise duplicates are skipped.', 'bw-map-magnet' ); ?>
					</label>
				</p>
				<p>
					<label>
						<input type="checkbox" name="bw_map_download_images" value="1" />
						<?php esc_html_e( 'Download images and set as featured images.', 'bw-map-magnet' ); ?>
					</label>
					<br />
					<span class="description" style="margin-left: 24px;">
						<?php esc_html_e( 'When the image_url column is set, fetch the image into your media library and attach it as the post\'s featured image. Slower, but images become part of your site instead of hot-linking. Failed downloads fall back to the external URL.', 'bw-map-magnet' ); ?>
					</span>
				</p>
				<?php submit_button( __( 'Import CSV', 'bw-map-magnet' ), 'primary', 'submit', false ); ?>
			</form>

			<h3 style="margin-top: 28px;"><?php esc_html_e( 'CSV format', 'bw-map-magnet' ); ?></h3>
			<p><?php esc_html_e( 'Required columns:', 'bw-map-magnet' ); ?> <code>title</code>, <code>latitude</code>, <code>longitude</code></p>
			<p><?php esc_html_e( 'Optional columns:', 'bw-map-magnet' ); ?> <code>category</code> (slug or name; auto-created if missing), <code>excerpt</code>, <code>content</code>, <code>image_url</code></p>

			<details style="margin-top: 12px;">
				<summary style="cursor: pointer; font-weight: 600;"><?php esc_html_e( 'Example CSV', 'bw-map-magnet' ); ?></summary>
				<pre style="background: #f6f7f7; padding: 12px; border-radius: 4px; overflow-x: auto;"><?php echo esc_html( $this->example_csv() ); ?></pre>
			</details>
		</div>
		<?php
	}

	private function example_csv() {
		return "title,category,latitude,longitude,excerpt,content,image_url\n"
			. "Grace Bay Beach,Beaches,21.7960,-72.1750,\"Three miles of powder-soft sand.\",\"Long-form description here.\",https://example.com/grace-bay.jpg\n"
			. "Da Conch Shack,Best Restaurants,21.7710,-72.2820,\"Fresh-cracked conch on the beach.\",\"\",\n";
	}

	public function handle_export() {
		if ( ! isset( $_POST['bw_map_magnet_action'] ) || 'export' !== $_POST['bw_map_magnet_action'] ) {
			return;
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		if ( ! isset( $_POST['bw_map_magnet_export_nonce'] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bw_map_magnet_export_nonce'] ) ), 'bw_map_magnet_export' ) ) {
			return;
		}

		$this->stream_export();
		exit;
	}

	private function stream_export() {
		$filename = 'bw-map-magnet-' . gmdate( 'Y-m-d' ) . '.csv';

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );

		$out = fopen( 'php://output', 'w' );
		// UTF-8 BOM so Excel detects encoding.
		fwrite( $out, "\xEF\xBB\xBF" );
		fputcsv( $out, self::COLUMNS );

		$query = new WP_Query( [
			'post_type'      => BW_MAP_MAGNET_CPT,
			'posts_per_page' => -1,
			'post_status'    => [ 'publish', 'draft', 'private' ],
			'orderby'        => 'title',
			'order'          => 'ASC',
		] );

		while ( $query->have_posts() ) {
			$query->the_post();
			$post    = get_post();
			$post_id = $post->ID;

			$terms     = get_the_terms( $post_id, BW_MAP_MAGNET_TAX );
			$cat_slug  = ( ! empty( $terms ) && ! is_wp_error( $terms ) ) ? $terms[0]->slug : '';
			$image_url = get_post_meta( $post_id, '_bw_map_image_url', true );

			fputcsv( $out, [
				$post->post_title,
				$cat_slug,
				get_post_meta( $post_id, '_bw_map_lat', true ),
				get_post_meta( $post_id, '_bw_map_lng', true ),
				$post->post_excerpt,
				$post->post_content,
				$image_url,
			] );
		}
		wp_reset_postdata();

		fclose( $out );
	}

	public function handle_import() {
		if ( ! isset( $_POST['bw_map_magnet_action'] ) || 'import' !== $_POST['bw_map_magnet_action'] ) {
			return;
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		if ( ! isset( $_POST['bw_map_magnet_import_nonce'] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bw_map_magnet_import_nonce'] ) ), 'bw_map_magnet_import' ) ) {
			return;
		}
		if ( empty( $_FILES['bw_map_csv'] ) || empty( $_FILES['bw_map_csv']['tmp_name'] ) ) {
			$this->last_import = $this->result_with_error( __( 'No file uploaded.', 'bw-map-magnet' ) );
			return;
		}
		if ( ! empty( $_FILES['bw_map_csv']['error'] ) ) {
			$this->last_import = $this->result_with_error(
				/* translators: %d: PHP upload error code */
				sprintf( __( 'Upload error (code %d).', 'bw-map-magnet' ), (int) $_FILES['bw_map_csv']['error'] )
			);
			return;
		}

		$tmp_name = isset( $_FILES['bw_map_csv']['tmp_name'] ) ? (string) $_FILES['bw_map_csv']['tmp_name'] : '';
		if ( ! is_uploaded_file( $tmp_name ) ) {
			$this->last_import = $this->result_with_error( __( 'Uploaded file is not valid.', 'bw-map-magnet' ) );
			return;
		}

		$update_existing  = ! empty( $_POST['bw_map_update_existing'] );
		$download_images  = ! empty( $_POST['bw_map_download_images'] );

		// Image sideloads can take a while.
		@set_time_limit( 600 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged

		$this->last_import = $this->import_csv( $tmp_name, $update_existing, $download_images );
	}

	private function result_with_error( $message ) {
		return [
			'created'            => 0,
			'updated'            => 0,
			'skipped'            => 0,
			'failed'             => 1,
			'images_downloaded'  => 0,
			'images_failed'      => 0,
			'errors'             => [ $message ],
		];
	}

	private function import_csv( $path, $update_existing, $download_images = false ) {
		$result = [
			'created'           => 0,
			'updated'           => 0,
			'skipped'           => 0,
			'failed'            => 0,
			'images_downloaded' => 0,
			'images_failed'     => 0,
			'errors'            => [],
		];

		$handle = fopen( $path, 'r' );
		if ( ! $handle ) {
			return $this->result_with_error( __( 'Could not open uploaded file.', 'bw-map-magnet' ) );
		}

		$header = fgetcsv( $handle );
		if ( ! $header ) {
			fclose( $handle );
			return $this->result_with_error( __( 'CSV is empty or missing a header row.', 'bw-map-magnet' ) );
		}

		// Strip BOM from the first column.
		if ( isset( $header[0] ) ) {
			$header[0] = preg_replace( '/^\xEF\xBB\xBF/', '', $header[0] );
		}

		$map     = $this->header_map( $header );
		$missing = array_diff( [ 'title', 'latitude', 'longitude' ], array_keys( $map ) );
		if ( ! empty( $missing ) ) {
			fclose( $handle );
			return $this->result_with_error(
				sprintf(
					/* translators: %s: list of missing required column names */
					__( 'Missing required columns: %s', 'bw-map-magnet' ),
					implode( ', ', $missing )
				)
			);
		}

		$row_number = 1; // header.

		while ( ( $row = fgetcsv( $handle ) ) !== false ) {
			$row_number++;
			$entry = $this->row_to_entry( $row, $map );

			$validation = $this->validate_entry( $entry, $row_number );
			if ( $validation ) {
				$result['failed']++;
				$result['errors'][] = $validation;
				continue;
			}

			$existing_id = $this->find_existing_id( $entry['title'] );

			if ( $existing_id && ! $update_existing ) {
				$result['skipped']++;
				continue;
			}

			$term_id = '';
			if ( '' !== $entry['category'] ) {
				$term_id = $this->resolve_term_id( $entry['category'] );
			}

			$post_data = [
				'post_type'    => BW_MAP_MAGNET_CPT,
				'post_status'  => 'publish',
				'post_title'   => $entry['title'],
				'post_content' => $entry['content'],
				'post_excerpt' => $entry['excerpt'],
			];

			if ( $existing_id ) {
				$post_data['ID'] = $existing_id;
				$post_id         = wp_update_post( $post_data, true );
			} else {
				$post_id = wp_insert_post( $post_data, true );
			}

			if ( is_wp_error( $post_id ) || ! $post_id ) {
				$result['failed']++;
				$result['errors'][] = sprintf(
					/* translators: 1: row number, 2: title, 3: error message */
					__( 'Row %1$d (%2$s): %3$s', 'bw-map-magnet' ),
					$row_number,
					$entry['title'],
					is_wp_error( $post_id ) ? $post_id->get_error_message() : __( 'unknown error', 'bw-map-magnet' )
				);
				continue;
			}

			update_post_meta( $post_id, '_bw_map_lat', (float) $entry['latitude'] );
			update_post_meta( $post_id, '_bw_map_lng', (float) $entry['longitude'] );

			if ( '' === $entry['image_url'] ) {
				delete_post_meta( $post_id, '_bw_map_image_url' );
			} else {
				$downloaded = false;
				if ( $download_images ) {
					$attachment_id = $this->sideload_image( $entry['image_url'], $post_id, $entry['title'] );
					if ( $attachment_id ) {
						set_post_thumbnail( $post_id, $attachment_id );
						delete_post_meta( $post_id, '_bw_map_image_url' );
						$result['images_downloaded']++;
						$downloaded = true;
					} else {
						$result['images_failed']++;
						$result['errors'][] = sprintf(
							/* translators: 1: row number, 2: title */
							__( 'Row %1$d (%2$s): image download failed; URL stored as fallback.', 'bw-map-magnet' ),
							$row_number,
							$entry['title']
						);
					}
				}

				if ( ! $downloaded ) {
					// Either the option was off, or the sideload failed — keep the URL as a fallback.
					update_post_meta( $post_id, '_bw_map_image_url', $entry['image_url'] );
				}
			}

			if ( $term_id ) {
				wp_set_object_terms( $post_id, [ (int) $term_id ], BW_MAP_MAGNET_TAX );
			}

			if ( $existing_id ) {
				$result['updated']++;
			} else {
				$result['created']++;
			}
		}

		fclose( $handle );
		return $result;
	}

	private function header_map( $header ) {
		$aliases = [
			'title'      => [ 'title', 'name' ],
			'category'   => [ 'category', 'cat' ],
			'latitude'   => [ 'latitude', 'lat' ],
			'longitude'  => [ 'longitude', 'lng', 'lon' ],
			'excerpt'    => [ 'excerpt', 'summary' ],
			'content'    => [ 'content', 'description', 'body' ],
			'image_url'  => [ 'image_url', 'image', 'thumbnail' ],
		];

		$out = [];
		foreach ( $header as $idx => $col_raw ) {
			$col = strtolower( trim( (string) $col_raw ) );
			foreach ( $aliases as $canonical => $names ) {
				if ( in_array( $col, $names, true ) ) {
					$out[ $canonical ] = $idx;
					break;
				}
			}
		}
		return $out;
	}

	private function row_to_entry( $row, $map ) {
		$get = function ( $key ) use ( $row, $map ) {
			if ( ! isset( $map[ $key ] ) ) {
				return '';
			}
			$idx = $map[ $key ];
			return isset( $row[ $idx ] ) ? trim( (string) $row[ $idx ] ) : '';
		};

		return [
			'title'     => $get( 'title' ),
			'category'  => $get( 'category' ),
			'latitude'  => $get( 'latitude' ),
			'longitude' => $get( 'longitude' ),
			'excerpt'   => $get( 'excerpt' ),
			'content'   => $get( 'content' ),
			'image_url' => esc_url_raw( $get( 'image_url' ) ),
		];
	}

	private function validate_entry( $entry, $row_number ) {
		if ( '' === $entry['title'] ) {
			return sprintf( /* translators: %d: row number */ __( 'Row %d: missing title.', 'bw-map-magnet' ), $row_number );
		}
		if ( '' === $entry['latitude'] || '' === $entry['longitude'] ) {
			return sprintf( /* translators: %d: row number */ __( 'Row %d: missing latitude or longitude.', 'bw-map-magnet' ), $row_number );
		}
		if ( ! is_numeric( $entry['latitude'] ) || ! is_numeric( $entry['longitude'] ) ) {
			return sprintf( /* translators: %d: row number */ __( 'Row %d: latitude/longitude must be numeric.', 'bw-map-magnet' ), $row_number );
		}
		$lat = (float) $entry['latitude'];
		$lng = (float) $entry['longitude'];
		if ( $lat < -90 || $lat > 90 || $lng < -180 || $lng > 180 ) {
			return sprintf( /* translators: %d: row number */ __( 'Row %d: latitude/longitude out of range.', 'bw-map-magnet' ), $row_number );
		}
		return null;
	}

	private function find_existing_id( $title ) {
		$q = new WP_Query( [
			'post_type'              => BW_MAP_MAGNET_CPT,
			'title'                  => $title,
			'post_status'            => 'any',
			'posts_per_page'         => 1,
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		] );
		return $q->have_posts() ? (int) $q->posts[0] : 0;
	}

	/** @var array Image mimes accepted by the sideloader. */
	private static $sideload_mimes = [
		'image/jpeg' => 'jpg',
		'image/png'  => 'png',
		'image/gif'  => 'gif',
		'image/webp' => 'webp',
	];

	/**
	 * Download an image URL into the media library and attach it to the post.
	 * Returns the attachment ID on success, or 0 on failure.
	 */
	private function sideload_image( $url, $post_id, $title ) {
		if ( ! function_exists( 'media_handle_sideload' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			require_once ABSPATH . 'wp-admin/includes/media.php';
			require_once ABSPATH . 'wp-admin/includes/image.php';
		}

		$tmp = download_url( $url, 30 );
		if ( is_wp_error( $tmp ) ) {
			return 0;
		}

		// Sniff the real mime so the filename has the right extension AND so our repair
		// filter (below) can hand WordPress a valid filetype record if other plugins
		// have intercepted wp_check_filetype_and_ext.
		$mime = function_exists( 'wp_get_image_mime' ) ? wp_get_image_mime( $tmp ) : '';
		if ( ! isset( self::$sideload_mimes[ $mime ] ) ) {
			@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			return 0;
		}

		$slug = sanitize_title( $title );
		if ( '' === $slug ) {
			$slug = 'map-item-' . (int) $post_id;
		}
		$file_array = [
			'name'     => $slug . '.' . self::$sideload_mimes[ $mime ],
			'tmp_name' => $tmp,
		];

		// Some site-wide plugins (typically SVG-upload extensions) intercept
		// wp_check_filetype_and_ext and over-filter the result — breaking legitimate
		// JPEG/PNG uploads. Run a high-priority repair filter just for this sideload
		// that restores the correct ext/type when a real image is detected.
		add_filter( 'wp_check_filetype_and_ext', [ $this, 'repair_filetype_check' ], 999, 4 );

		$attachment_id = media_handle_sideload( $file_array, $post_id, $title );

		remove_filter( 'wp_check_filetype_and_ext', [ $this, 'repair_filetype_check' ], 999 );

		if ( is_wp_error( $attachment_id ) ) {
			if ( file_exists( $tmp ) ) {
				@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			}
			return 0;
		}

		update_post_meta( $attachment_id, '_wp_attachment_image_alt', $title );

		return (int) $attachment_id;
	}

	/**
	 * Repair filter — if upstream filters returned an empty/invalid filetype record
	 * for a file we know is a real image (by content-sniffed MIME), restore a valid
	 * record so media_handle_sideload accepts the upload.
	 */
	public function repair_filetype_check( $data, $file, $filename, $mimes ) {
		if ( ! empty( $data['ext'] ) && ! empty( $data['type'] ) ) {
			return $data;
		}
		if ( ! function_exists( 'wp_get_image_mime' ) || ! file_exists( $file ) ) {
			return $data;
		}
		$real_mime = wp_get_image_mime( $file );
		if ( isset( self::$sideload_mimes[ $real_mime ] ) ) {
			return [
				'ext'             => self::$sideload_mimes[ $real_mime ],
				'type'            => $real_mime,
				'proper_filename' => $filename,
			];
		}
		return $data;
	}

	private function resolve_term_id( $value ) {
		$value = trim( (string) $value );
		if ( '' === $value ) {
			return 0;
		}

		// Try slug first.
		$by_slug = get_term_by( 'slug', sanitize_title( $value ), BW_MAP_MAGNET_TAX );
		if ( $by_slug ) {
			return (int) $by_slug->term_id;
		}

		// Then exact name.
		$by_name = get_term_by( 'name', $value, BW_MAP_MAGNET_TAX );
		if ( $by_name ) {
			return (int) $by_name->term_id;
		}

		// Create it.
		$created = wp_insert_term( $value, BW_MAP_MAGNET_TAX );
		if ( is_wp_error( $created ) ) {
			return 0;
		}
		return (int) $created['term_id'];
	}
}
