<?php
/**
 * Livada SEO — Image SEO Bulk Editor
 *
 * Lists every WP attachment whose alt-text is empty (or equal to the filename)
 * and lets the operator fill it in inline. An optional "Generate with AI"
 * button calls a vision-capable LLM (Anthropic Claude by default) and pre-fills
 * the field with a suggested alt.
 *
 * The Anthropic API key is stored under `livada_seo_settings.anthropic_api_key`.
 * For modest defence-in-depth we encrypt it at rest with a key derived from
 * `wp_salt('auth')`. Anyone with DB + filesystem access can still recover it,
 * which is the same threat model as any other WP secret.
 *
 * @package Livada_SEO
 */

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

class Livada_SEO_Image_Bulk {

	const PER_PAGE = 50;

	const AI_MODEL = 'claude-sonnet-4-5-20250929';

	// ── Bootstrap ─────────────────────────────────────────────────────────────

	public static function init(): void {
		add_action( 'admin_menu', [ self::class, 'register_menu' ], 22 );

		add_action( 'wp_ajax_livada_seo_bulk_save_alt',     [ self::class, 'ajax_save_alt' ] );
		add_action( 'wp_ajax_livada_seo_bulk_apply_alt',    [ self::class, 'ajax_apply_alt' ] );
		add_action( 'wp_ajax_livada_seo_ai_generate_alt',   [ self::class, 'ajax_ai_alt' ] );
	}

	public static function register_menu(): void {
		add_submenu_page(
			'livada-seo',
			__( 'Image SEO — Livada SEO', 'livada-seo' ),
			__( 'Image SEO', 'livada-seo' ),
			'manage_options',
			'livada-seo-images',
			[ self::class, 'render_page' ]
		);
	}

	// ── Public helpers (used by REST + MCP) ──────────────────────────────────

	/**
	 * Returns the paginated list of attachments missing alt-text or whose
	 * alt-text is identical to the filename.
	 *
	 * @return array{items:array<int,array<string,mixed>>,total:int}
	 */
	public static function get_attachments_missing_alt( int $page = 1, int $per_page = 50, string $search = '' ): array {
		$page     = max( 1, $page );
		$per_page = max( 1, min( 200, $per_page ) );

		$args = [
			'post_type'      => 'attachment',
			'post_status'    => 'inherit',
			'post_mime_type' => 'image',
			'posts_per_page' => $per_page,
			'paged'          => $page,
			'orderby'        => 'date',
			'order'          => 'DESC',
			'meta_query'     => [
				'relation' => 'OR',
				[ 'key' => '_wp_attachment_image_alt', 'compare' => 'NOT EXISTS' ],
				[ 'key' => '_wp_attachment_image_alt', 'value' => '', 'compare' => '=' ],
			],
		];

		if ( '' !== $search ) {
			$args['s'] = $search;
		}

		$query = new WP_Query( $args );

		$items = [];
		foreach ( $query->posts as $post ) {
			$alt = (string) get_post_meta( $post->ID, '_wp_attachment_image_alt', true );

			$items[] = [
				'id'        => (int) $post->ID,
				'filename'  => basename( (string) get_attached_file( $post->ID ) ),
				'title'     => (string) $post->post_title,
				'alt'       => $alt,
				'thumb'     => (string) wp_get_attachment_image_url( $post->ID, 'thumbnail' ),
				'full'      => (string) wp_get_attachment_image_url( $post->ID, 'full' ),
				'edit_url'  => (string) get_edit_post_link( $post->ID, 'raw' ),
				'mime'      => (string) $post->post_mime_type,
			];
		}

		wp_reset_postdata();

		return [
			'items' => $items,
			'total' => (int) $query->found_posts,
			'pages' => (int) $query->max_num_pages,
		];
	}

	/**
	 * Updates the alt-text of an attachment.
	 *
	 * @return true|WP_Error
	 */
	public static function set_alt( int $attachment_id, string $alt ) {
		$attachment_id = absint( $attachment_id );

		if ( $attachment_id <= 0 || ! current_user_can( 'edit_post', $attachment_id ) ) {
			return new WP_Error( 'forbidden', __( 'Not allowed to edit this attachment.', 'livada-seo' ), [ 'status' => 403 ] );
		}

		$post = get_post( $attachment_id );
		if ( ! $post || 'attachment' !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Attachment not found.', 'livada-seo' ), [ 'status' => 404 ] );
		}

		$alt = sanitize_text_field( $alt );

		if ( '' === $alt ) {
			delete_post_meta( $attachment_id, '_wp_attachment_image_alt' );
		} else {
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt );
		}

		return true;
	}

	/**
	 * Generates an alt-text suggestion for an attachment using a vision-capable
	 * LLM. Returns a string on success, WP_Error on failure.
	 *
	 * @param int    $attachment_id
	 * @param string $context Optional sentence to bias the description (page topic,
	 *                        location, season, …).
	 *
	 * @return string|WP_Error
	 */
	public static function generate_alt_with_ai( int $attachment_id, string $context = '' ) {
		$attachment_id = absint( $attachment_id );
		$post = get_post( $attachment_id );

		if ( ! $post || 'attachment' !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Attachment not found.', 'livada-seo' ), [ 'status' => 404 ] );
		}

		$image_url = (string) wp_get_attachment_image_url( $attachment_id, 'large' );
		if ( '' === $image_url ) {
			return new WP_Error( 'no_image_url', __( 'No image URL available.', 'livada-seo' ), [ 'status' => 400 ] );
		}

		$provider = (string) apply_filters( 'livada_seo_image_bulk_ai_provider', 'anthropic' );

		if ( 'anthropic' !== $provider ) {
			return new WP_Error(
				'unsupported_provider',
				sprintf(
					/* translators: %s: provider slug */
					__( 'AI provider "%s" is not supported by the built-in handler. Wire it up via the livada_seo_image_bulk_ai_alt filter.', 'livada-seo' ),
					$provider
				),
				[ 'status' => 501 ]
			);
		}

		$api_key = self::get_anthropic_key();
		if ( '' === $api_key ) {
			return new WP_Error(
				'missing_api_key',
				__( 'Anthropic API key is not configured. Add it under General settings.', 'livada-seo' ),
				[ 'status' => 412 ]
			);
		}

		$prompt = sprintf(
			"You write concise, accessible HTML alt attributes for images embedded in a website.\n"
			. "Respond with the alt text only — no quotes, no preamble, no trailing period.\n"
			. "Hard limit: 125 characters. Plain text. Match the page's likely language.\n"
			. "%sDescribe the image in a useful way for blind users and search engines.",
			'' !== $context ? "Context: " . $context . "\n\n" : ''
		);

		$body = [
			'model'      => self::AI_MODEL,
			'max_tokens' => 120,
			'messages'   => [
				[
					'role'    => 'user',
					'content' => [
						[
							'type'   => 'image',
							'source' => [
								'type' => 'url',
								'url'  => $image_url,
							],
						],
						[
							'type' => 'text',
							'text' => $prompt,
						],
					],
				],
			],
		];

		$response = wp_remote_post(
			'https://api.anthropic.com/v1/messages',
			[
				'timeout' => 30,
				'headers' => [
					'x-api-key'         => $api_key,
					'anthropic-version' => '2023-06-01',
					'content-type'      => 'application/json',
				],
				'body'    => wp_json_encode( $body ),
			]
		);

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

		$code = (int) wp_remote_retrieve_response_code( $response );
		$raw  = (string) wp_remote_retrieve_body( $response );
		$json = json_decode( $raw, true );

		if ( $code >= 400 ) {
			$message = is_array( $json ) && isset( $json['error']['message'] )
				? (string) $json['error']['message']
				: 'HTTP ' . $code;
			return new WP_Error( 'ai_http_error', $message, [ 'status' => $code ] );
		}

		// Anthropic returns { content: [ { type: 'text', text: '...' }, ... ] }.
		$alt = '';
		if ( is_array( $json ) && isset( $json['content'] ) && is_array( $json['content'] ) ) {
			foreach ( $json['content'] as $block ) {
				if ( isset( $block['type'], $block['text'] ) && 'text' === $block['type'] ) {
					$alt .= (string) $block['text'];
				}
			}
		}

		$alt = trim( $alt );
		// Strip any wrapping quotes the model might still produce.
		$alt = trim( $alt, " \t\n\r\0\x0B\"'`" );
		$alt = preg_replace( '/\s+/u', ' ', $alt ) ?? $alt;
		// Hard cap at 125 chars to match the prompt.
		$alt = mb_substr( $alt, 0, 125 );

		if ( '' === $alt ) {
			return new WP_Error( 'empty_response', __( 'AI returned an empty response.', 'livada-seo' ), [ 'status' => 502 ] );
		}

		return $alt;
	}

	// ── Anthropic key storage (light obfuscation, not strong encryption) ─────

	/**
	 * Returns the decrypted Anthropic API key, or '' when unset.
	 */
	public static function get_anthropic_key(): string {
		$stored = (string) Livada_SEO_Loader::get_setting( 'anthropic_api_key', '' );
		if ( '' === $stored ) {
			return '';
		}

		// Plaintext (legacy) values are returned untouched.
		if ( 0 !== strpos( $stored, 'enc:' ) ) {
			return $stored;
		}

		return self::decrypt( substr( $stored, 4 ) );
	}

	/**
	 * Encrypts a plaintext API key for storage in the settings option.
	 * Returns the storage representation including the `enc:` prefix.
	 */
	public static function encrypt_key_for_storage( string $plain ): string {
		if ( '' === $plain ) {
			return '';
		}
		return 'enc:' . self::encrypt( $plain );
	}

	private static function derive_key(): string {
		// 32 raw bytes derived from wp_salt('auth') so the key never lands in
		// the same row as the ciphertext.
		return substr( hash( 'sha256', wp_salt( 'auth' ) . 'livada_seo_image_bulk', true ), 0, 32 );
	}

	private static function encrypt( string $plain ): string {
		if ( ! function_exists( 'openssl_encrypt' ) ) {
			return base64_encode( $plain );
		}
		$iv  = random_bytes( 16 );
		$ct  = openssl_encrypt( $plain, 'AES-256-CBC', self::derive_key(), OPENSSL_RAW_DATA, $iv );
		if ( false === $ct ) {
			return base64_encode( $plain );
		}
		return base64_encode( $iv . $ct );
	}

	private static function decrypt( string $encoded ): string {
		$bin = base64_decode( $encoded, true );
		if ( false === $bin || strlen( $bin ) <= 16 ) {
			return '';
		}
		if ( ! function_exists( 'openssl_decrypt' ) ) {
			return '';
		}
		$iv  = substr( $bin, 0, 16 );
		$ct  = substr( $bin, 16 );
		$out = openssl_decrypt( $ct, 'AES-256-CBC', self::derive_key(), OPENSSL_RAW_DATA, $iv );
		return false === $out ? '' : (string) $out;
	}

	// ── AJAX ─────────────────────────────────────────────────────────────────

	public static function ajax_save_alt(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ], 403 );
		}

		$id  = isset( $_POST['id'] )  ? (int) $_POST['id'] : 0;
		$alt = isset( $_POST['alt'] ) ? wp_unslash( (string) $_POST['alt'] ) : '';

		$result = self::set_alt( $id, $alt );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( [ 'message' => $result->get_error_message() ], (int) ( $result->get_error_data()['status'] ?? 400 ) );
		}

		wp_send_json_success( [ 'id' => $id, 'alt' => sanitize_text_field( $alt ) ] );
	}

	public static function ajax_apply_alt(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ], 403 );
		}

		$ids = isset( $_POST['ids'] ) ? (array) $_POST['ids'] : [];
		$ids = array_values( array_filter( array_map( 'absint', $ids ) ) );
		$alt = isset( $_POST['alt'] ) ? wp_unslash( (string) $_POST['alt'] ) : '';

		if ( empty( $ids ) || '' === trim( $alt ) ) {
			wp_send_json_error( [ 'message' => __( 'Select at least one image and provide an alt text.', 'livada-seo' ) ], 400 );
		}

		$updated = 0;
		foreach ( $ids as $id ) {
			$res = self::set_alt( $id, $alt );
			if ( ! is_wp_error( $res ) ) {
				$updated++;
			}
		}

		wp_send_json_success( [ 'updated' => $updated ] );
	}

	public static function ajax_ai_alt(): void {
		check_ajax_referer( 'livada_seo_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'livada-seo' ) ], 403 );
		}

		$id      = isset( $_POST['id'] )      ? (int) $_POST['id'] : 0;
		$context = isset( $_POST['context'] ) ? sanitize_textarea_field( wp_unslash( (string) $_POST['context'] ) ) : '';

		$alt = self::generate_alt_with_ai( $id, $context );

		if ( is_wp_error( $alt ) ) {
			wp_send_json_error(
				[ 'message' => $alt->get_error_message() ],
				(int) ( $alt->get_error_data()['status'] ?? 500 )
			);
		}

		wp_send_json_success( [ 'id' => $id, 'alt' => (string) $alt ] );
	}

	// ── Admin page ───────────────────────────────────────────────────────────

	public static function render_page(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'livada-seo' ) );
		}

		$page   = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1; // phpcs:ignore
		$search = isset( $_GET['s'] )     ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore

		$result = self::get_attachments_missing_alt( $page, self::PER_PAGE, $search );
		$items  = $result['items'];
		$total  = $result['total'];
		$pages  = $result['pages'];

		$has_ai = '' !== self::get_anthropic_key();
		?>
		<div class="wrap livada-seo-wrap">
			<h1><?php esc_html_e( 'Image SEO', 'livada-seo' ); ?></h1>
			<p class="description">
				<?php
				printf(
					/* translators: %d: count */
					esc_html( _n( '%d image without alt text.', '%d images without alt text.', $total, 'livada-seo' ) ),
					(int) $total
				);
				?>
				<?php if ( ! $has_ai ) : ?>
					· <?php
					printf(
						/* translators: %s: settings page link */
						wp_kses(
							__( 'Add an Anthropic API key in <a href="%s">General settings</a> to enable one-click AI alt-text suggestions.', 'livada-seo' ),
							[ 'a' => [ 'href' => [] ] ]
						),
						esc_url( admin_url( 'admin.php?page=livada-seo-general' ) )
					);
				?>
				<?php endif; ?>
			</p>

			<form method="get" style="margin: 12px 0;">
				<input type="hidden" name="page" value="livada-seo-images">
				<input type="search" name="s" value="<?php echo esc_attr( $search ); ?>" placeholder="<?php esc_attr_e( 'Search by filename or title…', 'livada-seo' ); ?>">
				<button type="submit" class="button"><?php esc_html_e( 'Filter', 'livada-seo' ); ?></button>
			</form>

			<div class="livada-bulk-toolbar" style="display:flex; gap:8px; align-items:center; margin: 8px 0; flex-wrap:wrap;">
				<input type="text" id="livada-img-bulk-alt" placeholder="<?php esc_attr_e( 'Apply same alt to selected…', 'livada-seo' ); ?>" style="flex:1; min-width:240px; max-width:520px;">
				<button type="button" id="livada-img-bulk-apply" class="button" disabled>
					<?php esc_html_e( 'Apply to selection', 'livada-seo' ); ?>
				</button>
				<?php if ( $has_ai ) : ?>
					<button type="button" id="livada-img-ai-all" class="button button-primary">
						<?php esc_html_e( 'AI ✨ fill empty alts on this page', 'livada-seo' ); ?>
					</button>
				<?php endif; ?>
				<span id="livada-img-bulk-status" class="description"></span>
			</div>

			<table class="wp-list-table widefat fixed striped">
				<thead>
					<tr>
						<th style="width: 36px;"><input type="checkbox" id="livada-img-cb-all"></th>
						<th style="width: 110px;"><?php esc_html_e( 'Image', 'livada-seo' ); ?></th>
						<th style="width: 22%;"><?php esc_html_e( 'File', 'livada-seo' ); ?></th>
						<th><?php esc_html_e( 'Alt text', 'livada-seo' ); ?></th>
						<th style="width: 220px;"><?php esc_html_e( 'Actions', 'livada-seo' ); ?></th>
					</tr>
				</thead>
				<tbody id="livada-img-tbody">
					<?php if ( empty( $items ) ) : ?>
						<tr><td colspan="5"><?php esc_html_e( 'All images have alt text. ✨', 'livada-seo' ); ?></td></tr>
					<?php else : ?>
						<?php foreach ( $items as $img ) : ?>
							<tr data-id="<?php echo (int) $img['id']; ?>">
								<td><input type="checkbox" class="livada-img-cb" value="<?php echo (int) $img['id']; ?>"></td>
								<td>
									<?php if ( '' !== $img['thumb'] ) : ?>
										<a href="<?php echo esc_url( $img['full'] ); ?>" target="_blank" rel="noopener noreferrer">
											<img src="<?php echo esc_url( $img['thumb'] ); ?>" alt="" style="max-width:96px; height:auto; border:1px solid #c3c4c7;">
										</a>
									<?php endif; ?>
								</td>
								<td>
									<strong><?php echo esc_html( $img['filename'] ); ?></strong>
									<?php if ( '' !== $img['title'] && $img['title'] !== $img['filename'] ) : ?>
										<br><small><?php echo esc_html( $img['title'] ); ?></small>
									<?php endif; ?>
								</td>
								<td>
									<input type="text" class="livada-img-alt" value="<?php echo esc_attr( $img['alt'] ); ?>" maxlength="160" style="width: 100%;">
								</td>
								<td>
									<button type="button" class="button button-small livada-img-save"><?php esc_html_e( 'Save', 'livada-seo' ); ?></button>
									<?php if ( $has_ai ) : ?>
										<button type="button" class="button button-small livada-img-ai"><?php esc_html_e( 'AI', 'livada-seo' ); ?></button>
									<?php endif; ?>
									<a class="button-link" href="<?php echo esc_url( $img['edit_url'] ); ?>"><?php esc_html_e( 'Edit', 'livada-seo' ); ?></a>
								</td>
							</tr>
						<?php endforeach; ?>
					<?php endif; ?>
				</tbody>
			</table>

			<?php if ( $pages > 1 ) : ?>
				<div class="tablenav" style="margin-top: 16px;">
					<?php
					$base = add_query_arg( [ 'page' => 'livada-seo-images', 's' => $search ], admin_url( 'admin.php' ) );
					echo wp_kses_post( paginate_links( [
						'base'    => add_query_arg( 'paged', '%#%', $base ),
						'format'  => '',
						'current' => $page,
						'total'   => $pages,
					] ) );
					?>
				</div>
			<?php endif; ?>
		</div>

		<script>
		(function () {
			'use strict';

			var data    = window.livadaSEO || {};
			var ajaxUrl = data.ajaxUrl || '<?php echo esc_js( admin_url( 'admin-ajax.php' ) ); ?>';
			var nonce   = data.nonce   || '<?php echo esc_js( wp_create_nonce( 'livada_seo_admin_nonce' ) ); ?>';

			function post(action, payload) {
				var body = new URLSearchParams();
				body.append('action', action);
				body.append('nonce',  nonce);
				Object.keys(payload).forEach(function (k) {
					var v = payload[k];
					if (Array.isArray(v)) {
						v.forEach(function (i) { body.append(k + '[]', i); });
					} else {
						body.append(k, v);
					}
				});
				return fetch(ajaxUrl, {
					method:      'POST',
					credentials: 'same-origin',
					headers:     { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
					body:        body.toString(),
				}).then(function (r) { return r.json(); });
			}

			var tbody = document.getElementById('livada-img-tbody');
			if (!tbody) return;

			tbody.addEventListener('click', function (e) {
				var btn = e.target.closest('button');
				if (!btn) return;
				var row = btn.closest('tr[data-id]');
				if (!row) return;
				var id  = parseInt(row.getAttribute('data-id'), 10);
				var inp = row.querySelector('.livada-img-alt');
				if (!inp) return;

				if (btn.classList.contains('livada-img-save')) {
					btn.disabled = true;
					inp.style.borderColor = '#dba617';
					post('livada_seo_bulk_save_alt', { id: id, alt: inp.value })
						.then(function (res) {
							btn.disabled = false;
							inp.style.borderColor = res && res.success ? '#00a32a' : '#d63638';
							setTimeout(function () { inp.style.borderColor = ''; }, 1200);
						})
						.catch(function () { btn.disabled = false; inp.style.borderColor = '#d63638'; });
				} else if (btn.classList.contains('livada-img-ai')) {
					btn.disabled = true;
					var oldText = btn.textContent;
					btn.textContent = '…';
					post('livada_seo_ai_generate_alt', { id: id, context: document.title })
						.then(function (res) {
							btn.disabled = false;
							btn.textContent = oldText;
							if (res && res.success && res.data && res.data.alt) {
								inp.value = res.data.alt;
								inp.focus();
								inp.style.borderColor = '#2271b1';
								setTimeout(function () { inp.style.borderColor = ''; }, 1500);
							} else {
								alert((res && res.data && res.data.message) || 'AI generation failed.');
							}
						})
						.catch(function () { btn.disabled = false; btn.textContent = oldText; });
				}
			});

			// Bulk apply.
			var checkAll  = document.getElementById('livada-img-cb-all');
			var bulkInput = document.getElementById('livada-img-bulk-alt');
			var bulkBtn   = document.getElementById('livada-img-bulk-apply');
			var bulkSt    = document.getElementById('livada-img-bulk-status');

			function refreshBulkBtn() {
				var n = tbody.querySelectorAll('.livada-img-cb:checked').length;
				bulkBtn.disabled = (n === 0 || bulkInput.value.trim() === '');
				bulkBtn.textContent = n > 0
					? '<?php echo esc_js( __( 'Apply to selection', 'livada-seo' ) ); ?>' + ' (' + n + ')'
					: '<?php echo esc_js( __( 'Apply to selection', 'livada-seo' ) ); ?>';
			}

			if (checkAll) {
				checkAll.addEventListener('change', function () {
					tbody.querySelectorAll('.livada-img-cb').forEach(function (cb) { cb.checked = checkAll.checked; });
					refreshBulkBtn();
				});
			}
			tbody.addEventListener('change', function (e) {
				if (e.target && e.target.classList && e.target.classList.contains('livada-img-cb')) {
					refreshBulkBtn();
				}
			});
			bulkInput.addEventListener('input', refreshBulkBtn);

			bulkBtn.addEventListener('click', function () {
				var ids = Array.prototype.slice
					.call(tbody.querySelectorAll('.livada-img-cb:checked'))
					.map(function (cb) { return parseInt(cb.value, 10); });
				if (!ids.length) return;
				bulkBtn.disabled = true;
				bulkSt.textContent = '…';
				post('livada_seo_bulk_apply_alt', { ids: ids, alt: bulkInput.value })
					.then(function (res) {
						bulkSt.textContent = (res && res.success && res.data)
							? (res.data.updated + ' updated')
							: ((res && res.data && res.data.message) || 'Failed');
						setTimeout(function () { window.location.reload(); }, 800);
					})
					.catch(function () { bulkSt.textContent = 'Failed'; bulkBtn.disabled = false; });
			});

			// ── AI fill all empty alts on the visible page ────────────────────
			var aiAllBtn = document.getElementById('livada-img-ai-all');
			if (aiAllBtn) {
				aiAllBtn.addEventListener('click', function () {
					var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr[data-id]'));
					var queue = rows.filter(function (r) {
						var inp = r.querySelector('.livada-img-alt');
						return inp && '' === inp.value.trim();
					});

					if (!queue.length) {
						bulkSt.textContent = '<?php echo esc_js( __( 'Nothing to fill — every alt is already set.', 'livada-seo' ) ); ?>';
						return;
					}

					if (!window.confirm(queue.length + ' <?php echo esc_js( __( 'images will be processed via the Anthropic API. Continue?', 'livada-seo' ) ); ?>')) {
						return;
					}

					aiAllBtn.disabled = true;
					var done = 0, ok = 0, fail = 0;

					function step(i) {
						if (i >= queue.length) {
							aiAllBtn.disabled = false;
							bulkSt.textContent = ok + ' filled, ' + fail + ' failed';
							return;
						}

						var row = queue[i];
						var id  = parseInt(row.getAttribute('data-id'), 10);
						var inp = row.querySelector('.livada-img-alt');

						bulkSt.textContent = '<?php echo esc_js( __( 'Generating', 'livada-seo' ) ); ?>… ' + (++done) + '/' + queue.length;

						post('livada_seo_ai_generate_alt', { id: id, context: document.title })
							.then(function (res) {
								if (res && res.success && res.data && res.data.alt) {
									inp.value = res.data.alt;
									// Save inline so it persists.
									return post('livada_seo_bulk_save_alt', { id: id, alt: res.data.alt })
										.then(function () { ok++; });
								} else {
									fail++;
								}
							})
							.catch(function () { fail++; })
							.then(function () {
								// 250 ms throttle.
								setTimeout(function () { step(i + 1); }, 250);
							});
					}

					step(0);
				});
			}
		})();
		</script>
		<?php
	}
}
