<?php

defined( 'ABSPATH' ) || exit;

/**
 * Image Optimizer module.
 *
 * Processes image uploads on the way in so the original 8-10MB Gemini-style
 * file never lands on disk. Uses the `wp_handle_upload_prefilter` filter to
 * mutate the temp file before WordPress saves it — only the optimized version
 * is stored.
 *
 * Phase 1 scope (this commit):
 *   - Resize to a target box (fit-to-bounds or cover-then-crop with anchor)
 *   - JPEG compression at configurable quality
 *   - Edge crops (L/R/T/B as %)
 *   - PNG transparency preservation (real alpha → keep PNG; decorative alpha → flatten to JPEG)
 *   - Filename `_bw` suffix
 *   - Skip conditions: non-images, SVGs, already-small JPEGs, user-mode off
 *   - Per-user admin-bar widget with Active / Resize-only / Off
 *   - Lifetime stats counter (images processed, bytes saved)
 *   - Imagick-first with WP image-editor fallback (no watermark removal on fallback)
 *
 * Phase 2 (next commit): watermark removal — bottom-right rectangle filled
 * with a sampled color, optional bilinear-resized noise, gradient feather.
 *
 * Settings shape (under `bw_dev_settings[image_optimizer]`):
 *   - enabled            bool   master "process uploads" toggle, separate from
 *                                 the Modules-tab enable. Default OFF so an
 *                                 update to bw-dev doesn't surprise-mutate
 *                                 uploads on existing client sites.
 *   - active_profile     string slug of the active profile (only "web_1920"
 *                                 in v1; multi-profile is a v1.2 feature)
 *   - profiles           array  profile_slug => profile_config
 *   - stats              array  ['images_processed' => int, 'bytes_saved' => int]
 *
 * Per-user state:
 *   - user meta `_bw_dev_image_optimizer_mode` ∈ { active | resize_only | off }.
 *     Default = active. Settable via admin-bar widget.
 *
 * @package BW_Dev
 */

class BW_Dev_Module_Image_Optimizer implements BW_Dev_Module_Interface {

	const META_USER_PROFILE = '_bw_dev_image_optimizer_profile';
	const META_USER_MODE    = '_bw_dev_image_optimizer_mode'; // legacy pre-multi-profile, migrated on read
	const MODE_OFF          = 'off';

	const SET_MODE_ACTION = 'bw_dev_image_optimizer_set_mode';
	const NONCE_SET_MODE  = 'bw_dev_image_optimizer_set_mode';

	const DEFAULT_PROFILE_SLUG = 'web_1920';

	// Reserved sentinel — never use as a real profile slug.
	const RESERVED_SLUGS = array( 'off', '__deleted', '__new' );

	const SKIP_IF_BELOW_BYTES = 524288; // 512 KB

	const SUPPORTED_EXTS = array( 'jpg', 'jpeg', 'png', 'webp' );

	const PREVIEW_ACTION = 'bw_dev_image_optimizer_preview';
	const NONCE_PREVIEW  = 'bw_dev_image_optimizer_preview';

	// Cap preview output to this max width so the AJAX round-trip stays fast.
	const PREVIEW_MAX_WIDTH = 900;

	public function slug(): string {
		return 'image_optimizer';
	}

	public function label(): string {
		return __( 'Image Optimizer', 'bw-dev' );
	}

	public function group(): string {
		return 'editor_admin';
	}

	public function default_settings(): array {
		return array(
			'enabled'               => false,
			'default_profile'       => self::DEFAULT_PROFILE_SLUG,
			'preview_attachment_id' => 0,
			'profiles'              => array(
				self::DEFAULT_PROFILE_SLUG => $this->default_profile(),
			),
			'stats'                 => array(
				'images_processed' => 0,
				'bytes_saved'      => 0,
			),
		);
	}

	private function default_profile(): array {
		return array(
			'label'      => 'Web 1920',
			'max_width'  => 1920,
			'max_height' => 1080,
			'mode'       => 'fit',
			'anchor'     => 'center',
			'quality'    => 82,
			'crop'       => array(
				'left'   => 0,
				'right'  => 0,
				'top'    => 0,
				'bottom' => 0,
			),
			// Watermark structure is stored from v1 so Phase 2 can drop in
			// without a settings migration. Phase 1 ignores it.
			'watermark'  => array(
				'enabled' => false,
				'w_pct'   => 11,
				'h_pct'   => 11,
				'sample'  => 'outside',
				'noise'   => 0,
			),
		);
	}

	public function sanitize( array $data ): array {
		$defaults = $this->default_settings();
		$out      = $defaults;

		$out['enabled']               = ! empty( $data['enabled'] );
		$out['preview_attachment_id'] = isset( $data['preview_attachment_id'] ) ? absint( $data['preview_attachment_id'] ) : 0;

		// Multi-profile: each profile arrives keyed by its current slug. New
		// profiles submitted with a temporary `_new_*` slug get re-slugged
		// from their label after sanitization. Slugs listed in
		// `__deleted_profiles[]` are skipped.
		$deleted = isset( $data['__deleted_profiles'] ) && is_array( $data['__deleted_profiles'] )
			? array_map( 'sanitize_key', $data['__deleted_profiles'] )
			: array();

		$profiles_in = isset( $data['profiles'] ) && is_array( $data['profiles'] ) ? $data['profiles'] : array();
		$cleaned     = array();
		$used_slugs  = array();

		foreach ( $profiles_in as $submitted_slug => $profile_in ) {
			$submitted_slug = sanitize_key( (string) $submitted_slug );
			if ( in_array( $submitted_slug, $deleted, true ) ) {
				continue;
			}
			if ( ! is_array( $profile_in ) ) {
				continue;
			}
			$profile      = $this->sanitize_profile_payload( $profile_in );
			$is_new       = ( 0 === strpos( $submitted_slug, '_new_' ) ) || in_array( $submitted_slug, self::RESERVED_SLUGS, true );
			$target_slug  = $is_new ? $this->slugify_label( $profile['label'], $used_slugs ) : $submitted_slug;
			if ( in_array( $target_slug, self::RESERVED_SLUGS, true ) || '' === $target_slug ) {
				$target_slug = $this->slugify_label( $profile['label'], $used_slugs );
			}
			// Collision: another already-cleaned profile claimed this slug.
			if ( isset( $cleaned[ $target_slug ] ) && $submitted_slug !== $target_slug ) {
				$target_slug = $this->dedupe_slug( $target_slug, $used_slugs );
			}
			$cleaned[ $target_slug ] = $profile;
			$used_slugs[]            = $target_slug;
		}

		// Always-at-least-one invariant: if every profile was deleted (or
		// nothing was submitted), restore the seed profile.
		if ( empty( $cleaned ) ) {
			$cleaned[ self::DEFAULT_PROFILE_SLUG ] = $this->default_profile();
		}

		$out['profiles'] = $cleaned;

		// Default profile pointer: must reference an existing slug.
		$desired_default = isset( $data['default_profile'] ) ? sanitize_key( (string) $data['default_profile'] ) : '';
		if ( '' !== $desired_default && isset( $cleaned[ $desired_default ] ) ) {
			$out['default_profile'] = $desired_default;
		} else {
			// Stash a label→slug map so a "_new_*" default reference can be
			// resolved post-reslug.
			$label_to_slug = array();
			foreach ( $cleaned as $slug => $p ) {
				$label_to_slug[ sanitize_key( $p['label'] ) ] = $slug;
			}
			$resolved = isset( $label_to_slug[ sanitize_key( $desired_default ) ] ) ? $label_to_slug[ sanitize_key( $desired_default ) ] : null;
			$out['default_profile'] = $resolved ? $resolved : array_keys( $cleaned )[0];
		}

		// Preserve stats unless reset was requested.
		$current_stats = bw_dev()->settings()->get( $this->slug(), 'stats', null );
		if ( is_array( $current_stats ) ) {
			$out['stats']['images_processed'] = absint( $current_stats['images_processed'] ?? 0 );
			$out['stats']['bytes_saved']      = max( 0, (int) ( $current_stats['bytes_saved'] ?? 0 ) );
		}
		if ( ! empty( $data['reset_stats'] ) ) {
			$out['stats'] = array( 'images_processed' => 0, 'bytes_saved' => 0 );
		}

		return $out;
	}

	private function sanitize_profile_payload( array $profile_in ): array {
		$profile = $this->default_profile();

		if ( isset( $profile_in['label'] ) ) {
			$label = sanitize_text_field( wp_unslash( (string) $profile_in['label'] ) );
			if ( '' !== $label ) {
				$profile['label'] = $label;
			}
		}

		$profile['max_width']  = max( 1, min( 8000, absint( $profile_in['max_width']  ?? $profile['max_width'] ) ) );
		$profile['max_height'] = max( 1, min( 8000, absint( $profile_in['max_height'] ?? $profile['max_height'] ) ) );
		$profile['mode']       = ( ( $profile_in['mode'] ?? '' ) === 'cover' ) ? 'cover' : 'fit';
		$profile['anchor']     = $this->sanitize_anchor( $profile_in['anchor'] ?? 'center' );
		$profile['quality']    = max( 10, min( 100, absint( $profile_in['quality'] ?? 82 ) ) );

		foreach ( array( 'left', 'right', 'top', 'bottom' ) as $side ) {
			$raw                       = isset( $profile_in['crop'][ $side ] ) ? $profile_in['crop'][ $side ] : 0;
			$profile['crop'][ $side ]  = max( 0, min( 50, absint( $raw ) ) );
		}

		$wm_in = isset( $profile_in['watermark'] ) && is_array( $profile_in['watermark'] ) ? $profile_in['watermark'] : array();
		$profile['watermark']['enabled'] = ! empty( $wm_in['enabled'] );
		$profile['watermark']['w_pct']   = max( 1, min( 50, absint( $wm_in['w_pct'] ?? 11 ) ) );
		$profile['watermark']['h_pct']   = max( 1, min( 50, absint( $wm_in['h_pct'] ?? 11 ) ) );
		$profile['watermark']['sample']  = ( ( $wm_in['sample'] ?? 'outside' ) === 'inside' ) ? 'inside' : 'outside';
		$profile['watermark']['noise']   = max( 0, min( 100, absint( $wm_in['noise'] ?? 0 ) ) );

		return $profile;
	}

	private function slugify_label( string $label, array $used_slugs ): string {
		$base = sanitize_key( $label );
		if ( '' === $base ) {
			$base = 'profile';
		}
		// Avoid slugs that conflict with reserved sentinels.
		if ( in_array( $base, self::RESERVED_SLUGS, true ) ) {
			$base .= '_p';
		}
		return $this->dedupe_slug( $base, $used_slugs );
	}

	private function dedupe_slug( string $base, array $used_slugs ): string {
		if ( ! in_array( $base, $used_slugs, true ) ) {
			return $base;
		}
		$i = 2;
		while ( in_array( $base . '_' . $i, $used_slugs, true ) ) {
			$i++;
		}
		return $base . '_' . $i;
	}

	private function sanitize_anchor( $raw ): string {
		$valid = array(
			'top-left', 'top', 'top-right',
			'left',     'center', 'right',
			'bottom-left', 'bottom', 'bottom-right',
		);
		$raw = is_string( $raw ) ? sanitize_key( str_replace( '_', '-', $raw ) ) : 'center';
		return in_array( $raw, $valid, true ) ? $raw : 'center';
	}

	public function register(): void {
		// Both filters share the same $file structure. Hooking both covers:
		//   - upload_prefilter:   wp-admin uploader, block editor, classic editor
		//   - sideload_prefilter: wp-cli, programmatic imports, remote-URL imports,
		//                         some block editor "insert from URL" flows
		add_filter( 'wp_handle_upload_prefilter',   array( $this, 'process_upload' ), 10, 1 );
		add_filter( 'wp_handle_sideload_prefilter', array( $this, 'process_upload' ), 10, 1 );

		add_action( 'admin_bar_menu', array( $this, 'admin_bar_widget' ), 100 );
		add_action( 'admin_post_' . self::SET_MODE_ACTION, array( $this, 'handle_set_mode' ) );

		add_action( 'wp_ajax_' . self::PREVIEW_ACTION, array( $this, 'handle_preview_ajax' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_settings_assets' ) );
	}

	public function uninstall(): void {
		global $wpdb;
		// Drop per-user profile/mode meta on plugin uninstall.
		$wpdb->delete( $wpdb->usermeta, array( 'meta_key' => self::META_USER_PROFILE ) ); // phpcs:ignore WordPress.DB.SlowDBQuery
		$wpdb->delete( $wpdb->usermeta, array( 'meta_key' => self::META_USER_MODE ) );    // phpcs:ignore WordPress.DB.SlowDBQuery
	}

	// -------------------------------------------------------------------
	// Upload pipeline
	// -------------------------------------------------------------------

	/**
	 * @param array $file ['name','type','tmp_name','error','size']
	 */
	public function process_upload( $file ) {
		if ( ! is_array( $file ) ) {
			return $file;
		}
		if ( ! empty( $file['error'] ) ) {
			return $file;
		}

		// Feature globally enabled?
		if ( ! $this->is_feature_enabled() ) {
			return $file;
		}

		// User opt-out? Either explicit "off" or no profiles configured.
		$user_choice = $this->get_user_choice();
		if ( self::MODE_OFF === $user_choice ) {
			return $file;
		}

		// Supported extension?
		$ext = strtolower( pathinfo( (string) $file['name'], PATHINFO_EXTENSION ) );
		if ( ! in_array( $ext, self::SUPPORTED_EXTS, true ) ) {
			return $file;
		}

		// Resolve which profile applies to this upload.
		$profile = $this->get_profile_for_user( $user_choice );
		if ( ! is_array( $profile ) ) {
			return $file;
		}

		// Read source bytes.
		$tmp_name      = (string) $file['tmp_name'];
		if ( ! is_readable( $tmp_name ) ) {
			return $file;
		}
		$source_bytes  = file_get_contents( $tmp_name ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		if ( false === $source_bytes || '' === $source_bytes ) {
			return $file;
		}
		$source_size = strlen( $source_bytes );

		// Already-small heuristic: small JPEG that's also below profile dims → skip.
		if ( in_array( $ext, array( 'jpg', 'jpeg' ), true ) && $source_size < self::SKIP_IF_BELOW_BYTES ) {
			$dims = $this->probe_dimensions( $tmp_name );
			if ( $dims && $dims[0] <= (int) $profile['max_width'] && $dims[1] <= (int) $profile['max_height'] ) {
				return $file;
			}
		}

		// Each profile carries its own watermark-enabled flag. The Off case
		// short-circuited above; anything else means "apply the picked profile
		// in full", including its watermark sub-setting.
		$apply_watermark = ! empty( $profile['watermark']['enabled'] );

		$result = $this->run_pipeline( $source_bytes, $profile, $apply_watermark );
		if ( null === $result ) {
			// Pipeline failure — pass through untouched.
			return $file;
		}

		list( $new_bytes, $new_mime, $new_ext ) = $result;
		$new_size                               = strlen( $new_bytes );

		// Only replace if we actually got smaller.
		if ( $new_size >= $source_size ) {
			return $file;
		}

		$put = file_put_contents( $tmp_name, $new_bytes ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents
		if ( false === $put ) {
			return $file;
		}

		$file['name'] = $this->bw_suffix_name( (string) $file['name'], $new_ext );
		$file['type'] = $new_mime;
		$file['size'] = $new_size;

		$this->record_stats( $source_size - $new_size );

		return $file;
	}

	private function probe_dimensions( string $path ): ?array {
		$info = @getimagesize( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		if ( ! is_array( $info ) || ! isset( $info[0], $info[1] ) ) {
			return null;
		}
		return array( (int) $info[0], (int) $info[1] );
	}

	private function bw_suffix_name( string $original_name, string $new_ext ): string {
		$info = pathinfo( $original_name );
		$base = isset( $info['filename'] ) ? (string) $info['filename'] : 'image';
		if ( '_bw' !== substr( $base, -3 ) ) {
			$base .= '_bw';
		}
		// sanitize_file_name is safer than trusting the raw base
		$out = sanitize_file_name( $base . '.' . $new_ext );
		return '' === $out ? 'image_bw.' . $new_ext : $out;
	}

	// -------------------------------------------------------------------
	// Imagick pipeline (with WP fallback)
	// -------------------------------------------------------------------

	/**
	 * Returns [bytes, mime, ext] on success, null on failure.
	 */
	private function run_pipeline( string $bytes, array $profile, bool $apply_watermark ): ?array {
		if ( class_exists( 'Imagick' ) ) {
			return $this->run_pipeline_imagick( $bytes, $profile, $apply_watermark );
		}
		return $this->run_pipeline_fallback( $bytes, $profile );
	}

	private function run_pipeline_imagick( string $bytes, array $profile, bool $apply_watermark ): ?array {
		try {
			$im = new Imagick();
			$im->readImageBlob( $bytes );

			// Honor EXIF orientation, then strip the tag so the output is upright.
			$this->auto_orient( $im );

			$keep_png = $this->has_real_transparency( $im );

			// Watermark removal BEFORE crop/resize so the rectangle sampling
			// is relative to the source dimensions the user configured against.
			if ( $apply_watermark && ! empty( $profile['watermark']['enabled'] ) ) {
				$this->apply_watermark_removal( $im, (array) $profile['watermark'] );
			}

			$this->apply_edge_crops( $im, $profile['crop'] );
			$this->apply_resize( $im, $profile );

			if ( $keep_png ) {
				$im->setImageFormat( 'png' );
				$im->setImageCompressionQuality( 95 ); // PNG zlib level (95 ≈ default)
				$im->stripImage();
				$new_mime = 'image/png';
				$new_ext  = 'png';
			} else {
				// Flatten any alpha onto white before JPEG.
				if ( $im->getImageAlphaChannel() ) {
					$im->setImageBackgroundColor( 'white' );
					$im->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 11 );
					$flat = $im->mergeImageLayers( Imagick::LAYERMETHOD_FLATTEN );
					$im->clear();
					$im = $flat;
				}
				$im->setImageFormat( 'jpeg' );
				$im->setImageCompression( Imagick::COMPRESSION_JPEG );
				$im->setImageCompressionQuality( (int) $profile['quality'] );
				$im->setSamplingFactors( array( '2x2', '1x1', '1x1' ) ); // 4:2:0
				$im->stripImage();
				$new_mime = 'image/jpeg';
				$new_ext  = 'jpg';
			}

			$out = $im->getImageBlob();
			$im->clear();

			return array( $out, $new_mime, $new_ext );
		} catch ( Exception $e ) {
			return null;
		}
	}

	private function auto_orient( Imagick $im ): void {
		try {
			$orientation = $im->getImageOrientation();
			switch ( $orientation ) {
				case Imagick::ORIENTATION_TOPRIGHT:    $im->flopImage(); break;
				case Imagick::ORIENTATION_BOTTOMRIGHT: $im->rotateImage( 'black', 180 ); break;
				case Imagick::ORIENTATION_BOTTOMLEFT:  $im->flopImage(); $im->rotateImage( 'black', 180 ); break;
				case Imagick::ORIENTATION_LEFTTOP:     $im->flopImage(); $im->rotateImage( 'black', -90 ); break;
				case Imagick::ORIENTATION_RIGHTTOP:    $im->rotateImage( 'black', 90 ); break;
				case Imagick::ORIENTATION_RIGHTBOTTOM: $im->flopImage(); $im->rotateImage( 'black', 90 ); break;
				case Imagick::ORIENTATION_LEFTBOTTOM:  $im->rotateImage( 'black', -90 ); break;
			}
			$im->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
		} catch ( Exception $e ) {
			// Non-fatal — keep going with original pixels.
		}
	}

	private function has_real_transparency( Imagick $im ): bool {
		try {
			if ( ! $im->getImageAlphaChannel() ) {
				return false;
			}
			// Imagick 7 doesn't populate `getImageChannelStatistics()[CHANNEL_ALPHA]`
			// reliably — use `getImageChannelRange()` which returns the actual
			// min/max for the named channel. Min < quantum means at least one
			// pixel is partially or fully transparent.
			$quantum = (float) $im->getQuantum();
			if ( method_exists( $im, 'getImageChannelRange' ) ) {
				$range = $im->getImageChannelRange( Imagick::CHANNEL_ALPHA );
				if ( isset( $range['minima'] ) ) {
					$min = (float) $range['minima'];
					// Some builds normalize to 0..1; handle both.
					if ( $min <= 1.0 && $quantum > 1.0 ) {
						$min *= $quantum;
					}
					// Tiny tolerance to absorb float noise.
					return $min < ( $quantum - 1.0 );
				}
			}
			// Fall back to image type — if it ends in MATTE, channel is enabled.
			// Without channel-range data we can't tell if any pixel is actually
			// transparent, so err on the side of preserving alpha.
			$type = (int) $im->getImageType();
			return in_array(
				$type,
				array(
					Imagick::IMGTYPE_TRUECOLORMATTE,
					Imagick::IMGTYPE_GRAYSCALEMATTE,
					Imagick::IMGTYPE_PALETTEMATTE,
					Imagick::IMGTYPE_COLORSEPARATIONMATTE,
				),
				true
			);
		} catch ( Exception $e ) {
			return true;
		}
	}

	private function apply_edge_crops( Imagick $im, array $crop ): void {
		$w  = $im->getImageWidth();
		$h  = $im->getImageHeight();
		$cl = max( 0, min( 50, (int) ( $crop['left']   ?? 0 ) ) );
		$cr = max( 0, min( 50, (int) ( $crop['right']  ?? 0 ) ) );
		$ct = max( 0, min( 50, (int) ( $crop['top']    ?? 0 ) ) );
		$cb = max( 0, min( 50, (int) ( $crop['bottom'] ?? 0 ) ) );
		if ( 0 === $cl + $cr + $ct + $cb ) {
			return;
		}
		$x0 = (int) round( $w * $cl / 100 );
		$y0 = (int) round( $h * $ct / 100 );
		$x1 = $w - (int) round( $w * $cr / 100 );
		$y1 = $h - (int) round( $h * $cb / 100 );
		if ( $x1 > $x0 && $y1 > $y0 ) {
			$im->cropImage( $x1 - $x0, $y1 - $y0, $x0, $y0 );
			$im->setImagePage( 0, 0, 0, 0 );
		}
	}

	private function apply_resize( Imagick $im, array $profile ): void {
		$target_w = max( 1, (int) $profile['max_width'] );
		$target_h = max( 1, (int) $profile['max_height'] );
		$src_w    = $im->getImageWidth();
		$src_h    = $im->getImageHeight();

		if ( 'cover' === $profile['mode'] ) {
			$src_ratio    = $src_w / max( 1, $src_h );
			$target_ratio = $target_w / max( 1, $target_h );
			if ( $src_ratio > $target_ratio ) {
				$new_h = $target_h;
				$new_w = (int) round( $src_w * $target_h / max( 1, $src_h ) );
			} else {
				$new_w = $target_w;
				$new_h = (int) round( $src_h * $target_w / max( 1, $src_w ) );
			}
			$im->resizeImage( $new_w, $new_h, Imagick::FILTER_LANCZOS, 1 );
			$extra_w = $new_w - $target_w;
			$extra_h = $new_h - $target_h;
			list( $ax, $ay ) = $this->anchor_offset( (string) $profile['anchor'], $extra_w, $extra_h );
			$im->cropImage( $target_w, $target_h, $ax, $ay );
			$im->setImagePage( 0, 0, 0, 0 );
		} else {
			// fit-to-bounds: only shrink, never enlarge
			if ( $src_w > $target_w || $src_h > $target_h ) {
				$im->resizeImage( $target_w, $target_h, Imagick::FILTER_LANCZOS, 1, true );
			}
		}
	}

	private function anchor_offset( string $anchor, int $extra_w, int $extra_h ): array {
		$ax = (int) floor( $extra_w / 2 );
		$ay = (int) floor( $extra_h / 2 );
		if ( false !== strpos( $anchor, 'left' ) )  { $ax = 0; }
		if ( false !== strpos( $anchor, 'right' ) ) { $ax = $extra_w; }
		if ( false !== strpos( $anchor, 'top' ) )   { $ay = 0; }
		if ( false !== strpos( $anchor, 'bottom' ) ){ $ay = $extra_h; }
		return array( $ax, $ay );
	}

	// -------------------------------------------------------------------
	// Watermark removal (Phase 2)
	// -------------------------------------------------------------------

	/**
	 * Port of opti/app.py:apply_watermark_removal().
	 *
	 * Covers a bottom-right rectangle with a sampled fill color, optionally
	 * mixed with bilinear-resized noise for texture, blended into the
	 * surrounding image via a corner-distance gradient mask.
	 *
	 * @param Imagick $im Modified in-place.
	 * @param array   $w  Watermark config: w_pct, h_pct, sample, noise.
	 */
	private function apply_watermark_removal( Imagick $im, array $w ): void {
		try {
			$img_w = $im->getImageWidth();
			$img_h = $im->getImageHeight();
			if ( $img_w < 4 || $img_h < 4 ) {
				return;
			}

			$w_pct = max( 1, min( 50, (int) ( $w['w_pct'] ?? 11 ) ) ) / 100.0;
			$h_pct = max( 1, min( 50, (int) ( $w['h_pct'] ?? 11 ) ) ) / 100.0;
			$bw    = max( 2, (int) round( $img_w * $w_pct ) );
			$bh    = max( 2, (int) round( $img_h * $h_pct ) );
			$box_x = $img_w - $bw;
			$box_y = $img_h - $bh;

			$sample_mode = ( ( $w['sample'] ?? 'outside' ) === 'inside' ) ? 'inside' : 'outside';
			$noise_pct   = max( 0, min( 100, (int) ( $w['noise'] ?? 0 ) ) );

			// 1. Sample inside (box itself) and outside (strip above + to the left).
			$sample_strip = max( 2, (int) floor( min( $bw, $bh ) / 2 ) );
			$inside_avg   = $this->sample_avg_color( $im, $box_x, $box_y, $bw, $bh );

			$out_x0 = max( 0, $box_x - $sample_strip );
			$out_y0 = max( 0, $box_y - $sample_strip );
			$top_w  = $img_w - $out_x0; // strip from out_x0 to right edge
			$top_h  = $box_y - $out_y0;
			$left_w = $box_x - $out_x0;
			$left_h = $img_h - $box_y;

			$outside_samples = array();
			if ( $top_w > 0 && $top_h > 0 ) {
				$outside_samples[] = array(
					$this->sample_avg_color( $im, $out_x0, $out_y0, $top_w, $top_h ),
					$top_w * $top_h,
				);
			}
			if ( $left_w > 0 && $left_h > 0 ) {
				$outside_samples[] = array(
					$this->sample_avg_color( $im, $out_x0, $box_y, $left_w, $left_h ),
					$left_w * $left_h,
				);
			}
			$outside_avg = $this->weighted_avg_color( $outside_samples, $inside_avg );

			$primary   = ( 'inside' === $sample_mode ) ? $inside_avg : $outside_avg;
			$secondary = ( 'inside' === $sample_mode ) ? $outside_avg : $inside_avg;

			// 2. Build the fill layer.
			$fill = $this->build_fill_layer( $bw, $bh, $primary, $secondary, $noise_pct );
			if ( ! $fill instanceof Imagick ) {
				return;
			}

			// 3. Build the gradient mask (corner-distance feather) and apply
			//    it to the fill layer's alpha channel.
			$mask = $this->build_gradient_mask( $bw, $bh );
			$fill->setImageMatte( true );
			$fill->compositeImage( $mask, Imagick::COMPOSITE_COPYOPACITY, 0, 0 );
			$mask->clear();

			// 4. Composite the (now-feathered) fill over the original image
			//    at the bottom-right box position.
			$im->compositeImage( $fill, Imagick::COMPOSITE_OVER, $box_x, $box_y );
			$fill->clear();
		} catch ( Exception $e ) {
			// Non-fatal — leave the image as-is, keep the rest of the pipeline.
			return;
		}
	}

	/**
	 * Average RGB color of a rectangle. Returns [r,g,b] each 0..255.
	 *
	 * Implementation trick: crop a clone to the region, then resize to 1x1
	 * with the box filter — gives an averaged single pixel.
	 */
	private function sample_avg_color( Imagick $im, int $x, int $y, int $w, int $h ): array {
		if ( $w <= 0 || $h <= 0 ) {
			return array( 128, 128, 128 );
		}
		$x = max( 0, $x );
		$y = max( 0, $y );
		$w = min( $w, $im->getImageWidth()  - $x );
		$h = min( $h, $im->getImageHeight() - $y );
		if ( $w <= 0 || $h <= 0 ) {
			return array( 128, 128, 128 );
		}
		try {
			$clone = clone $im;
			$clone->cropImage( $w, $h, $x, $y );
			$clone->setImagePage( 0, 0, 0, 0 );
			$clone->resizeImage( 1, 1, Imagick::FILTER_BOX, 1 );
			$px = $clone->getImagePixelColor( 0, 0 )->getColor();
			$clone->clear();
			return array(
				(int) max( 0, min( 255, $px['r'] ?? 128 ) ),
				(int) max( 0, min( 255, $px['g'] ?? 128 ) ),
				(int) max( 0, min( 255, $px['b'] ?? 128 ) ),
			);
		} catch ( Exception $e ) {
			return array( 128, 128, 128 );
		}
	}

	/**
	 * Weighted average of two-or-more RGB samples. Falls back to $fallback if
	 * the input list is empty.
	 */
	private function weighted_avg_color( array $samples, array $fallback ): array {
		if ( empty( $samples ) ) {
			return $fallback;
		}
		$tw = 0; $tr = 0; $tg = 0; $tb = 0;
		foreach ( $samples as $s ) {
			list( $rgb, $weight ) = $s;
			$tw += $weight;
			$tr += $rgb[0] * $weight;
			$tg += $rgb[1] * $weight;
			$tb += $rgb[2] * $weight;
		}
		if ( $tw <= 0 ) {
			return $fallback;
		}
		return array(
			(int) round( $tr / $tw ),
			(int) round( $tg / $tw ),
			(int) round( $tb / $tw ),
		);
	}

	/**
	 * Build the solid (or noise-textured) fill layer for the watermark box.
	 * No alpha applied here — the gradient mask is composited on after.
	 */
	private function build_fill_layer( int $bw, int $bh, array $primary, array $secondary, int $noise_pct ): ?Imagick {
		try {
			$fill = new Imagick();
			$fill->newImage( $bw, $bh, $this->rgb_to_imagick_color( $primary ) );
			$fill->setImageFormat( 'png' );

			if ( $noise_pct > 0 && $primary !== $secondary ) {
				$noise = $this->build_noise_mask( $bw, $bh, $noise_pct );
				if ( $noise instanceof Imagick ) {
					$secondary_layer = new Imagick();
					$secondary_layer->newImage( $bw, $bh, $this->rgb_to_imagick_color( $secondary ) );
					$secondary_layer->setImageFormat( 'png' );
					$secondary_layer->setImageMatte( true );
					$secondary_layer->compositeImage( $noise, Imagick::COMPOSITE_COPYOPACITY, 0, 0 );
					$fill->compositeImage( $secondary_layer, Imagick::COMPOSITE_OVER, 0, 0 );
					$secondary_layer->clear();
					$noise->clear();
				}
			}

			return $fill;
		} catch ( Exception $e ) {
			return null;
		}
	}

	/**
	 * Bilinear-upscaled random noise pattern as a grayscale mask. Matches
	 * opti's NOISE_DIVISIONS=12 logic: noise grid is sized so the longer
	 * side has 12 cells and the shorter side scales proportionally.
	 */
	private function build_noise_mask( int $bw, int $bh, int $noise_pct ): ?Imagick {
		try {
			if ( $bw >= $bh ) {
				$nw = 12;
				$nh = max( 2, (int) round( 12 * $bh / max( 1, $bw ) ) );
			} else {
				$nh = 12;
				$nw = max( 2, (int) round( 12 * $bw / max( 1, $bh ) ) );
			}

			$pixels = array();
			$scale  = $noise_pct / 100.0;
			for ( $i = 0; $i < $nw * $nh; $i++ ) {
				$v = wp_rand( 0, 255 );
				if ( $scale < 1.0 ) {
					$v = (int) round( $v * $scale );
				}
				$pixels[] = max( 0, min( 255, $v ) );
			}

			$mask = new Imagick();
			$mask->newImage( $nw, $nh, 'black' );
			$mask->setImageFormat( 'png' );
			$mask->importImagePixels( 0, 0, $nw, $nh, 'I', Imagick::PIXEL_CHAR, $pixels );
			// Bilinear-resize to the box dimensions.
			$mask->resizeImage( $bw, $bh, Imagick::FILTER_TRIANGLE, 1 );
			return $mask;
		} catch ( Exception $e ) {
			return null;
		}
	}

	/**
	 * Build a grayscale gradient mask matching opti's `1 - sqrt((1-u)^2+(1-v)^2)`
	 * formula. Fully transparent at the top-left corner of the box (where it
	 * meets the rest of the image — so the original shows through), ramping to
	 * fully opaque inside the fade region, then fully opaque the rest of the box.
	 *
	 * Generated at a small size for speed and bilinear-resized up. The result
	 * is the size of the watermark box; the caller composites it as the
	 * alpha channel of the fill layer.
	 */
	private function build_gradient_mask( int $bw, int $bh ): Imagick {
		$fade_w = max( 1, (int) round( $bw * 0.3 ) );
		$fade_h = max( 1, (int) round( $bh * 0.3 ) );

		$small_w = 64;
		$small_h = 64;
		$pixels  = array();
		for ( $sy = 0; $sy < $small_h; $sy++ ) {
			$y_box       = $sy * $bh / $small_h;
			$u           = min( 1.0, $y_box / max( 1, $fade_h ) );
			$one_minus_u = 1.0 - $u;
			for ( $sx = 0; $sx < $small_w; $sx++ ) {
				$x_box       = $sx * $bw / $small_w;
				$v           = min( 1.0, $x_box / max( 1, $fade_w ) );
				$one_minus_v = 1.0 - $v;
				$d           = sqrt( $one_minus_u * $one_minus_u + $one_minus_v * $one_minus_v );
				$alpha       = 1.0 - $d;
				if ( $alpha < 0.0 ) { $alpha = 0.0; }
				if ( $alpha > 1.0 ) { $alpha = 1.0; }
				$pixels[] = (int) round( $alpha * 255 );
			}
		}

		$mask = new Imagick();
		$mask->newImage( $small_w, $small_h, 'black' );
		$mask->setImageFormat( 'png' );
		$mask->importImagePixels( 0, 0, $small_w, $small_h, 'I', Imagick::PIXEL_CHAR, $pixels );
		$mask->resizeImage( $bw, $bh, Imagick::FILTER_TRIANGLE, 1 );
		return $mask;
	}

	private function rgb_to_imagick_color( array $rgb ): string {
		return sprintf(
			'rgb(%d,%d,%d)',
			max( 0, min( 255, (int) ( $rgb[0] ?? 128 ) ) ),
			max( 0, min( 255, (int) ( $rgb[1] ?? 128 ) ) ),
			max( 0, min( 255, (int) ( $rgb[2] ?? 128 ) ) )
		);
	}

	/**
	 * Fallback for hosts without Imagick: use WP's image editor (GD).
	 * Only does the resize + JPEG re-encode — no watermark removal.
	 */
	private function run_pipeline_fallback( string $bytes, array $profile ): ?array {
		$tmp = wp_tempnam();
		if ( ! $tmp ) {
			return null;
		}
		if ( false === file_put_contents( $tmp, $bytes ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_put_contents_file_put_contents
			return null;
		}

		// Sniff PNG signature before handing the bytes to WP. Without Imagick
		// we can't cheaply inspect alpha-channel stats, so be conservative:
		// any PNG input keeps PNG output so we never destroy transparency.
		$keep_png = ( "\x89PNG\r\n\x1a\n" === substr( $bytes, 0, 8 ) );

		$editor = wp_get_image_editor( $tmp );
		if ( is_wp_error( $editor ) ) {
			@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			return null;
		}

		// Edge crops via WP editor.
		$cl = max( 0, min( 50, (int) ( $profile['crop']['left']   ?? 0 ) ) );
		$cr = max( 0, min( 50, (int) ( $profile['crop']['right']  ?? 0 ) ) );
		$ct = max( 0, min( 50, (int) ( $profile['crop']['top']    ?? 0 ) ) );
		$cb = max( 0, min( 50, (int) ( $profile['crop']['bottom'] ?? 0 ) ) );
		if ( 0 < $cl + $cr + $ct + $cb ) {
			$size = $editor->get_size();
			$w    = (int) ( $size['width'] ?? 0 );
			$h    = (int) ( $size['height'] ?? 0 );
			if ( $w > 0 && $h > 0 ) {
				$x0 = (int) round( $w * $cl / 100 );
				$y0 = (int) round( $h * $ct / 100 );
				$cw = $w - $x0 - (int) round( $w * $cr / 100 );
				$ch = $h - $y0 - (int) round( $h * $cb / 100 );
				if ( $cw > 0 && $ch > 0 ) {
					$editor->crop( $x0, $y0, $cw, $ch );
				}
			}
		}

		$target_w = max( 1, (int) $profile['max_width'] );
		$target_h = max( 1, (int) $profile['max_height'] );
		$crop_after = ( 'cover' === ( $profile['mode'] ?? 'fit' ) );
		$resized = $editor->resize( $target_w, $target_h, $crop_after );
		if ( is_wp_error( $resized ) ) {
			@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			return null;
		}

		$editor->set_quality( max( 10, min( 100, (int) ( $profile['quality'] ?? 82 ) ) ) );

		if ( $keep_png ) {
			$saved = $editor->save( null, 'image/png' );
		} else {
			$saved = $editor->save( null, 'image/jpeg' );
		}
		@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		if ( is_wp_error( $saved ) || empty( $saved['path'] ) || ! is_readable( $saved['path'] ) ) {
			return null;
		}
		$out = file_get_contents( $saved['path'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		@unlink( $saved['path'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		if ( false === $out ) {
			return null;
		}
		$new_mime = $saved['mime-type'] ?? ( $keep_png ? 'image/png' : 'image/jpeg' );
		$new_ext  = $keep_png ? 'png' : 'jpg';
		return array( $out, $new_mime, $new_ext );
	}

	// -------------------------------------------------------------------
	// Stats
	// -------------------------------------------------------------------

	private function record_stats( int $bytes_saved ): void {
		if ( $bytes_saved < 0 ) {
			$bytes_saved = 0;
		}

		// Bypass the cached settings layer — go straight to the option so we
		// pick up any concurrent writes from this same request and don't get
		// clobbered by a stale snapshot held by the singleton.
		$option = get_option( BW_Dev_Settings::OPTION, array() );
		if ( ! is_array( $option ) ) {
			$option = array();
		}
		$section = isset( $option[ $this->slug() ] ) && is_array( $option[ $this->slug() ] )
			? $option[ $this->slug() ]
			: array();

		$stats = isset( $section['stats'] ) && is_array( $section['stats'] )
			? $section['stats']
			: array( 'images_processed' => 0, 'bytes_saved' => 0 );

		$stats['images_processed'] = absint( $stats['images_processed'] ?? 0 ) + 1;
		$stats['bytes_saved']      = max( 0, (int) ( $stats['bytes_saved'] ?? 0 ) ) + $bytes_saved;

		$section['stats']            = $stats;
		$option[ $this->slug() ]     = $section;

		update_option( BW_Dev_Settings::OPTION, $option );

		// Keep the in-memory settings cache in sync so subsequent reads in this
		// same request (e.g., admin-bar widget render after upload) see the
		// fresh stats.
		bw_dev()->settings()->update_section( $this->slug(), $section );
	}

	// -------------------------------------------------------------------
	// Per-user mode (admin bar widget)
	// -------------------------------------------------------------------

	/**
	 * Returns the user's per-user upload choice: either 'off' or a profile slug.
	 * Falls back to the site-wide default profile when no per-user value is set.
	 * Handles a one-time migration from the pre-multi-profile `_mode` meta.
	 */
	public function get_user_choice( ?int $user_id = null ): string {
		$user_id = $user_id ?: get_current_user_id();
		$default = (string) bw_dev()->settings()->get( $this->slug(), 'default_profile', self::DEFAULT_PROFILE_SLUG );
		$default = '' !== $default ? $default : self::DEFAULT_PROFILE_SLUG;

		if ( ! $user_id ) {
			return $default;
		}

		$raw = (string) get_user_meta( $user_id, self::META_USER_PROFILE, true );
		if ( '' === $raw ) {
			// One-time migration from the old `_mode` meta (off/active/resize_only).
			$legacy = (string) get_user_meta( $user_id, self::META_USER_MODE, true );
			if ( 'off' === $legacy ) {
				update_user_meta( $user_id, self::META_USER_PROFILE, self::MODE_OFF );
				delete_user_meta( $user_id, self::META_USER_MODE );
				return self::MODE_OFF;
			}
			if ( in_array( $legacy, array( 'active', 'resize_only' ), true ) ) {
				update_user_meta( $user_id, self::META_USER_PROFILE, $default );
				delete_user_meta( $user_id, self::META_USER_MODE );
				return $default;
			}
			return $default;
		}

		if ( self::MODE_OFF === $raw ) {
			return self::MODE_OFF;
		}

		$profiles = (array) bw_dev()->settings()->get( $this->slug(), 'profiles', array() );
		if ( isset( $profiles[ $raw ] ) ) {
			return $raw;
		}
		// Profile was deleted since the user last picked — fall back to default.
		return $default;
	}

	public function admin_bar_widget( $bar ): void {
		if ( ! ( $bar instanceof WP_Admin_Bar ) ) {
			return;
		}
		if ( ! current_user_can( 'upload_files' ) ) {
			return;
		}
		if ( ! $this->is_feature_enabled() ) {
			return;
		}

		$choice   = $this->get_user_choice();
		$profiles = (array) bw_dev()->settings()->get( $this->slug(), 'profiles', array() );
		if ( empty( $profiles ) ) {
			return;
		}

		// Top label: emoji + current profile name (or "Off").
		if ( self::MODE_OFF === $choice ) {
			$top_label = '⚫ ' . __( 'Optimizer: Off', 'bw-dev' );
		} else {
			$current = isset( $profiles[ $choice ] ) ? $profiles[ $choice ] : reset( $profiles );
			$name    = isset( $current['label'] ) ? (string) $current['label'] : (string) $choice;
			$top_label = '🟢 ' . sprintf( __( 'Optimizer: %s', 'bw-dev' ), $name );
		}

		$bar->add_node(
			array(
				'id'    => 'bw-dev-image-optimizer',
				'title' => $top_label,
				'href'  => false,
			)
		);

		// Off (always first).
		$off_url = wp_nonce_url(
			admin_url( 'admin-post.php?action=' . self::SET_MODE_ACTION . '&mode=' . self::MODE_OFF ),
			self::NONCE_SET_MODE
		);
		$bar->add_node(
			array(
				'id'     => 'bw-dev-image-optimizer-off',
				'parent' => 'bw-dev-image-optimizer',
				'title'  => '⚫ ' . __( 'Off', 'bw-dev' ) . ( self::MODE_OFF === $choice ? ' ✓' : '' ),
				'href'   => $off_url,
			)
		);

		// One node per profile.
		foreach ( $profiles as $slug => $profile ) {
			$url   = wp_nonce_url(
				admin_url( 'admin-post.php?action=' . self::SET_MODE_ACTION . '&mode=' . rawurlencode( $slug ) ),
				self::NONCE_SET_MODE
			);
			$name  = isset( $profile['label'] ) ? (string) $profile['label'] : (string) $slug;
			$check = ( $slug === $choice ) ? ' ✓' : '';
			$bar->add_node(
				array(
					'id'     => 'bw-dev-image-optimizer-profile-' . sanitize_html_class( $slug ),
					'parent' => 'bw-dev-image-optimizer',
					'title'  => '🟢 ' . $name . $check,
					'href'   => $url,
				)
			);
		}
	}

	/**
	 * Conditionally enqueue wp.media + the settings-page JS only when the
	 * Image Optimizer tab is the active one. Keeps the asset off every other
	 * admin page.
	 */
	public function maybe_enqueue_settings_assets( $hook ): void {
		if ( 'settings_page_' . BW_Dev_Admin_Page::MENU_SLUG !== $hook ) {
			return;
		}
		$query = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$tab   = isset( $query['tab'] ) ? sanitize_key( (string) $query['tab'] ) : '';
		if ( $this->slug() !== $tab ) {
			return;
		}
		wp_enqueue_media();
	}

	/**
	 * AJAX endpoint that runs the watermark-removal algorithm on the configured
	 * sample image and returns the result as a data URL. Lets the settings
	 * page show before/after previews as the user tweaks watermark sliders.
	 */
	public function handle_preview_ajax(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Forbidden.', 'bw-dev' ) ), 403 );
		}
		check_ajax_referer( self::NONCE_PREVIEW );

		$attachment_id = isset( $_POST['attachment_id'] ) ? absint( $_POST['attachment_id'] ) : 0;
		if ( $attachment_id <= 0 ) {
			wp_send_json_error( array( 'message' => __( 'No sample image selected.', 'bw-dev' ) ), 400 );
		}

		$file = get_attached_file( $attachment_id );
		if ( ! $file || ! is_readable( $file ) ) {
			wp_send_json_error( array( 'message' => __( 'Sample image file is not readable.', 'bw-dev' ) ), 400 );
		}
		$ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
		if ( ! in_array( $ext, self::SUPPORTED_EXTS, true ) ) {
			wp_send_json_error( array( 'message' => __( 'Sample image must be PNG, JPG, or WebP.', 'bw-dev' ) ), 400 );
		}

		if ( ! class_exists( 'Imagick' ) ) {
			wp_send_json_error( array( 'message' => __( 'Imagick is required for the watermark preview. Phase 1 resize/compress still works on GD-only hosts; the preview just isn\'t available.', 'bw-dev' ) ), 400 );
		}

		// Build a full profile from form values. This makes the preview
		// reflect the entire pipeline (watermark + edge crops + resize +
		// JPEG quality) so the user sees exactly what their upload would
		// produce, not just the watermark step.
		$profile_in = isset( $_POST['profile'] ) && is_array( $_POST['profile'] ) ? wp_unslash( $_POST['profile'] ) : array();
		$profile    = $this->sanitize_profile_payload( $profile_in );

		try {
			$im = new Imagick( $file );
			$this->auto_orient( $im );

			// Mirror run_pipeline_imagick: watermark first (so the box
			// position is relative to the source dims the user sees),
			// then edge crops, then resize.
			if ( ! empty( $profile['watermark']['enabled'] ) ) {
				$this->apply_watermark_removal( $im, $profile['watermark'] );
			}
			$this->apply_edge_crops( $im, $profile['crop'] );
			$this->apply_resize( $im, $profile );

			// After the profile's resize, downscale further for transport
			// only if the result is still bigger than the preview-panel cap.
			// Smaller profile targets (e.g. Square 1080) skip this step.
			if ( $im->getImageWidth() > self::PREVIEW_MAX_WIDTH ) {
				$im->resizeImage( self::PREVIEW_MAX_WIDTH, 0, Imagick::FILTER_LANCZOS, 1 );
			}

			$im->setImageFormat( 'jpeg' );
			$im->setImageCompression( Imagick::COMPRESSION_JPEG );
			// Use the profile's quality so the preview also shows how the
			// JPEG compression setting affects the final image.
			$im->setImageCompressionQuality( (int) $profile['quality'] );
			$im->stripImage();
			$bytes = $im->getImageBlob();
			$im->clear();

			$data_url = 'data:image/jpeg;base64,' . base64_encode( $bytes ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode

			wp_send_json_success(
				array(
					'data_url' => $data_url,
					'bytes'    => strlen( $bytes ),
				)
			);
		} catch ( Exception $e ) {
			wp_send_json_error(
				array( 'message' => sprintf( __( 'Preview failed: %s', 'bw-dev' ), $e->getMessage() ) ),
				500
			);
		}
	}

	public function handle_set_mode(): void {
		if ( ! current_user_can( 'upload_files' ) ) {
			wp_die( esc_html__( 'You do not have permission to do this.', 'bw-dev' ), '', array( 'response' => 403 ) );
		}
		check_admin_referer( self::NONCE_SET_MODE );

		$choice = isset( $_GET['mode'] ) ? sanitize_key( wp_unslash( (string) $_GET['mode'] ) ) : '';
		if ( self::MODE_OFF !== $choice ) {
			$profiles = (array) bw_dev()->settings()->get( $this->slug(), 'profiles', array() );
			if ( ! isset( $profiles[ $choice ] ) ) {
				wp_die( esc_html__( 'Unknown profile.', 'bw-dev' ), '', array( 'response' => 400 ) );
			}
		}

		update_user_meta( get_current_user_id(), self::META_USER_PROFILE, $choice );
		// Clear the legacy meta opportunistically.
		delete_user_meta( get_current_user_id(), self::META_USER_MODE );

		$back = wp_get_referer();
		wp_safe_redirect( $back ? $back : admin_url() );
		exit;
	}

	// -------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------

	public function is_feature_enabled(): bool {
		return (bool) bw_dev()->settings()->get( $this->slug(), 'enabled', false );
	}

	/**
	 * Look up a profile by slug, with safe fallbacks. Returns null only
	 * if the user explicitly chose 'off'.
	 */
	public function get_profile_for_user( string $user_choice ): ?array {
		if ( self::MODE_OFF === $user_choice ) {
			return null;
		}
		$profiles = (array) bw_dev()->settings()->get( $this->slug(), 'profiles', array() );
		if ( empty( $profiles ) ) {
			return $this->default_profile();
		}
		if ( isset( $profiles[ $user_choice ] ) && is_array( $profiles[ $user_choice ] ) ) {
			return wp_parse_args( $profiles[ $user_choice ], $this->default_profile() );
		}
		$default_slug = (string) bw_dev()->settings()->get( $this->slug(), 'default_profile', '' );
		if ( '' !== $default_slug && isset( $profiles[ $default_slug ] ) && is_array( $profiles[ $default_slug ] ) ) {
			return wp_parse_args( $profiles[ $default_slug ], $this->default_profile() );
		}
		$first = reset( $profiles );
		return is_array( $first ) ? wp_parse_args( $first, $this->default_profile() ) : $this->default_profile();
	}

	// -------------------------------------------------------------------
	// Settings tab
	// -------------------------------------------------------------------

	public function render_tab(): void {
		$settings = (array) bw_dev()->settings()->get( $this->slug(), null, array() );
		$settings = wp_parse_args( $settings, $this->default_settings() );
		$profiles = isset( $settings['profiles'] ) && is_array( $settings['profiles'] ) && ! empty( $settings['profiles'] )
			? $settings['profiles']
			: array( self::DEFAULT_PROFILE_SLUG => $this->default_profile() );
		// Ensure each profile is fully shaped (filling defaults for any missing keys).
		foreach ( $profiles as $slug => $p ) {
			$profiles[ $slug ] = wp_parse_args( is_array( $p ) ? $p : array(), $this->default_profile() );
			$profiles[ $slug ]['watermark'] = wp_parse_args(
				isset( $p['watermark'] ) && is_array( $p['watermark'] ) ? $p['watermark'] : array(),
				$this->default_profile()['watermark']
			);
			$profiles[ $slug ]['crop'] = wp_parse_args(
				isset( $p['crop'] ) && is_array( $p['crop'] ) ? $p['crop'] : array(),
				$this->default_profile()['crop']
			);
		}

		$default_slug = (string) ( $settings['default_profile'] ?? self::DEFAULT_PROFILE_SLUG );
		if ( ! isset( $profiles[ $default_slug ] ) ) {
			$default_slug = array_keys( $profiles )[0];
		}

		$preview_id  = absint( $settings['preview_attachment_id'] ?? 0 );
		$preview_url = $preview_id ? wp_get_attachment_image_url( $preview_id, 'large' ) : '';

		$stats       = isset( $settings['stats'] ) ? $settings['stats'] : array( 'images_processed' => 0, 'bytes_saved' => 0 );
		$name_prefix = BW_Dev_Settings::OPTION . '[' . $this->slug() . ']';
		$has_imagick = class_exists( 'Imagick' );

		$anchors = array(
			'top-left'     => __( 'Top left', 'bw-dev' ),
			'top'          => __( 'Top', 'bw-dev' ),
			'top-right'    => __( 'Top right', 'bw-dev' ),
			'left'         => __( 'Left', 'bw-dev' ),
			'center'       => __( 'Center', 'bw-dev' ),
			'right'        => __( 'Right', 'bw-dev' ),
			'bottom-left'  => __( 'Bottom left', 'bw-dev' ),
			'bottom'       => __( 'Bottom', 'bw-dev' ),
			'bottom-right' => __( 'Bottom right', 'bw-dev' ),
		);
		?>
		<style>
			.bw-dev-io-profile-tabs { display:flex; gap:2px; flex-wrap:wrap; margin:14px 0 0; border-bottom:1px solid #ccd0d4; padding-bottom:0; }
			.bw-dev-io-profile-tabs .bw-dev-io-tab { padding:8px 14px; background:#f0f0f1; border:1px solid #ccd0d4; border-bottom:none; border-radius:4px 4px 0 0; cursor:pointer; color:#1d2327; }
			.bw-dev-io-profile-tabs .bw-dev-io-tab.active { background:#fff; font-weight:600; position:relative; top:1px; }
			.bw-dev-io-profile-tabs .bw-dev-io-tab.add { background:transparent; border-style:dashed; }
			.bw-dev-io-profile-pane { display:none; border:1px solid #ccd0d4; border-top:none; padding:20px; background:#fff; }
			.bw-dev-io-profile-pane.active { display:block; }
			.bw-dev-io-layout { display:flex; gap:24px; flex-wrap:wrap; align-items:flex-start; }
			.bw-dev-io-col-form { flex:1 1 480px; min-width:0; }
			.bw-dev-io-col-preview { flex:1 1 400px; min-width:0; position:sticky; top:42px; }
			.bw-dev-io-preview-stage { background:#f6f7f7; border:1px solid #ccd0d4; padding:12px; border-radius:4px; }
			.bw-dev-io-preview-stage img { display:block; max-width:100%; height:auto; }
			.bw-dev-io-preview-stage figure { margin:0 0 12px; }
			.bw-dev-io-preview-stage figure:last-child { margin-bottom:0; }
			.bw-dev-io-preview-stage figcaption { font-weight:600; margin-bottom:6px; }
			.bw-dev-io-preview-stage.bw-dev-io-stale figure:last-child img { outline:2px dashed #d8a900; outline-offset:-2px; }
			.bw-dev-io-preview-spinner { display:inline-block; width:14px; height:14px; vertical-align:-2px; border:2px solid #ccc; border-top-color:#2271b1; border-radius:50%; animation:bw-dev-io-spin 0.8s linear infinite; }
			@keyframes bw-dev-io-spin { to { transform: rotate(360deg); } }
			.bw-dev-io-profile-meta { margin-top:14px; padding-top:14px; border-top:1px solid #eee; }
			.bw-dev-io-delete { color:#a00; }
			.bw-dev-io-delete:hover { color:#dc3232; }
		</style>

		<p class="description">
			<?php esc_html_e( 'Optimizes image uploads before WordPress saves them — preventing huge originals (typical of AI-generated PNGs) from filling the Media Library. Each profile bundles a resize target + JPEG quality + optional watermark removal. Editors pick which profile to apply from the admin-bar widget.', 'bw-dev' ); ?>
		</p>

		<?php if ( ! $has_imagick ) : ?>
			<div class="notice notice-warning inline" style="margin:14px 0;">
				<p>
					<?php esc_html_e( 'Imagick is not installed on this host. The module will still resize and compress uploads using WordPress\'s built-in image editor (GD), but watermark removal and the live preview require Imagick.', 'bw-dev' ); ?>
				</p>
			</div>
		<?php endif; ?>

		<table class="form-table" role="presentation">
			<tbody>
				<tr>
					<th scope="row"><?php esc_html_e( 'Process new uploads', 'bw-dev' ); ?></th>
					<td>
						<label>
							<input type="checkbox" name="<?php echo esc_attr( $name_prefix . '[enabled]' ); ?>" value="1" <?php checked( ! empty( $settings['enabled'] ) ); ?> />
							<?php esc_html_e( 'Apply the editor\'s selected profile to every new image upload (PNG, JPG, WebP).', 'bw-dev' ); ?>
						</label>
						<p class="description">
							<?php esc_html_e( 'Off by default. Once on, the admin-bar "Optimizer" widget lets each editor pick a profile (or Off) per-session.', 'bw-dev' ); ?>
						</p>
					</td>
				</tr>
			</tbody>
		</table>

		<h2><?php esc_html_e( 'Profiles', 'bw-dev' ); ?></h2>
		<p class="description">
			<?php esc_html_e( 'Each profile is a reusable bundle: resize target + JPEG quality + optional watermark removal. Editors pick one from the admin bar before uploading. The profile flagged as "default" applies to editors who haven\'t made a choice yet.', 'bw-dev' ); ?>
		</p>

		<nav class="bw-dev-io-profile-tabs" id="bw-dev-io-tabs">
			<?php
			$is_first = true;
			foreach ( $profiles as $slug => $profile ) :
				$is_active = ( $slug === $default_slug ) ? true : $is_first;
				$is_first  = false;
				?>
				<button type="button"
					class="bw-dev-io-tab <?php echo $is_active ? 'active' : ''; ?>"
					data-target="<?php echo esc_attr( $slug ); ?>">
					<?php echo esc_html( (string) $profile['label'] ); ?>
				</button>
			<?php endforeach; ?>
			<button type="button" class="bw-dev-io-tab add" id="bw-dev-io-add-profile">
				+ <?php esc_html_e( 'Add profile', 'bw-dev' ); ?>
			</button>
		</nav>

		<?php
		$is_first = true;
		foreach ( $profiles as $slug => $profile ) :
			$wm        = $profile['watermark'];
			$pane_id   = 'bw-dev-io-pane-' . sanitize_html_class( $slug );
			$pane_base = $name_prefix . '[profiles][' . $slug . ']';
			$is_active = ( $slug === $default_slug ) ? true : $is_first;
			$is_first  = false;
			?>
			<div class="bw-dev-io-profile-pane <?php echo $is_active ? 'active' : ''; ?>"
				id="<?php echo esc_attr( $pane_id ); ?>"
				data-slug="<?php echo esc_attr( $slug ); ?>">

				<div class="bw-dev-io-layout">
					<div class="bw-dev-io-col-form">

						<table class="form-table" role="presentation">
							<tbody>
								<tr>
									<th scope="row"><?php esc_html_e( 'Profile name', 'bw-dev' ); ?></th>
									<td>
										<input type="text"
											name="<?php echo esc_attr( $pane_base . '[label]' ); ?>"
											value="<?php echo esc_attr( $profile['label'] ); ?>"
											class="regular-text bw-dev-io-label-input"
											data-slug="<?php echo esc_attr( $slug ); ?>" />
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Max dimensions (px)', 'bw-dev' ); ?></th>
									<td>
										<label><?php esc_html_e( 'Width', 'bw-dev' ); ?>
											<input type="number" min="1" max="8000" name="<?php echo esc_attr( $pane_base . '[max_width]' ); ?>" value="<?php echo esc_attr( (string) $profile['max_width'] ); ?>" class="small-text" />
										</label>
										&nbsp;×&nbsp;
										<label><?php esc_html_e( 'Height', 'bw-dev' ); ?>
											<input type="number" min="1" max="8000" name="<?php echo esc_attr( $pane_base . '[max_height]' ); ?>" value="<?php echo esc_attr( (string) $profile['max_height'] ); ?>" class="small-text" />
										</label>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Resize mode', 'bw-dev' ); ?></th>
									<td>
										<label><input type="radio" name="<?php echo esc_attr( $pane_base . '[mode]' ); ?>" value="fit" <?php checked( $profile['mode'], 'fit' ); ?> /> <?php esc_html_e( 'Fit (preserve aspect ratio; never enlarges)', 'bw-dev' ); ?></label><br />
										<label><input type="radio" name="<?php echo esc_attr( $pane_base . '[mode]' ); ?>" value="cover" <?php checked( $profile['mode'], 'cover' ); ?> /> <?php esc_html_e( 'Cover (fills the box; then crops to anchor)', 'bw-dev' ); ?></label>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Anchor (cover mode)', 'bw-dev' ); ?></th>
									<td>
										<select name="<?php echo esc_attr( $pane_base . '[anchor]' ); ?>">
											<?php foreach ( $anchors as $val => $label ) : ?>
												<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $profile['anchor'], $val ); ?>><?php echo esc_html( $label ); ?></option>
											<?php endforeach; ?>
										</select>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'JPEG quality', 'bw-dev' ); ?></th>
									<td>
										<input type="number" min="10" max="100" name="<?php echo esc_attr( $pane_base . '[quality]' ); ?>" value="<?php echo esc_attr( (string) $profile['quality'] ); ?>" class="small-text" />
										<p class="description"><?php esc_html_e( '10 = tiny + ugly, 100 = huge + pristine. 82 is a good default.', 'bw-dev' ); ?></p>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Edge crops (%)', 'bw-dev' ); ?></th>
									<td>
										<?php foreach ( array( 'left' => __( 'Left', 'bw-dev' ), 'top' => __( 'Top', 'bw-dev' ), 'right' => __( 'Right', 'bw-dev' ), 'bottom' => __( 'Bottom', 'bw-dev' ) ) as $side => $label ) : ?>
											<label style="margin-right:14px;">
												<?php echo esc_html( $label ); ?>
												<input type="number" min="0" max="50" name="<?php echo esc_attr( $pane_base . '[crop][' . $side . ']' ); ?>" value="<?php echo esc_attr( (string) ( $profile['crop'][ $side ] ?? 0 ) ); ?>" class="small-text" />
											</label>
										<?php endforeach; ?>
									</td>
								</tr>
							</tbody>
						</table>

						<h3 style="margin-top:24px;"><?php esc_html_e( 'Watermark removal', 'bw-dev' ); ?></h3>
						<table class="form-table" role="presentation">
							<tbody>
								<tr>
									<th scope="row"><?php esc_html_e( 'Enable', 'bw-dev' ); ?></th>
									<td>
										<label>
											<input type="checkbox" class="bw-dev-io-wm-field"
												name="<?php echo esc_attr( $pane_base . '[watermark][enabled]' ); ?>"
												value="1" <?php checked( ! empty( $wm['enabled'] ) ); ?> />
											<?php esc_html_e( 'Remove a bottom-right watermark when this profile is applied.', 'bw-dev' ); ?>
										</label>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Box size (% of image)', 'bw-dev' ); ?></th>
									<td>
										<label style="margin-right:14px;"><?php esc_html_e( 'Width', 'bw-dev' ); ?>
											<input type="number" min="1" max="50" class="small-text bw-dev-io-wm-field"
												name="<?php echo esc_attr( $pane_base . '[watermark][w_pct]' ); ?>"
												value="<?php echo esc_attr( (string) $wm['w_pct'] ); ?>" />%
										</label>
										<label><?php esc_html_e( 'Height', 'bw-dev' ); ?>
											<input type="number" min="1" max="50" class="small-text bw-dev-io-wm-field"
												name="<?php echo esc_attr( $pane_base . '[watermark][h_pct]' ); ?>"
												value="<?php echo esc_attr( (string) $wm['h_pct'] ); ?>" />%
										</label>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Sample colors from', 'bw-dev' ); ?></th>
									<td>
										<label style="margin-right:14px;">
											<input type="radio" class="bw-dev-io-wm-field"
												name="<?php echo esc_attr( $pane_base . '[watermark][sample]' ); ?>"
												value="outside" <?php checked( $wm['sample'], 'outside' ); ?> />
											<?php esc_html_e( 'Outside the box', 'bw-dev' ); ?>
										</label>
										<label>
											<input type="radio" class="bw-dev-io-wm-field"
												name="<?php echo esc_attr( $pane_base . '[watermark][sample]' ); ?>"
												value="inside" <?php checked( $wm['sample'], 'inside' ); ?> />
											<?php esc_html_e( 'Inside the box', 'bw-dev' ); ?>
										</label>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Noise', 'bw-dev' ); ?></th>
									<td>
										<input type="number" min="0" max="100" class="small-text bw-dev-io-wm-field"
											name="<?php echo esc_attr( $pane_base . '[watermark][noise]' ); ?>"
											value="<?php echo esc_attr( (string) $wm['noise'] ); ?>" />%
										<p class="description"><?php esc_html_e( '0 = flat fill, 100 = fully variable. Helpful over busy backgrounds.', 'bw-dev' ); ?></p>
									</td>
								</tr>
							</tbody>
						</table>

						<div class="bw-dev-io-profile-meta">
							<label>
								<input type="radio"
									name="<?php echo esc_attr( $name_prefix . '[default_profile]' ); ?>"
									value="<?php echo esc_attr( $slug ); ?>"
									<?php checked( $slug, $default_slug ); ?> />
								<?php esc_html_e( 'Use as the default profile (applied to editors who haven\'t picked one)', 'bw-dev' ); ?>
							</label>
							&nbsp;&nbsp;
							<button type="button" class="button-link bw-dev-io-delete bw-dev-io-delete-btn"
								data-slug="<?php echo esc_attr( $slug ); ?>">
								<?php esc_html_e( 'Delete this profile', 'bw-dev' ); ?>
							</button>
						</div>

					</div><!-- /.bw-dev-io-col-form -->

					<?php if ( $is_active ) : // Render the preview column once, in the active pane only. ?>
					<div class="bw-dev-io-col-preview" id="bw-dev-io-preview-col">
						<h3 style="margin-top:0;"><?php esc_html_e( 'Preview', 'bw-dev' ); ?></h3>
						<p class="description"><?php esc_html_e( 'Click Update preview after changing settings. Server-side processing can take a few seconds on slower hosts.', 'bw-dev' ); ?></p>

						<div style="margin-bottom:10px;">
							<input type="hidden" id="bw-dev-io-preview-id" name="<?php echo esc_attr( $name_prefix . '[preview_attachment_id]' ); ?>" value="<?php echo esc_attr( (string) $preview_id ); ?>" />
							<button type="button" class="button" id="bw-dev-io-preview-pick"><?php esc_html_e( 'Choose sample image', 'bw-dev' ); ?></button>
							<button type="button" class="button" id="bw-dev-io-preview-clear" <?php echo $preview_id ? '' : 'style="display:none;"'; ?>><?php esc_html_e( 'Clear', 'bw-dev' ); ?></button>
						</div>

						<div style="margin-bottom:12px;">
							<button type="button" class="button button-primary" id="bw-dev-io-preview-update" <?php echo $preview_id ? '' : 'disabled'; ?>>
								<span class="bw-dev-io-preview-spinner" aria-hidden="true" style="display:none; margin-right:6px;"></span>
								<span class="bw-dev-io-preview-btn-label"><?php esc_html_e( 'Update preview', 'bw-dev' ); ?></span>
							</button>
							<span id="bw-dev-io-preview-status" style="font-size:12px; color:#666; margin-left:8px;"></span>
						</div>

						<div class="bw-dev-io-preview-stage" id="bw-dev-io-preview-stage" style="display:<?php echo $preview_id ? 'block' : 'none'; ?>;">
							<figure>
								<figcaption><?php esc_html_e( 'Before', 'bw-dev' ); ?></figcaption>
								<img id="bw-dev-io-preview-before" src="<?php echo esc_url( $preview_url ); ?>" alt="" />
							</figure>
							<figure>
								<figcaption><?php esc_html_e( 'After', 'bw-dev' ); ?></figcaption>
								<img id="bw-dev-io-preview-after" src="" alt="" style="opacity:0.4;" />
							</figure>
						</div>
					</div>
					<?php endif; ?>

				</div><!-- /.bw-dev-io-layout -->
			</div><!-- /.bw-dev-io-profile-pane -->
		<?php endforeach; ?>

		<!-- Hidden container that JS appends __deleted_profiles[] inputs into. -->
		<div id="bw-dev-io-deleted-bin" style="display:none;"></div>

		<!-- Template for a new profile pane (cloned by JS). -->
		<template id="bw-dev-io-profile-template">
			<?php
			$tpl = $this->default_profile();
			?>
			<div class="bw-dev-io-profile-pane" data-slug="__SLUG__" id="bw-dev-io-pane-__SLUG__">
				<div class="bw-dev-io-layout">
					<div class="bw-dev-io-col-form">
						<table class="form-table" role="presentation">
							<tbody>
								<tr><th scope="row"><?php esc_html_e( 'Profile name', 'bw-dev' ); ?></th>
								<td><input type="text" class="regular-text bw-dev-io-label-input" data-slug="__SLUG__" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][label]" value="<?php echo esc_attr( __( 'New profile', 'bw-dev' ) ); ?>" /></td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Max dimensions (px)', 'bw-dev' ); ?></th>
								<td>
									<label><?php esc_html_e( 'Width', 'bw-dev' ); ?> <input type="number" min="1" max="8000" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][max_width]" value="<?php echo (int) $tpl['max_width']; ?>" class="small-text" /></label>
									&nbsp;×&nbsp;
									<label><?php esc_html_e( 'Height', 'bw-dev' ); ?> <input type="number" min="1" max="8000" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][max_height]" value="<?php echo (int) $tpl['max_height']; ?>" class="small-text" /></label>
								</td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Resize mode', 'bw-dev' ); ?></th>
								<td>
									<label><input type="radio" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][mode]" value="fit" checked /> <?php esc_html_e( 'Fit', 'bw-dev' ); ?></label><br />
									<label><input type="radio" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][mode]" value="cover" /> <?php esc_html_e( 'Cover', 'bw-dev' ); ?></label>
								</td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Anchor', 'bw-dev' ); ?></th>
								<td>
									<select name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][anchor]">
										<?php foreach ( $anchors as $val => $label ) : ?>
											<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $val, $tpl['anchor'] ); ?>><?php echo esc_html( $label ); ?></option>
										<?php endforeach; ?>
									</select>
								</td></tr>
								<tr><th scope="row"><?php esc_html_e( 'JPEG quality', 'bw-dev' ); ?></th>
								<td><input type="number" min="10" max="100" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][quality]" value="<?php echo (int) $tpl['quality']; ?>" class="small-text" /></td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Edge crops (%)', 'bw-dev' ); ?></th>
								<td>
									<?php foreach ( array( 'left', 'top', 'right', 'bottom' ) as $side ) : ?>
										<label style="margin-right:14px;"><?php echo esc_html( ucfirst( $side ) ); ?> <input type="number" min="0" max="50" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][crop][<?php echo esc_attr( $side ); ?>]" value="0" class="small-text" /></label>
									<?php endforeach; ?>
								</td></tr>
							</tbody>
						</table>
						<h3 style="margin-top:24px;"><?php esc_html_e( 'Watermark removal', 'bw-dev' ); ?></h3>
						<table class="form-table" role="presentation">
							<tbody>
								<tr><th scope="row"><?php esc_html_e( 'Enable', 'bw-dev' ); ?></th>
								<td><label><input type="checkbox" class="bw-dev-io-wm-field" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][watermark][enabled]" value="1" /> <?php esc_html_e( 'Remove a bottom-right watermark.', 'bw-dev' ); ?></label></td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Box size (% of image)', 'bw-dev' ); ?></th>
								<td>
									<label style="margin-right:14px;"><?php esc_html_e( 'Width', 'bw-dev' ); ?> <input type="number" min="1" max="50" class="small-text bw-dev-io-wm-field" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][watermark][w_pct]" value="<?php echo (int) $tpl['watermark']['w_pct']; ?>" />%</label>
									<label><?php esc_html_e( 'Height', 'bw-dev' ); ?> <input type="number" min="1" max="50" class="small-text bw-dev-io-wm-field" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][watermark][h_pct]" value="<?php echo (int) $tpl['watermark']['h_pct']; ?>" />%</label>
								</td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Sample', 'bw-dev' ); ?></th>
								<td>
									<label style="margin-right:14px;"><input type="radio" class="bw-dev-io-wm-field" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][watermark][sample]" value="outside" checked /> <?php esc_html_e( 'Outside', 'bw-dev' ); ?></label>
									<label><input type="radio" class="bw-dev-io-wm-field" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][watermark][sample]" value="inside" /> <?php esc_html_e( 'Inside', 'bw-dev' ); ?></label>
								</td></tr>
								<tr><th scope="row"><?php esc_html_e( 'Noise', 'bw-dev' ); ?></th>
								<td><input type="number" min="0" max="100" class="small-text bw-dev-io-wm-field" name="<?php echo esc_attr( $name_prefix ); ?>[profiles][__SLUG__][watermark][noise]" value="0" />%</td></tr>
							</tbody>
						</table>
						<div class="bw-dev-io-profile-meta">
							<label><input type="radio" name="<?php echo esc_attr( $name_prefix ); ?>[default_profile]" value="__SLUG__" /> <?php esc_html_e( 'Use as the default profile', 'bw-dev' ); ?></label>
							&nbsp;&nbsp;
							<button type="button" class="button-link bw-dev-io-delete bw-dev-io-delete-btn" data-slug="__SLUG__"><?php esc_html_e( 'Delete this profile', 'bw-dev' ); ?></button>
						</div>
					</div>
				</div>
			</div>
		</template>

		<script>
		(function ($) {
			'use strict';
			var ajaxUrl = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
			var nonce   = <?php echo wp_json_encode( wp_create_nonce( self::NONCE_PREVIEW ) ); ?>;
			var action  = <?php echo wp_json_encode( self::PREVIEW_ACTION ); ?>;
			var namePrefix = <?php echo wp_json_encode( $name_prefix ); ?>;
			var deletedField = namePrefix + '[__deleted_profiles][]';

			var $tabsRoot = $('#bw-dev-io-tabs');
			var $addBtn   = $('#bw-dev-io-add-profile');
			var $deletedBin = $('#bw-dev-io-deleted-bin');
			var $previewCol; // moved between panes
			var frame;
			var lastAjax;

			function activeSlug() {
				return $tabsRoot.find('.bw-dev-io-tab.active').not('.add').data('target');
			}

			function gatherProfile() {
				var slug = activeSlug();
				if (!slug) { return null; }
				var $pane = $('#bw-dev-io-pane-' + slug.toString().replace(/[^a-zA-Z0-9_-]/g, ''));
				if (!$pane.length) { $pane = $('.bw-dev-io-profile-pane.active'); }
				function intVal(sel, def) { var v = parseInt($pane.find(sel).val(), 10); return isNaN(v) ? def : v; }
				return {
					label:      $pane.find('.bw-dev-io-label-input').val() || '',
					max_width:  intVal('input[name$="[max_width]"]', 1920),
					max_height: intVal('input[name$="[max_height]"]', 1080),
					mode:       $pane.find('input[name$="[mode]"]:checked').val() || 'fit',
					anchor:     $pane.find('select[name$="[anchor]"]').val() || 'center',
					quality:    intVal('input[name$="[quality]"]', 82),
					crop: {
						left:   intVal('input[name$="[crop][left]"]', 0),
						top:    intVal('input[name$="[crop][top]"]', 0),
						right:  intVal('input[name$="[crop][right]"]', 0),
						bottom: intVal('input[name$="[crop][bottom]"]', 0)
					},
					watermark: {
						enabled: $pane.find('input[name$="[watermark][enabled]"]').is(':checked') ? 1 : 0,
						w_pct:   intVal('input[name$="[watermark][w_pct]"]', 11),
						h_pct:   intVal('input[name$="[watermark][h_pct]"]', 11),
						sample:  $pane.find('input[name$="[watermark][sample]"]:checked').val() || 'outside',
						noise:   intVal('input[name$="[watermark][noise]"]', 0)
					}
				};
			}

			function setPreviewStale(stale) {
				$('#bw-dev-io-preview-stage').toggleClass('bw-dev-io-stale', !!stale);
				var $status = $('#bw-dev-io-preview-status');
				if (stale && $('#bw-dev-io-preview-after').attr('src')) {
					$status.text('settings changed — click Update preview');
				}
			}

			function setPreviewBusy(busy) {
				var $btn = $('#bw-dev-io-preview-update');
				$btn.prop('disabled', busy);
				$btn.find('.bw-dev-io-preview-spinner').toggle(busy);
				$btn.find('.bw-dev-io-preview-btn-label').text(busy ? 'Processing…' : 'Update preview');
			}

			function refreshPreview() {
				var $id = $('#bw-dev-io-preview-id');
				if (!$id.length) { return; }
				var id = parseInt($id.val(), 10) || 0;
				if (!id) { return; }
				var $status = $('#bw-dev-io-preview-status');
				var $after  = $('#bw-dev-io-preview-after');
				var profile = gatherProfile();
				if (!profile) { return; }

				setPreviewBusy(true);
				$status.text('');
				if (lastAjax && lastAjax.readyState !== 4) { lastAjax.abort(); }

				var started = Date.now();
				lastAjax = $.ajax({
					url: ajaxUrl, type: 'POST', dataType: 'json',
					data: { action: action, _ajax_nonce: nonce, attachment_id: id, profile: profile },
					success: function (resp) {
						if (resp && resp.success && resp.data && resp.data.data_url) {
							$after.attr('src', resp.data.data_url).css('opacity', 1);
							var ms = Date.now() - started;
							$status.text('Done in ' + (ms / 1000).toFixed(1) + 's');
							setPreviewStale(false);
						} else {
							$status.text((resp && resp.data && resp.data.message) || 'Preview failed');
						}
					},
					error: function (jq, status) { if (status !== 'abort') { $status.text('Preview request failed'); } },
					complete: function () { setPreviewBusy(false); }
				});
			}

			function activateTab(slug) {
				$tabsRoot.find('.bw-dev-io-tab').removeClass('active');
				$tabsRoot.find('.bw-dev-io-tab[data-target="' + slug + '"]').addClass('active');

				$('.bw-dev-io-profile-pane').removeClass('active');
				var $targetPane = $('#bw-dev-io-pane-' + slug.toString().replace(/[^a-zA-Z0-9_-]/g, ''));
				$targetPane.addClass('active');

				// Relocate the preview column into the new active pane.
				if (!$previewCol) { $previewCol = $('#bw-dev-io-preview-col'); }
				if ($previewCol && $previewCol.length) {
					$targetPane.find('.bw-dev-io-layout').append($previewCol);
				}

				updateDeleteButtons();
				// Tab switched — preview reflects the old pane's settings,
				// so mark stale until the user clicks Update.
				setPreviewStale(true);
			}

			function updateDeleteButtons() {
				var panes = $('.bw-dev-io-profile-pane').filter(function () { return $(this).css('display') !== 'none' || $(this).hasClass('active'); });
				// Count panes not marked-for-delete (presence in DOM is enough — JS removes deleted ones).
				var liveCount = $('.bw-dev-io-profile-pane').length;
				$('.bw-dev-io-delete-btn').prop('disabled', liveCount <= 1).attr('title', liveCount <= 1 ? 'At least one profile must exist' : '');
			}

			// Tab clicks
			$tabsRoot.on('click', '.bw-dev-io-tab:not(.add)', function () {
				activateTab($(this).data('target'));
			});

			// Add profile
			$addBtn.on('click', function () {
				var newSlug = '_new_' + Date.now();
				var tpl = $('#bw-dev-io-profile-template').html().replace(/__SLUG__/g, newSlug);
				$(tpl).insertBefore($('#bw-dev-io-deleted-bin'));
				// Default name from "New profile 1, 2, 3..."
				var n = $tabsRoot.find('.bw-dev-io-tab').not('.add').length + 1;
				var newName = 'New profile ' + n;
				$('#bw-dev-io-pane-' + newSlug).find('.bw-dev-io-label-input').val(newName);
				// Insert new tab before the [+ Add] button
				$('<button type="button" class="bw-dev-io-tab" data-target="' + newSlug + '">' + newName + '</button>')
					.insertBefore($addBtn);
				activateTab(newSlug);
			});

			// Delete profile
			$(document).on('click', '.bw-dev-io-delete-btn', function () {
				if ($(this).prop('disabled')) { return; }
				var slug = $(this).data('slug');
				var $pane = $('#bw-dev-io-pane-' + slug.toString().replace(/[^a-zA-Z0-9_-]/g, ''));
				var name  = $pane.find('.bw-dev-io-label-input').val() || slug;
				if (!window.confirm('Delete "' + name + '"? This will be applied when you save the page.')) { return; }
				// Existing (non-temp) profiles need a hidden __deleted_profiles[] input on submit.
				if (slug.toString().indexOf('_new_') !== 0) {
					$deletedBin.append('<input type="hidden" name="' + deletedField + '" value="' + slug + '">');
				}
				$pane.remove();
				$tabsRoot.find('.bw-dev-io-tab[data-target="' + slug + '"]').remove();
				// Activate first remaining tab.
				var $first = $tabsRoot.find('.bw-dev-io-tab').not('.add').first();
				if ($first.length) { activateTab($first.data('target')); }
				updateDeleteButtons();
			});

			// Live-rename: typing in the profile name updates the tab label.
			$(document).on('input', '.bw-dev-io-label-input', function () {
				var slug = $(this).data('slug');
				var val = $(this).val() || '(untitled)';
				$tabsRoot.find('.bw-dev-io-tab[data-target="' + slug + '"]').text(val);
			});

			// Sample image picker
			$('#bw-dev-io-preview-pick').on('click', function (e) {
				e.preventDefault();
				if (frame) { frame.open(); return; }
				frame = wp.media({
					title:    <?php echo wp_json_encode( __( 'Choose preview sample image', 'bw-dev' ) ); ?>,
					button:   { text: <?php echo wp_json_encode( __( 'Use this image', 'bw-dev' ) ); ?> },
					library:  { type: 'image' },
					multiple: false
				});
				frame.on('select', function () {
					var att = frame.state().get('selection').first().toJSON();
					$('#bw-dev-io-preview-id').val(att.id);
					var src = (att.sizes && att.sizes.large) ? att.sizes.large.url : att.url;
					$('#bw-dev-io-preview-before').attr('src', src);
					$('#bw-dev-io-preview-after').attr('src', '');
					$('#bw-dev-io-preview-stage').show();
					$('#bw-dev-io-preview-clear').show();
					$('#bw-dev-io-preview-update').prop('disabled', false);
					setPreviewStale(true);
					refreshPreview(); // initial render on pick is fine — they expect it
				});
				frame.open();
			});

			$(document).on('click', '#bw-dev-io-preview-clear', function (e) {
				e.preventDefault();
				$('#bw-dev-io-preview-id').val(0);
				$('#bw-dev-io-preview-before, #bw-dev-io-preview-after').attr('src', '');
				$('#bw-dev-io-preview-stage').hide();
				$('#bw-dev-io-preview-update').prop('disabled', true);
				$(this).hide();
			});

			// Manual preview update — user clicks the button.
			$(document).on('click', '#bw-dev-io-preview-update', function (e) {
				e.preventDefault();
				if ($(this).prop('disabled')) { return; }
				refreshPreview();
			});

			// Field changes mark the preview as stale. NO auto-refresh —
			// preview is expensive on slow hosts (Imagick on Flywheel can be
			// several seconds per render), so we wait for the explicit click.
			var staleSelectors = [
				'input[name$="[max_width]"]',
				'input[name$="[max_height]"]',
				'input[name$="[mode]"]',
				'select[name$="[anchor]"]',
				'input[name$="[quality]"]',
				'input[name$="[crop][left]"]',
				'input[name$="[crop][top]"]',
				'input[name$="[crop][right]"]',
				'input[name$="[crop][bottom]"]',
				'.bw-dev-io-wm-field'
			].join(', ');
			$(document).on('change input', staleSelectors, function () { setPreviewStale(true); });

			// Initial state on page load: render once if a sample is set so the
			// user sees the current settings reflected; subsequent updates wait
			// for explicit button clicks.
			updateDeleteButtons();
			if (parseInt($('#bw-dev-io-preview-id').val(), 10) > 0) { refreshPreview(); }
		})(jQuery);
		</script>

		<h2><?php esc_html_e( 'Lifetime savings', 'bw-dev' ); ?></h2>
		<table class="form-table" role="presentation">
			<tbody>
				<tr>
					<th scope="row"><?php esc_html_e( 'Images processed', 'bw-dev' ); ?></th>
					<td><?php echo esc_html( number_format_i18n( (int) ( $stats['images_processed'] ?? 0 ) ) ); ?></td>
				</tr>
				<tr>
					<th scope="row"><?php esc_html_e( 'Bytes saved', 'bw-dev' ); ?></th>
					<td>
						<?php echo esc_html( size_format( (int) ( $stats['bytes_saved'] ?? 0 ), 2 ) ); ?>
						<label style="margin-left:20px;">
							<input type="checkbox" name="<?php echo esc_attr( $name_prefix . '[reset_stats]' ); ?>" value="1" />
							<?php esc_html_e( 'Reset on save', 'bw-dev' ); ?>
						</label>
					</td>
				</tr>
			</tbody>
		</table>
		<?php
	}
}
