<?php
/**
 * WebP Conversion Module
 *
 * @package WebpConversion
 */

namespace WebpConversion;

use WP_PowerSuite\Core\AbstractModule;

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

class Module extends AbstractModule {

	private const MODULE_ID = 'webp-conversion';
	private const MODULE_VERSION = '1.0.0';

	private const CONVERTIBLE_EXTENSIONS = [ 'jpg', 'jpeg', 'png' ];
	private const DEFAULT_QUALITY = 80;
	private const MIN_QUALITY = 60;
	private const MAX_QUALITY = 100;
	private const DEFAULT_EXCLUSION_SUFFIX = '-wpps';

	private const RESTORABLE_COUNT_TRANSIENT = 'wp_powersuite_webp_restorable_count_v1';

	public const RESTORE_NONCE_ACTION = 'wp_powersuite_webp_restore';

	private const MAX_SCAN = 5000;

	private static bool $suppress_conversion_during_metadata_regeneration = false;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'WebP Conversion', 'wppowersuite-lite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Automatically convert uploaded JPEG and PNG images to WebP format for significantly smaller file sizes and faster page loads. Cannot be enabled together with AVIF Conversion.', 'wppowersuite-lite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'rest', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		if ( is_admin() ) {
			add_action( 'admin_notices', [ $this, 'maybeAdminNotice' ] );
			add_action( 'wp_ajax_wp_powersuite_webp_restore_batch', [ $this, 'ajaxRestoreBatch' ] );
		}

		$settings = $this->getSettings();
		if ( ! $this->needsConversionHooks( $settings ) ) {
			return;
		}

		$webp_priority = (int) apply_filters( 'wp_powersuite_webp_metadata_priority', 9 );
		$webp_priority = max( 0, min( 999, $webp_priority ) );
		add_filter( 'wp_generate_attachment_metadata', [ $this, 'convertToWebp' ], $webp_priority, 2 );
		add_filter( 'upload_mimes', [ $this, 'ensureWebpMime' ] );
	}

	/**
	 * @param array<string, mixed> $settings
	 */
	private function needsConversionHooks( array $settings ): bool {
		if ( empty( $settings['convert_jpeg'] ) && empty( $settings['convert_png'] ) ) {
			return false;
		}

		if ( ! $this->serverSupportsWebp() ) {
			return false;
		}

		if ( wp_doing_ajax() ) {
			return true;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return true;
		}

		return is_admin();
	}

	/**
	 * @param array<string, string> $mimes
	 * @return array<string, string>
	 */
	public function ensureWebpMime( array $mimes ): array {
		if ( ! isset( $mimes['webp'] ) ) {
			$mimes['webp'] = 'image/webp';
		}
		return $mimes;
	}

	public function serverSupportsWebp(): bool {
		if ( function_exists( 'imagewebp' ) ) {
			return true;
		}
		if ( class_exists( 'Imagick' ) ) {
			$formats = \Imagick::queryFormats();
			return is_array( $formats ) && in_array( 'WEBP', $formats, true );
		}
		return false;
	}

	public function getServerCapabilityMessage(): string {
		if ( function_exists( 'imagewebp' ) ) {
			return __( 'GD Library with WebP support (imagewebp) is available.', 'wppowersuite-lite' );
		}
		if ( class_exists( 'Imagick' ) ) {
			$formats = \Imagick::queryFormats();
			if ( is_array( $formats ) && in_array( 'WEBP', $formats, true ) ) {
				return __( 'ImageMagick with WebP support is available.', 'wppowersuite-lite' );
			}
			return __( 'ImageMagick is available but WebP format is not supported.', 'wppowersuite-lite' );
		}
		return __( 'WebP is not supported. GD (imagewebp) or ImageMagick with WebP is required.', 'wppowersuite-lite' );
	}

	public function maybeAdminNotice(): void {
		if ( ! is_admin() || ! \WP_PowerSuite\Core\NetworkScope::canManagePlugin() ) {
			return;
		}

		if ( ! \WP_PowerSuite\Core\AdminMenu::isPowerSuiteAdminScreen() ) {
			$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
			$on_media = $screen instanceof \WP_Screen && in_array( $screen->id, [ 'upload', 'media' ], true );
			if ( ! $on_media ) {
				return;
			}
		}

		if ( $this->serverSupportsWebp() ) {
			return;
		}

		echo '<div class="notice notice-warning is-dismissible"><p>';
		echo esc_html(
			sprintf(
				/* translators: %s: module name */
				__( '%s is on, but this server cannot create WebP files. New uploads will not be converted until WebP support is enabled—or turn this module off.', 'wppowersuite-lite' ),
				$this->getName()
			)
		);
		echo '</p></div>';
	}

	/**
	 * @param array<string, mixed> $metadata
	 * @return array<string, mixed>
	 */
	public function convertToWebp( array $metadata, int $attachment_id ): array {
		if ( ! $this->isEnabled() ) {
			return $metadata;
		}

		if ( self::$suppress_conversion_during_metadata_regeneration ) {
			return $metadata;
		}

		if ( apply_filters( 'wp_powersuite_skip_webp_conversion', false, $attachment_id, $metadata ) ) {
			return $metadata;
		}

		$file = get_attached_file( $attachment_id, true );
		if ( ! is_string( $file ) || ! $this->isAllowedUploadPath( $file ) || ! is_file( $file ) || ! is_readable( $file ) ) {
			return $metadata;
		}

		$path_info = pathinfo( $file );
		$ext       = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : '';
		if ( ! in_array( $ext, self::CONVERTIBLE_EXTENSIONS, true ) ) {
			return $metadata;
		}

		$settings = $this->getSettings();
		if ( ( $ext === 'jpg' || $ext === 'jpeg' ) && empty( $settings['convert_jpeg'] ) ) {
			return $metadata;
		}
		if ( $ext === 'png' && empty( $settings['convert_png'] ) ) {
			return $metadata;
		}

		$basename_no_ext = $path_info['filename'] ?? '';
		if ( $basename_no_ext === '' ) {
			return $metadata;
		}

		$suffix = $settings['exclusion_suffix'];
		if ( $suffix !== '' && substr( $basename_no_ext, - strlen( $suffix ) ) === $suffix ) {
			return $metadata;
		}

		$quality = max( self::MIN_QUALITY, min( self::MAX_QUALITY, (int) $settings['quality'] ) );

		$max_pixels = (int) apply_filters( 'wp_powersuite_webp_max_pixels', 8000 * 8000 );
		if ( $max_pixels > 0 ) {
			$dims = @getimagesize( $file );
			if ( is_array( $dims ) && isset( $dims[0], $dims[1] ) && ( (int) $dims[0] * (int) $dims[1] ) > $max_pixels ) {
				return $metadata;
			}
		}

		$webp_path = ( $path_info['dirname'] ?? '' ) . DIRECTORY_SEPARATOR . $basename_no_ext . '.webp';
		if ( $webp_path === '' || ! $this->isAllowedUploadPath( $webp_path ) ) {
			return $metadata;
		}

		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'image' );
		}

		if ( ! $this->convertFileToWebp( $file, $webp_path, $quality, $ext ) ) {
			return $metadata;
		}

		$out_bytes = @filesize( $webp_path );
		if ( ! is_file( $webp_path ) || $out_bytes === false || $out_bytes <= 0 ) {
			if ( is_file( $webp_path ) ) {
				wp_delete_file( $webp_path );
			}
			return $metadata;
		}

		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) {
			wp_delete_file( $webp_path );
			return $metadata;
		}

		$base_dir = wp_normalize_path( $upload_dir['basedir'] );
		$new_rel  = ltrim( str_replace( $base_dir, '', wp_normalize_path( $webp_path ) ), '/' );
		$new_rel  = str_replace( '\\', '/', $new_rel );

		$metadata['file'] = $new_rel;
		update_post_meta( $attachment_id, '_wp_attached_file', $new_rel );

		$new_url = $upload_dir['baseurl'] . '/' . $new_rel;
		wp_update_post(
			[
				'ID'             => $attachment_id,
				'guid'           => $new_url,
				'post_mime_type' => 'image/webp',
			]
		);

		if ( $settings['original_files'] === 'delete' ) {
			wp_delete_file( $file );
		} else {
			$this->moveOriginalToOriginalsFolder( $file, $path_info );
		}

		$this->invalidateRestorableCache();

		return $metadata;
	}

	/**
	 * @param array<string, string> $path_info
	 */
	private function moveOriginalToOriginalsFolder( string $file, array $path_info ): void {
		$dir      = $path_info['dirname'] ?? '';
		$basename = $path_info['basename'] ?? '';
		if ( $dir === '' || $basename === '' || ! is_file( $file ) ) {
			return;
		}

		$originals_dir = $dir . DIRECTORY_SEPARATOR . 'originals';
		if ( ! wp_mkdir_p( $originals_dir ) ) {
			return;
		}

		$dest = $originals_dir . DIRECTORY_SEPARATOR . $basename;
		if ( $file === $dest ) {
			return;
		}

		if ( @rename( $file, $dest ) ) {
			return;
		}
		if ( @copy( $file, $dest ) ) {
			wp_delete_file( $file );
		}
	}

	private function convertFileToWebp( string $source, string $dest, int $quality, string $ext ): bool {
		if ( function_exists( 'imagewebp' ) ) {
			return $this->convertWithGd( $source, $dest, $quality, $ext );
		}
		if ( class_exists( 'Imagick' ) ) {
			return $this->convertWithImagick( $source, $dest, $quality );
		}
		return false;
	}

	private function convertWithGd( string $source, string $dest, int $quality, string $ext ): bool {
		$image = false;
		if ( $ext === 'png' ) {
			$image = @imagecreatefrompng( $source );
			if ( $image !== false ) {
				imagepalettetotruecolor( $image );
				imagealphablending( $image, true );
				imagesavealpha( $image, true );
			}
		} elseif ( $ext === 'jpg' || $ext === 'jpeg' ) {
			$image = @imagecreatefromjpeg( $source );
		}

		if ( $image === false ) {
			return false;
		}

		$result = imagewebp( $image, $dest, $quality );
		imagedestroy( $image );
		return $result;
	}

	private function convertWithImagick( string $source, string $dest, int $quality ): bool {
		try {
			$imagick = new \Imagick( $source );
			$imagick->setImageFormat( 'webp' );
			$imagick->setImageCompressionQuality( $quality );
			$result = $imagick->writeImage( $dest );
			$imagick->clear();
			$imagick->destroy();
			return $result;
		} catch ( \Exception $e ) {
			return false;
		}
	}

	public function countRestorableAttachments(): int {
		$cached = get_transient( self::RESTORABLE_COUNT_TRANSIENT );
		if ( false !== $cached ) {
			return max( 0, (int) $cached );
		}

		$n = 0;
		foreach ( $this->getCandidateRestoreAttachmentIds() as $id ) {
			if ( $this->attachmentIsRestorable( (int) $id ) ) {
				++$n;
			}
		}

		set_transient( self::RESTORABLE_COUNT_TRANSIENT, $n, 15 * MINUTE_IN_SECONDS );
		return $n;
	}

	/**
	 * @return int[]
	 */
	private function getCandidateRestoreAttachmentIds(): array {
		$ids = get_posts(
			[
				'post_type'              => 'attachment',
				'post_status'            => 'inherit',
				'post_mime_type'         => 'image/webp',
				'fields'                 => 'ids',
				'posts_per_page'         => self::MAX_SCAN,
				'orderby'                => 'ID',
				'order'                  => 'ASC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			]
		);
		return is_array( $ids ) ? array_map( 'intval', $ids ) : [];
	}

	public function attachmentIsRestorable( int $attachment_id ): bool {
		if ( $attachment_id <= 0 || get_post_type( $attachment_id ) !== 'attachment' ) {
			return false;
		}
		if ( get_post_mime_type( $attachment_id ) !== 'image/webp' ) {
			return false;
		}
		return $this->findOriginalBackupPath( $attachment_id ) !== '';
	}

	private function findOriginalBackupPath( int $attachment_id ): string {
		$main = get_attached_file( $attachment_id, true );
		if ( ! is_string( $main ) || ! $this->isAllowedUploadPath( $main ) ) {
			return '';
		}
		$stem = pathinfo( $main, PATHINFO_FILENAME );
		$dir  = dirname( $main );
		foreach ( [ 'jpg', 'jpeg', 'png' ] as $e ) {
			$p = $dir . DIRECTORY_SEPARATOR . 'originals' . DIRECTORY_SEPARATOR . $stem . '.' . $e;
			if ( $this->isAllowedUploadPath( $p ) && is_file( $p ) ) {
				return $p;
			}
		}
		return '';
	}

	public function restoreAttachment( int $attachment_id ): bool {
		if ( ! $this->attachmentIsRestorable( $attachment_id ) ) {
			return false;
		}

		$backup = $this->findOriginalBackupPath( $attachment_id );
		if ( $backup === '' ) {
			return false;
		}

		$main_webp = get_attached_file( $attachment_id, true );
		if ( ! is_string( $main_webp ) || ! $this->isAllowedUploadPath( $main_webp ) || ! is_file( $main_webp ) ) {
			return false;
		}

		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) {
			return false;
		}

		$target = dirname( $main_webp ) . DIRECTORY_SEPARATOR . basename( $backup );
		if ( ! $this->isAllowedUploadPath( $target ) ) {
			return false;
		}

		if ( is_file( $target ) && wp_normalize_path( $target ) !== wp_normalize_path( $backup ) ) {
			return false;
		}

		wp_delete_file( $main_webp );

		if ( ! @rename( $backup, $target ) ) {
			if ( ! @copy( $backup, $target ) ) {
				return false;
			}
			wp_delete_file( $backup );
		}

		$new_rel = ltrim( str_replace( wp_normalize_path( $upload_dir['basedir'] ), '', wp_normalize_path( $target ) ), '/' );
		$new_rel = str_replace( '\\', '/', $new_rel );

		update_post_meta( $attachment_id, '_wp_attached_file', $new_rel );

		$mime = str_ends_with( strtolower( $target ), '.png' ) ? 'image/png' : 'image/jpeg';

		$new_url = $upload_dir['baseurl'] . '/' . $new_rel;
		wp_update_post(
			[
				'ID'             => $attachment_id,
				'guid'           => $new_url,
				'post_mime_type' => $mime,
			]
		);

		if ( function_exists( 'wp_generate_attachment_metadata' ) ) {
			self::$suppress_conversion_during_metadata_regeneration = true;
			try {
				wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $target ) );
			} finally {
				self::$suppress_conversion_during_metadata_regeneration = false;
			}
		}

		return true;
	}

	private function findNextRestorableAttachmentId( int $cursor ): int {
		global $wpdb;

		$search_cursor = $cursor;
		$attempts      = 0;
		while ( $attempts < 200 ) {
			$id = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT ID FROM {$wpdb->posts}
					WHERE post_type = 'attachment'
					AND post_status = 'inherit'
					AND post_mime_type = 'image/webp'
					AND ID > %d
					ORDER BY ID ASC
					LIMIT 1",
					$search_cursor
				)
			);
			if ( $id <= 0 ) {
				return 0;
			}
			if ( $this->attachmentIsRestorable( $id ) ) {
				return $id;
			}
			$search_cursor = $id;
			++$attempts;
		}

		return 0;
	}

	public function ajaxRestoreBatch(): void {
		if ( ! check_ajax_referer( self::RESTORE_NONCE_ACTION, 'nonce', false ) ) {
			wp_send_json_error(
				[
					'message' => __( 'Security check failed.', 'wppowersuite-lite' ),
					'code'    => 'bad_nonce',
				],
				403
			);
		}

		if ( ! \WP_PowerSuite\Core\NetworkScope::canManagePlugin() ) {
			wp_send_json_error(
				[
					'message' => __( 'You do not have permission to restore attachments.', 'wppowersuite-lite' ),
					'code'    => 'forbidden',
				],
				403
			);
		}

		if ( ! $this->isEnabled() ) {
			wp_send_json_error(
				[
					'message' => __( 'WebP Conversion is disabled.', 'wppowersuite-lite' ),
					'code'    => 'module_disabled',
				]
			);
		}

		$cursor = isset( $_POST['cursor'] ) ? max( 0, (int) wp_unslash( $_POST['cursor'] ) ) : 0;
		if ( $cursor <= 0 && isset( $_POST['offset'] ) ) {
			$cursor = max( 0, (int) wp_unslash( $_POST['offset'] ) );
		}
		$batch = isset( $_POST['batch'] ) ? max( 1, min( 40, (int) wp_unslash( $_POST['batch'] ) ) ) : 12;

		$restored       = 0;
		$processed      = 0;
		$search_cursor  = $cursor;
		$last_processed = $cursor;

		while ( $processed < $batch ) {
			$aid = $this->findNextRestorableAttachmentId( $search_cursor );
			if ( $aid <= 0 ) {
				break;
			}

			do_action( 'wp_powersuite_webp_before_restore_attachment', $aid );

			if ( $this->restoreAttachment( $aid ) ) {
				++$restored;
			}

			++$processed;
			$search_cursor  = $aid;
			$last_processed = $aid;
		}

		$done = $processed < $batch;
		$this->invalidateRestorableCache();

		wp_send_json_success(
			[
				'done'        => $done,
				'total'       => $this->countRestorableAttachments(),
				'processed'   => $processed,
				'restored'    => $restored,
				'next_cursor' => $last_processed,
			]
		);
	}

	private function invalidateRestorableCache(): void {
		delete_transient( self::RESTORABLE_COUNT_TRANSIENT );
	}

	private function isAllowedUploadPath( string $file ): bool {
		$upload = wp_upload_dir();
		if ( ! empty( $upload['error'] ) ) {
			return false;
		}
		$base = wp_normalize_path( $upload['basedir'] );
		$norm = wp_normalize_path( $file );
		if ( $base === '' || $norm === '' ) {
			return false;
		}
		return str_starts_with( $norm, $base . '/' ) || $norm === $base;
	}

	/**
	 * @return array<string, mixed>
	 */
	private function getSettings(): array {
		$raw = $this->moduleSettings()->all( [] );
		if ( ! is_array( $raw ) ) {
			$raw = [];
		}
		return [
			'convert_jpeg'     => ! array_key_exists( 'convert_jpeg', $raw ) || ! empty( $raw['convert_jpeg'] ),
			'convert_png'      => ! array_key_exists( 'convert_png', $raw ) || ! empty( $raw['convert_png'] ),
			'quality'          => isset( $raw['quality'] ) ? (int) $raw['quality'] : self::DEFAULT_QUALITY,
			'original_files'   => ( $raw['original_files'] ?? 'keep' ) === 'delete' ? 'delete' : 'keep',
			'exclusion_suffix' => substr(
				sanitize_text_field(
					is_string( $raw['exclusion_suffix'] ?? null ) ? $raw['exclusion_suffix'] : self::DEFAULT_EXCLUSION_SUFFIX
				),
				0,
				40
			),
		];
	}
}
