<?php
/**
 * Image optimization workflow.
 *
 * @package NaveenCodesImageOptimizer
 */

namespace NaveenCodesImageOptimizer;

use WP_Error;

defined( 'ABSPATH' ) || exit;

/**
 * Hooks uploads, background queue processing, and bulk optimization.
 */
class Optimizer {

	/**
	 * Settings service.
	 *
	 * @var Settings
	 */
	protected $settings;

	/**
	 * API client.
	 *
	 * @var Api_Client
	 */
	protected $api_client;

	/**
	 * Queue.
	 *
	 * @var Queue
	 */
	protected $queue;

	/**
	 * Logger.
	 *
	 * @var Logger
	 */
	protected $logger;

	/**
	 * Supported MIME types.
	 *
	 * @var array
	 */
	protected $supported_mimes = array(
		'image/jpeg',
		'image/png',
		'image/gif',
		'image/webp',
		'image/avif',
	);

	/**
	 * Constructor.
	 *
	 * @param Settings   $settings   Settings.
	 * @param Api_Client $api_client API client.
	 * @param Queue      $queue      Queue.
	 * @param Logger     $logger     Logger.
	 */
	public function __construct( Settings $settings, Api_Client $api_client, Queue $queue, Logger $logger ) {
		$this->settings   = $settings;
		$this->api_client = $api_client;
		$this->queue      = $queue;
		$this->logger     = $logger;

		add_filter( 'wp_handle_upload', array( $this, 'maybe_optimize_uploaded_file' ), 20, 2 );
		add_filter( 'wp_generate_attachment_metadata', array( $this, 'maybe_optimize_generated_sizes' ), 20, 2 );
		add_action( 'add_attachment', array( $this, 'maybe_queue_new_attachment' ) );
		add_action( 'delete_attachment', array( $this, 'cleanup_deleted_attachment' ) );
		add_action( 'naveencodes_process_queue_event', array( $this, 'process_queue_event' ) );
	}

	/**
	 * Optimize uploads immediately when async mode is disabled.
	 *
	 * @param array  $upload  Upload result.
	 * @param string $context Upload context.
	 * @return array
	 */
	public function maybe_optimize_uploaded_file( $upload, $context ) {
		unset( $context );

		if ( ! empty( $upload['error'] ) || empty( $upload['file'] ) || $this->settings->get( 'async_mode' ) ) {
			return $upload;
		}

		if ( ! $this->is_supported_mime( $upload['type'] ?? '' ) ) {
			return $upload;
		}

		$result = $this->optimize_local_file( $upload['file'], 0, 'sync-upload' );

		if ( is_wp_error( $result ) ) {
			$this->logger->log(
				'warning',
				'Upload optimization failed; original file kept.',
				array(
					'file'  => wp_basename( $upload['file'] ),
					'error' => $result->get_error_message(),
				)
			);
		} else {
			set_transient( 'naveencodes_sync_upload_' . md5( wp_normalize_path( $upload['file'] ) ), $result, HOUR_IN_SECONDS );
		}

		return $upload;
	}

	/**
	 * Optimize generated sizes in sync mode after WordPress creates them.
	 *
	 * @param array $metadata      Attachment metadata.
	 * @param int   $attachment_id Attachment ID.
	 * @return array
	 */
	public function maybe_optimize_generated_sizes( $metadata, $attachment_id ) {
		if ( $this->settings->get( 'async_mode' ) || ! wp_attachment_is_image( $attachment_id ) ) {
			return $metadata;
		}

		$base_dir     = dirname( get_attached_file( $attachment_id ) );
		$total_before = 0;
		$total_after  = 0;
		$optimized    = 0;
		$main_file    = get_attached_file( $attachment_id );
		$cache_key    = $main_file ? 'naveencodes_sync_upload_' . md5( wp_normalize_path( $main_file ) ) : '';
		$cached       = $cache_key ? get_transient( $cache_key ) : false;

		if ( is_array( $cached ) ) {
			$total_before += absint( $cached['bytes_before'] ?? 0 );
			$total_after  += absint( $cached['bytes_after'] ?? 0 );
			$optimized++;

			delete_transient( $cache_key );
		}

		foreach ( (array) $metadata['sizes'] as $size ) {
			if ( empty( $size['file'] ) ) {
				continue;
			}

			$path = wp_normalize_path( trailingslashit( $base_dir ) . $size['file'] );
			$result = $this->optimize_local_file( $path, $attachment_id, 'sync-size' );

			if ( is_wp_error( $result ) ) {
				continue;
			}

			$total_before += absint( $result['bytes_before'] );
			$total_after  += absint( $result['bytes_after'] );
			$optimized++;
		}

		if ( $optimized > 0 ) {
			$this->record_successful_optimization( $attachment_id, $total_before, $total_after, 'sync-upload', $optimized );
		}

		return $metadata;
	}

	/**
	 * Queue supported image attachments in async mode.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return void
	 */
	public function maybe_queue_new_attachment( $attachment_id ) {
		if ( ! $this->settings->get( 'async_mode' ) || ! wp_attachment_is_image( $attachment_id ) ) {
			return;
		}

		$file = get_attached_file( $attachment_id );
		$mime = get_post_mime_type( $attachment_id );

		if ( empty( $file ) || ! $this->is_supported_mime( $mime ) ) {
			return;
		}

		$this->queue->enqueue_attachment( $attachment_id );
	}

	/**
	 * Remove queue rows when attachments are deleted.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return void
	 */
	public function cleanup_deleted_attachment( $attachment_id ) {
		$this->queue->remove_attachment( $attachment_id );
	}

	/**
	 * Process a cron batch.
	 *
	 * @return void
	 */
	public function process_queue_event() {
		$this->process_queue_batch();
	}

	/**
	 * Process pending queue items.
	 *
	 * @param int|null $limit Optional batch size override.
	 * @return array
	 */
	public function process_queue_batch( $limit = null ) {
		$limit       = null === $limit ? (int) $this->settings->get( 'batch_size' ) : absint( $limit );
		$max_retries = (int) $this->settings->get( 'retry_attempts' );
		$items       = $this->queue->claim_batch( $limit );
		$results     = array(
			'processed' => 0,
			'success'   => 0,
			'failed'    => 0,
		);

		foreach ( $items as $attachment_id ) {
			$results['processed']++;

			$result = $this->optimize_attachment( $attachment_id, 'queue' );

			if ( is_wp_error( $result ) ) {
				$this->record_attachment_failure( $attachment_id, $result->get_error_message() );
				$results['failed']++;
				$status = $this->queue->mark_retry_or_failed( $attachment_id, $max_retries );

				if ( 'failed' === $status ) {
					$this->increment_failed_stat();
				}

				$this->logger->log(
					'error',
					'Queued optimization failed.',
					array(
						'attachment_id' => (string) $attachment_id,
						'error'         => $result->get_error_message(),
					)
				);
				continue;
			}

			$results['success']++;
			$this->queue->mark_completed( $attachment_id );
		}

		return $results;
	}

	/**
	 * Optimize all files attached to a media item.
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $context       Optimization context.
	 * @return array|WP_Error
	 */
	public function optimize_attachment( $attachment_id, $context = 'manual' ) {
		$attachment_id = absint( $attachment_id );

		if ( ! $attachment_id || ! wp_attachment_is_image( $attachment_id ) ) {
			return new WP_Error( 'naveencodes_invalid_attachment', __( 'Attachment is not an optimizable image.', 'naveencodes-image-optimizer' ) );
		}

		$files = $this->get_attachment_files( $attachment_id );

		if ( empty( $files ) ) {
			return new WP_Error( 'naveencodes_missing_files', __( 'No local attachment files were found.', 'naveencodes-image-optimizer' ) );
		}

		$total_before = 0;
		$total_after  = 0;
		$optimized    = 0;
		$file_errors  = array();
		$prechecks    = $this->get_attachment_precheck_messages( $attachment_id, $files );

		if ( ! empty( $prechecks ) ) {
			$this->logger->log(
				'warning',
				'Attachment pre-check warning.',
				array(
					'attachment_id' => (string) $attachment_id,
					'warnings'      => $prechecks,
				)
			);
		}

		foreach ( $files as $role => $path ) {
			$result = $this->optimize_local_file( $path, $attachment_id, $context . ':' . $role );

			if ( is_wp_error( $result ) ) {
				$friendly_error = $this->get_friendly_optimization_error_message( $result, $path );

				$this->logger->log(
					'warning',
					'Attachment sub-file optimization failed.',
					array(
						'attachment_id' => (string) $attachment_id,
						'role'          => $role,
						'error'         => $friendly_error,
						'error_code'    => (string) $result->get_error_code(),
					)
				);
				$file_errors[] = $friendly_error;
				continue;
			}

			$total_before += absint( $result['bytes_before'] );
			$total_after  += absint( $result['bytes_after'] );
			$optimized++;
		}

		if ( ! $optimized ) {
			$failure_message = $this->summarize_attachment_failure( $file_errors, $prechecks );
			$this->record_attachment_failure( $attachment_id, $failure_message );
			return new WP_Error( 'naveencodes_no_files_optimized', $failure_message );
		}

		$this->record_successful_optimization( $attachment_id, $total_before, $total_after, $context, $optimized );

		return array(
			'bytes_before' => $total_before,
			'bytes_after'  => $total_after,
			'files'        => $optimized,
		);
	}

	/**
	 * Optimize a single physical file.
	 *
	 * @param string $file_path     Absolute local path.
	 * @param int    $attachment_id Attachment ID if known.
	 * @param string $context       Optimization context.
	 * @return array|WP_Error
	 */
	public function optimize_local_file( $file_path, $attachment_id = 0, $context = 'manual' ) {
		$file_path = wp_normalize_path( $file_path );

		if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
			return new WP_Error( 'naveencodes_file_missing', __( 'The local image file could not be read.', 'naveencodes-image-optimizer' ) );
		}

		$type = wp_check_filetype( $file_path );
		$mime = ! empty( $type['type'] ) ? $type['type'] : ( function_exists( 'mime_content_type' ) ? mime_content_type( $file_path ) : '' );

		if ( ! $this->is_supported_mime( $mime ) ) {
			return new WP_Error( 'naveencodes_unsupported_mime', __( 'This image MIME type is not supported.', 'naveencodes-image-optimizer' ) );
		}

		$result = $this->api_client->optimize_file(
			$file_path,
			array(
				'context' => sanitize_key( $context ),
			)
		);

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$write = $this->replace_file_safely( $file_path, $result['optimized_binary'] );

		if ( is_wp_error( $write ) ) {
			return $write;
		}

		if ( ! empty( $result['formats'] ) ) {
			$this->write_sidecar_formats( $file_path, $result['formats'], $attachment_id );
		}

		$this->logger->log(
			'info',
			'Image optimized successfully.',
			array(
				'attachment_id' => (string) absint( $attachment_id ),
				'file'          => wp_basename( $file_path ),
				'saved_bytes'   => (string) max( 0, absint( $result['bytes_before'] ) - absint( $result['bytes_after'] ) ),
			)
		);

		return array(
			'bytes_before' => absint( $result['bytes_before'] ),
			'bytes_after'  => absint( $result['bytes_after'] ),
		);
	}

	/**
	 * Collect non-blocking pre-check warnings for the attachment.
	 *
	 * These warnings help admins understand risky states such as empty titles,
	 * duplicate titles, duplicate stored file names, or suspicious filenames,
	 * without turning them into hard optimization blockers.
	 *
	 * @param int   $attachment_id Attachment ID.
	 * @param array $files         Attachment files.
	 * @return array
	 */
	protected function get_attachment_precheck_messages( $attachment_id, $files ) {
		$warnings = array();
		$title    = trim( (string) get_the_title( $attachment_id ) );
		$main_file = ! empty( $files['original'] ) ? (string) $files['original'] : (string) reset( $files );
		$file_name = $main_file ? wp_basename( $main_file ) : '';

		if ( '' === $title ) {
			$warnings[] = __( 'This Media Library item has no title, so verify that you are optimizing the correct image.', 'naveencodes-image-optimizer' );
		}

		if ( $title && $this->attachment_has_duplicate_title( $attachment_id, $title ) ) {
			$warnings[] = __( 'Another Media Library image uses the same title. Confirm that this is the intended attachment before re-running optimization.', 'naveencodes-image-optimizer' );
		}

		if ( $file_name && $this->attachment_has_duplicate_filename( $attachment_id, $file_name ) ) {
			$warnings[] = sprintf(
				/* translators: %s: file name. */
				__( 'Another Media Library image uses the file name %s. Confirm that you are optimizing the correct attachment.', 'naveencodes-image-optimizer' ),
				$file_name
			);
		}

		if ( $file_name && $this->filename_looks_suspicious( $file_name ) ) {
			$warnings[] = sprintf(
				/* translators: %s: file name. */
				__( '%s may contain unsupported or unusual filename characters. If optimization keeps failing, rename the original file and try again.', 'naveencodes-image-optimizer' ),
				$file_name
			);
		}

		return array_values( array_unique( array_filter( array_map( 'sanitize_text_field', $warnings ) ) ) );
	}

	/**
	 * Fetch image stats for the dashboard.
	 *
	 * @return array
	 */
	public function get_stats() {
		$stats = get_option( Settings::STATS_OPTION, Settings::get_default_stats() );
		$stats = wp_parse_args( is_array( $stats ) ? $stats : array(), Settings::get_default_stats() );

		$stats['average_compression'] = 0;

		if ( ! empty( $stats['total_original_bytes'] ) ) {
			$stats['average_compression'] = round( ( $stats['total_saved_bytes'] / $stats['total_original_bytes'] ) * 100, 2 );
		}

		return $stats;
	}

	/**
	 * Return a human-readable bytes label.
	 *
	 * @param int $bytes Bytes.
	 * @return string
	 */
	public function format_bytes( $bytes ) {
		return size_format( max( 0, (int) $bytes ), 2 );
	}

	/**
	 * Return stored optimization details for one attachment.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return array
	 */
	public function get_attachment_report( $attachment_id ) {
		$attachment_id = absint( $attachment_id );
		$before        = absint( get_post_meta( $attachment_id, '_naveencodes_original_bytes', true ) );
		$after         = absint( get_post_meta( $attachment_id, '_naveencodes_optimized_bytes', true ) );
		$saved         = absint( get_post_meta( $attachment_id, '_naveencodes_saved_bytes', true ) );
		$compression   = get_post_meta( $attachment_id, '_naveencodes_compression_percent', true );
		$formats       = get_post_meta( $attachment_id, '_naveencodes_generated_formats', true );

		return array(
			'is_optimized'        => (bool) get_post_meta( $attachment_id, '_naveencodes_optimized', true ),
			'before_bytes'        => $before,
			'after_bytes'         => $after,
			'saved_bytes'         => $saved,
			'compression_percent' => is_numeric( $compression ) ? (float) $compression : 0,
			'last_optimized_at'   => sanitize_text_field( (string) get_post_meta( $attachment_id, '_naveencodes_last_optimized_at', true ) ),
			'last_context'        => sanitize_text_field( (string) get_post_meta( $attachment_id, '_naveencodes_last_context', true ) ),
			'last_error'          => sanitize_text_field( (string) get_post_meta( $attachment_id, '_naveencodes_last_error', true ) ),
			'generated_formats'   => is_array( $formats ) ? array_map( 'sanitize_text_field', $formats ) : array(),
		);
	}

	/**
	 * Determine whether a MIME type is supported.
	 *
	 * @param string $mime MIME type.
	 * @return bool
	 */
	public function is_supported_mime( $mime ) {
		return in_array( (string) $mime, $this->supported_mimes, true );
	}

	/**
	 * Get all optimizable files for an attachment.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return array
	 */
	protected function get_attachment_files( $attachment_id ) {
		$files      = array();
		$main_file  = get_attached_file( $attachment_id );
		$upload_dir = wp_get_upload_dir();
		$metadata   = wp_get_attachment_metadata( $attachment_id );

		if ( $main_file && file_exists( $main_file ) ) {
			$files['original'] = wp_normalize_path( $main_file );
		}

		if ( ! empty( $metadata['file'] ) && ! empty( $metadata['sizes'] ) && ! empty( $upload_dir['basedir'] ) ) {
			$relative_dir = dirname( $metadata['file'] );
			$base_dir     = wp_normalize_path( trailingslashit( $upload_dir['basedir'] ) . ( '.' === $relative_dir ? '' : trailingslashit( $relative_dir ) ) );

			foreach ( (array) $metadata['sizes'] as $size_name => $size_data ) {
				if ( empty( $size_data['file'] ) ) {
					continue;
				}

				$path = wp_normalize_path( $base_dir . $size_data['file'] );

				if ( file_exists( $path ) ) {
					$files[ sanitize_key( $size_name ) ] = $path;
				}
			}
		}

		return $files;
	}

	/**
	 * Safely replace a file after writing to a temporary location.
	 *
	 * @param string $file_path Absolute file path.
	 * @param string $binary    Optimized binary payload.
	 * @return true|WP_Error
	 */
	protected function replace_file_safely( $file_path, $binary ) {
		$temp_file = wp_tempnam( $file_path );

		if ( ! $temp_file ) {
			return new WP_Error( 'naveencodes_temp_file', __( 'Unable to create a temporary file for replacement.', 'naveencodes-image-optimizer' ) );
		}

		if ( $this->settings->get( 'backup_original' ) ) {
			$backup_file = $file_path . '.naveencodes-backup';

			if ( ! file_exists( $backup_file ) ) {
				$this->copy_file( $file_path, $backup_file );
			}
		}

		if ( ! $this->put_file_contents( $temp_file, $binary ) ) {
			wp_delete_file( $temp_file );
			return new WP_Error( 'naveencodes_write_failed', __( 'Failed to write the optimized image.', 'naveencodes-image-optimizer' ) );
		}

		$permissions = file_exists( $file_path ) ? fileperms( $file_path ) : false;

		if ( ! $this->move_file( $temp_file, $file_path ) ) {
			$copied = $this->copy_file( $temp_file, $file_path );
			wp_delete_file( $temp_file );

			if ( ! $copied ) {
				return new WP_Error( 'naveencodes_replace_failed', __( 'Unable to replace the original image with the optimized file.', 'naveencodes-image-optimizer' ) );
			}
		}

		if ( $permissions ) {
			$this->chmod_file( $file_path, $permissions & 0777 );
		}

		clearstatcache( true, $file_path );

		return true;
	}

	/**
	 * Write WebP/AVIF sidecar files returned by the Node service.
	 *
	 * @param string $file_path     Original file path.
	 * @param array  $formats       Binary formats map.
	 * @param int    $attachment_id Attachment ID.
	 * @return void
	 */
	protected function write_sidecar_formats( $file_path, $formats, $attachment_id ) {
		$saved     = array();

		foreach ( $formats as $format => $binary ) {
			$format = sanitize_key( $format );

			if ( ! in_array( $format, array( 'webp', 'avif' ), true ) ) {
				continue;
			}

			if ( ( 'webp' === $format && ! $this->settings->get( 'enable_webp' ) ) || ( 'avif' === $format && ! $this->settings->get( 'enable_avif' ) ) ) {
				continue;
			}

			// Keep the original extension in the sidecar filename for broader rewrite compatibility.
			$target = $file_path . '.' . $format;
			$write  = $this->put_file_contents( $target, $binary );

			if ( $write ) {
				$saved[ $format ] = wp_basename( $target );
			}
		}

		if ( $attachment_id && ! empty( $saved ) ) {
			update_post_meta( $attachment_id, '_naveencodes_generated_formats', $saved );
		}
	}

	/**
	 * Update analytics after a successful optimization.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @param int $before        Original bytes.
	 * @param int $after         Optimized bytes.
	 * @return void
	 */
	protected function record_successful_optimization( $attachment_id, $before, $after, $context = 'manual', $files = 0 ) {
		$stats = $this->get_stats();

		$stats['total_optimized']++;
		$stats['total_original_bytes'] += max( 0, (int) $before );
		$stats['total_output_bytes']   += max( 0, (int) $after );
		$stats['total_saved_bytes']    += max( 0, (int) $before - (int) $after );

		update_option( Settings::STATS_OPTION, $stats, false );

		if ( $attachment_id ) {
			update_post_meta( $attachment_id, '_naveencodes_optimized', 1 );
			update_post_meta( $attachment_id, '_naveencodes_original_bytes', max( 0, (int) $before ) );
			update_post_meta( $attachment_id, '_naveencodes_optimized_bytes', max( 0, (int) $after ) );
			update_post_meta( $attachment_id, '_naveencodes_saved_bytes', max( 0, (int) $before - (int) $after ) );
			update_post_meta( $attachment_id, '_naveencodes_compression_percent', $before > 0 ? round( ( ( $before - $after ) / $before ) * 100, 2 ) : 0 );
			update_post_meta( $attachment_id, '_naveencodes_last_optimized_at', current_time( 'mysql' ) );
			update_post_meta( $attachment_id, '_naveencodes_last_context', sanitize_text_field( $context ) );
			update_post_meta( $attachment_id, '_naveencodes_optimized_file_count', absint( $files ) );
			delete_post_meta( $attachment_id, '_naveencodes_last_error' );

			do_action( 'naveencodes_image_optimized', $attachment_id );
		}
	}

	/**
	 * Increment failed stat counter.
	 *
	 * @return void
	 */
	protected function increment_failed_stat() {
		$stats = $this->get_stats();
		$stats['failed_optimizations']++;
		update_option( Settings::STATS_OPTION, $stats, false );
	}

	/**
	 * Persist the latest attachment failure reason.
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $message       Failure reason.
	 * @return void
	 */
	protected function record_attachment_failure( $attachment_id, $message ) {
		if ( ! $attachment_id ) {
			return;
		}

		update_post_meta( $attachment_id, '_naveencodes_last_error', sanitize_text_field( $message ) );
	}

	/**
	 * Convert low-level WP_Error messages into user-facing guidance.
	 *
	 * @param \WP_Error $error     Error object.
	 * @param string    $file_path Optional file path.
	 * @return string
	 */
	protected function get_friendly_optimization_error_message( $error, $file_path = '' ) {
		$code    = sanitize_key( (string) $error->get_error_code() );
		$message = strtolower( (string) $error->get_error_message() );
		$file    = $file_path ? wp_basename( $file_path ) : __( 'this image', 'naveencodes-image-optimizer' );

		switch ( $code ) {
			case 'naveencodes_invalid_filename':
				return sprintf(
					/* translators: %s: file name. */
					__( '%s has an invalid filename or could not be accepted by the optimizer.', 'naveencodes-image-optimizer' ),
					$file
				);
			case 'naveencodes_unsupported_mime':
				return sprintf(
					/* translators: %s: file name. */
					__( '%s uses an unsupported image format.', 'naveencodes-image-optimizer' ),
					$file
				);
			case 'naveencodes_file_too_large':
				return sprintf(
					/* translators: %s: file name. */
					__( '%s is too large for the current optimization limit.', 'naveencodes-image-optimizer' ),
					$file
				);
			case 'naveencodes_already_optimized':
				return sprintf(
					/* translators: %s: file name. */
					__( '%s appears to already be optimized.', 'naveencodes-image-optimizer' ),
					$file
				);
			case 'naveencodes_file_missing':
			case 'naveencodes_unreadable_file':
			case 'naveencodes_read_failed':
			case 'naveencodes_missing_files':
				return sprintf(
					/* translators: %s: file name. */
					__( '%s could not be found or read on the server.', 'naveencodes-image-optimizer' ),
					$file
				);
		}

		if ( false !== strpos( $message, 'too large' ) || false !== strpos( $message, 'file size' ) ) {
			return sprintf(
				/* translators: %s: file name. */
				__( '%s is too large for the current optimization limit.', 'naveencodes-image-optimizer' ),
				$file
			);
		}

		if ( false !== strpos( $message, 'unsupported' ) || false !== strpos( $message, 'format' ) || false !== strpos( $message, 'mime' ) ) {
			return sprintf(
				/* translators: %s: file name. */
				__( '%s uses an unsupported image format.', 'naveencodes-image-optimizer' ),
				$file
			);
		}

		if ( false !== strpos( $message, 'already optimized' ) || false !== strpos( $message, 'already processed' ) ) {
			return sprintf(
				/* translators: %s: file name. */
				__( '%s appears to already be optimized.', 'naveencodes-image-optimizer' ),
				$file
			);
		}

		return $error->get_error_message()
			? sanitize_text_field( $error->get_error_message() )
			: __( 'The image could not be optimized because of a service or file issue.', 'naveencodes-image-optimizer' );
	}

	/**
	 * Summarize one or more sub-file failures for attachment-level display.
	 *
	 * @param array $file_errors Collected file-level errors.
	 * @return string
	 */
	protected function summarize_attachment_failure( $file_errors, $precheck_messages = array() ) {
		$file_errors = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', (array) $file_errors ) ) ) );
		$precheck_messages = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', (array) $precheck_messages ) ) ) );

		if ( empty( $file_errors ) ) {
			if ( ! empty( $precheck_messages ) ) {
				return sprintf(
					/* translators: %s: warning message. */
					__( 'Optimization could not be completed. Note: %s', 'naveencodes-image-optimizer' ),
					$precheck_messages[0]
				);
			}

			return __( 'This image could not be optimized because no valid optimizable file could be processed.', 'naveencodes-image-optimizer' );
		}

		if ( ! empty( $precheck_messages ) ) {
			return sprintf(
				/* translators: 1: failure reason, 2: warning message. */
				__( '%1$s Note: %2$s', 'naveencodes-image-optimizer' ),
				$file_errors[0],
				$precheck_messages[0]
			);
		}

		return $file_errors[0];
	}

	/**
	 * Check whether another attachment uses the same exact title.
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $title         Attachment title.
	 * @return bool
	 */
	protected function attachment_has_duplicate_title( $attachment_id, $title ) {
		global $wpdb;

		$duplicate = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT ID
				FROM {$wpdb->posts}
				WHERE post_type = 'attachment'
					AND post_status = 'inherit'
					AND post_title = %s
					AND ID <> %d
				LIMIT 1",
				$title,
				$attachment_id
			)
		);

		return ! empty( $duplicate );
	}

	/**
	 * Check whether another attachment uses the same stored file basename.
	 *
	 * @param int    $attachment_id Attachment ID.
	 * @param string $file_name     Basename of the stored file.
	 * @return bool
	 */
	protected function attachment_has_duplicate_filename( $attachment_id, $file_name ) {
		global $wpdb;

		$like = '%/' . $wpdb->esc_like( $file_name );

		$duplicate = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT p.ID
				FROM {$wpdb->posts} p
				INNER JOIN {$wpdb->postmeta} pm
					ON p.ID = pm.post_id
				WHERE p.post_type = 'attachment'
					AND p.post_status = 'inherit'
					AND pm.meta_key = '_wp_attached_file'
					AND p.ID <> %d
					AND ( pm.meta_value = %s OR pm.meta_value LIKE %s )
				LIMIT 1",
				$attachment_id,
				$file_name,
				$like
			)
		);

		return ! empty( $duplicate );
	}

	/**
	 * Detect suspicious file names that may cause service-side validation issues.
	 *
	 * @param string $file_name File basename.
	 * @return bool
	 */
	protected function filename_looks_suspicious( $file_name ) {
		$sanitized = sanitize_file_name( $file_name );

		return '' === $sanitized || $sanitized !== $file_name;
	}

	/**
	 * Ensure the WordPress filesystem API is ready.
	 *
	 * @return bool
	 */
	protected function bootstrap_filesystem() {
		global $wp_filesystem;

		if ( $wp_filesystem ) {
			return true;
		}

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

		return WP_Filesystem();
	}

	/**
	 * Copy one file.
	 *
	 * @param string $source Source path.
	 * @param string $target Destination path.
	 * @return bool
	 */
	protected function copy_file( $source, $target ) {
		global $wp_filesystem;

		if ( $this->bootstrap_filesystem() && $wp_filesystem ) {
			return (bool) $wp_filesystem->copy( $source, $target, true );
		}

		return copy( $source, $target ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_copy
	}

	/**
	 * Move one file.
	 *
	 * @param string $source Source path.
	 * @param string $target Destination path.
	 * @return bool
	 */
	protected function move_file( $source, $target ) {
		global $wp_filesystem;

		if ( $this->bootstrap_filesystem() && $wp_filesystem ) {
			return (bool) $wp_filesystem->move( $source, $target, true );
		}

		return rename( $source, $target ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
	}

	/**
	 * Write file contents.
	 *
	 * @param string $file_path Destination path.
	 * @param string $binary    Binary contents.
	 * @return bool
	 */
	protected function put_file_contents( $file_path, $binary ) {
		global $wp_filesystem;

		if ( $this->bootstrap_filesystem() && $wp_filesystem ) {
			return (bool) $wp_filesystem->put_contents( $file_path, $binary, FS_CHMOD_FILE );
		}

		return false !== file_put_contents( $file_path, $binary, LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
	}

	/**
	 * Update file permissions.
	 *
	 * @param string $file_path File path.
	 * @param int    $mode      Permission mask.
	 * @return bool
	 */
	protected function chmod_file( $file_path, $mode ) {
		global $wp_filesystem;

		if ( $this->bootstrap_filesystem() && $wp_filesystem ) {
			return (bool) $wp_filesystem->chmod( $file_path, $mode );
		}

		return chmod( $file_path, $mode ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
	}
}
