<?php
/**
 * Modul: Automatische Alt-Texte (KI).
 *
 * - Einstellungsseite (Lizenzschlüssel, Aktivierung, Sprache) – gated über den
 *   serv01-Proxy: ohne freigeschaltetes Add-on passiert nichts.
 * - Automatik beim Upload (asynchron via WP-Cron).
 * - Stapelverarbeitung der bestehenden Mediathek (AJAX, mit Fortschritt).
 * - Frontend-Sicherheitsnetz: füllt leere img-alt aus den Medien-Metadaten,
 *   damit auch bereits im Layout platzierte Bilder ihren Alt-Text bekommen.
 * - Editor-Back-Fill (assets/alt-backfill.js) trägt fehlende Alts in Blöcke nach.
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class WAVE_Alt_Text {

	private static $instance = null;

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

	private function __construct() {
		// Automatik beim Upload
		add_action( 'add_attachment', array( $this, 'on_add_attachment' ) );
		add_action( 'wave_generate_alt_event', array( $this, 'cron_generate' ) );

		// Frontend-Sicherheitsnetz
		add_filter( 'render_block', array( $this, 'filter_render_block' ), 20, 2 );

		// Editor-Back-Fill
		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor' ) );

		// Admin (Einstellungen werden als Tab der Hauptseite gerendert; kein eigener Menüpunkt)
		add_action( 'admin_init', array( $this, 'handle_settings_save' ) );
		add_action( 'admin_init', array( $this, 'handle_features_save' ) );
		add_action( 'wp_ajax_wave_alt_scan', array( $this, 'ajax_scan' ) );
		add_action( 'wp_ajax_wave_alt_batch', array( $this, 'ajax_batch' ) );
	}

	/* =====================================================================
	   Erzeugung
	   ===================================================================== */

	/** Beim Upload: asynchron einplanen (Upload/Editor nicht blockieren). */
	public function on_add_attachment( $attachment_id ) {
		$o = WAVE_AI_Client::get_options();
		if ( empty( $o['auto_upload'] ) ) {
			return;
		}
		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			return;
		}
		if ( ! WAVE_AI_Client::feature_enabled( 'alt_text' ) ) {
			return;
		}
		// In ~5 s im Hintergrund erzeugen.
		if ( ! wp_next_scheduled( 'wave_generate_alt_event', array( $attachment_id ) ) ) {
			wp_schedule_single_event( time() + 5, 'wave_generate_alt_event', array( $attachment_id ) );
		}
	}

	public function cron_generate( $attachment_id ) {
		$this->generate_for_attachment( (int) $attachment_id, false );
	}

	/**
	 * Alt-Text für ein Attachment erzeugen und speichern.
	 *
	 * @param bool $overwrite Vorhandenen Alt-Text überschreiben?
	 * @return array|WP_Error
	 */
	public function generate_for_attachment( $attachment_id, $overwrite = false ) {
		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			return new WP_Error( 'not_image', 'Kein Bild.' );
		}
		$current = (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
		if ( ! $overwrite && '' !== trim( $current ) ) {
			return array( 'ok' => true, 'skipped' => true, 'alt' => $current );
		}

		list( $file, $mime ) = $this->pick_image_file( $attachment_id );

		// Vom Proxy unterstützte Bildtypen: Raster (Vision) + SVG (als Quelltext).
		if ( ! in_array( $mime, array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml' ), true ) ) {
			return array( 'ok' => true, 'skipped' => true, 'reason' => 'mime' );
		}

		$filename = wp_basename( get_attached_file( $attachment_id ) );
		$context  = (string) get_the_title( $attachment_id );

		$res = WAVE_AI_Client::alt_text( $file, $mime, $filename, $context );
		if ( is_wp_error( $res ) ) {
			return $res;
		}

		$alt = isset( $res['alt'] ) ? (string) $res['alt'] : '';
		if ( ! empty( $res['decorative'] ) ) {
			// Dekorativ: leeren Alt-Text setzen + markieren.
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', '' );
			update_post_meta( $attachment_id, '_wave_alt_decorative', 1 );
		} else {
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) );
			delete_post_meta( $attachment_id, '_wave_alt_decorative' );
		}
		update_post_meta( $attachment_id, '_wave_alt_generated', time() );
		return $res;
	}

	/** Kleinere generierte Bildgröße wählen (spart Übertragung/Kosten). */
	private function pick_image_file( $attachment_id ) {
		$path = get_attached_file( $attachment_id );
		$mime = get_post_mime_type( $attachment_id );

		foreach ( array( 'large', 'medium_large', 'medium' ) as $size ) {
			$inter = image_get_intermediate_size( $attachment_id, $size );
			if ( $inter && ! empty( $inter['path'] ) ) {
				$up   = wp_get_upload_dir();
				$cand = trailingslashit( $up['basedir'] ) . $inter['path'];
				if ( file_exists( $cand ) ) {
					$path = $cand;
					$ft   = wp_check_filetype( $cand );
					if ( ! empty( $ft['type'] ) ) {
						$mime = $ft['type'];
					}
					break;
				}
			}
		}
		return array( $path, $mime );
	}

	/* =====================================================================
	   Frontend-Sicherheitsnetz
	   ===================================================================== */

	public function filter_render_block( $content, $block ) {
		if ( is_admin() || empty( $content ) ) {
			return $content;
		}
		if ( false === strpos( $content, '<img' ) ) {
			return $content;
		}
		$name = isset( $block['blockName'] ) ? $block['blockName'] : '';
		$targets = array( 'core/image', 'core/cover', 'core/media-text', 'core/gallery', 'core/post-featured-image' );
		if ( ! in_array( $name, $targets, true ) ) {
			return $content;
		}
		if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
			return $content;
		}

		$p       = new WP_HTML_Tag_Processor( $content );
		$changed = false;
		while ( $p->next_tag( 'img' ) ) {
			$alt = $p->get_attribute( 'alt' );
			if ( null !== $alt && '' !== trim( (string) $alt ) ) {
				continue; // hat bereits Alt-Text
			}
			$id = $this->img_attachment_id( $p );
			if ( ! $id ) {
				continue;
			}
			// Dekorativ markiert → leeren Alt-Text bewusst belassen.
			if ( get_post_meta( $id, '_wave_alt_decorative', true ) ) {
				continue;
			}
			$meta_alt = (string) get_post_meta( $id, '_wp_attachment_image_alt', true );
			if ( '' !== trim( $meta_alt ) ) {
				$p->set_attribute( 'alt', $meta_alt );
				$changed = true;
			}
		}
		return $changed ? $p->get_updated_html() : $content;
	}

	/** Attachment-ID aus der wp-image-XXX-Klasse des img lesen. */
	private function img_attachment_id( $p ) {
		$class = (string) $p->get_attribute( 'class' );
		if ( $class && preg_match( '/wp-image-(\d+)/', $class, $m ) ) {
			return (int) $m[1];
		}
		return 0;
	}

	/* =====================================================================
	   Editor-Back-Fill
	   ===================================================================== */

	public function enqueue_editor() {
		wp_enqueue_script(
			'wave-alt-backfill',
			WAVE_PLUGIN_URL . 'assets/alt-backfill.js',
			array( 'wp-data', 'wp-blocks', 'wp-block-editor' ),
			WAVE_VERSION,
			true
		);
	}

	/* =====================================================================
	   Admin – Einstellungsseite + Batch
	   ===================================================================== */

	public function handle_settings_save() {
		if ( ! isset( $_POST['wave_ai_save'] ) || ! current_user_can( 'manage_options' ) ) {
			return;
		}
		check_admin_referer( 'wave_ai_settings' );
		$o = WAVE_AI_Client::get_options();
		$o['license_key'] = isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '';
		$o['server_url']  = isset( $_POST['server_url'] ) ? esc_url_raw( wp_unslash( $_POST['server_url'] ) ) : $o['server_url'];
		$o['auto_upload'] = ! empty( $_POST['auto_upload'] );
		$o['lang']        = isset( $_POST['lang'] ) ? sanitize_text_field( wp_unslash( $_POST['lang'] ) ) : '';
		WAVE_AI_Client::update_options( $o );
		add_settings_error( 'wave_ai', 'saved', __( 'Einstellungen gespeichert.', 'wave-access' ), 'updated' );
	}

	/** Schalter „Weitere KI-Funktionen" (Leichte Sprache / Vorlesen) speichern. */
	public function handle_features_save() {
		if ( ! isset( $_POST['wave_ai_features_save'] ) || ! current_user_can( 'manage_options' ) ) {
			return;
		}
		check_admin_referer( 'wave_ai_features' );
		$o                  = WAVE_AI_Client::get_options();
		$o['easy_language'] = ! empty( $_POST['easy_language'] );
		$o['tts']           = ! empty( $_POST['tts'] );
		WAVE_AI_Client::update_options( $o );
		add_settings_error( 'wave_ai', 'saved_features', __( 'KI-Funktionen gespeichert.', 'wave-access' ), 'updated' );
	}

	public function render_settings_tab() {
		$o      = WAVE_AI_Client::get_options();
		$status = WAVE_AI_Client::status( true ); // beim Öffnen frisch prüfen
		$alt_ok = ! empty( $status['ok'] ) && ! empty( $status['features'] ) && in_array( 'alt_text', (array) $status['features'], true );
		settings_errors( 'wave_ai' );
		?>
		<div class="wave-card is-pro">
			<div class="wave-card-head">
				<span class="wave-card-ic"><?php echo wave_icon( 'key', 20 ); ?></span>
				<div>
					<h2 class="wave-card-title"><?php esc_html_e( 'Lizenz & Status', 'wave-access' ); ?> <span class="wave-pill wave-pill-pro"><?php echo wave_icon( 'sparkles', 10 ); ?>Pro</span></h2>
					<p class="wave-card-desc"><?php esc_html_e( 'Die KI-Funktionen gehören zur Pro-Version und werden über deinen Lizenzschlüssel freigeschaltet.', 'wave-access' ); ?></p>
				</div>
			</div>

			<?php if ( ! empty( $status['ok'] ) ) : ?>
				<div class="wave-status wave-status-ok">
					<?php echo wave_icon( 'circle-check', 18 ); ?>
					<div>
						<b><?php echo $alt_ok ? esc_html__( 'Lizenz aktiv – Automatische Alt-Texte freigeschaltet', 'wave-access' ) : esc_html__( 'Lizenz aktiv', 'wave-access' ); ?></b><?php echo ! empty( $status['name'] ) ? ' · ' . esc_html( $status['name'] ) : ''; ?>
						<?php if ( isset( $status['quota'] ) && (int) $status['quota'] > 0 ) : ?>
							<br /><?php printf( esc_html__( 'Verbrauch diesen Monat: %1$d von %2$d Einheiten.', 'wave-access' ), (int) $status['used'], (int) $status['quota'] ); ?>
						<?php endif; ?>
					</div>
				</div>
			<?php else : ?>
				<div class="wave-status wave-status-warn">
					<?php echo wave_icon( 'triangle-exclamation', 18 ); ?>
					<div>
						<b><?php esc_html_e( 'Keine aktive Lizenz', 'wave-access' ); ?></b><?php echo ! empty( $status['message'] ) ? ' · ' . esc_html( $status['message'] ) : ''; ?>
					</div>
				</div>
			<?php endif; ?>

			<form method="post">
				<?php wp_nonce_field( 'wave_ai_settings' ); ?>
				<input type="hidden" name="wave_ai_save" value="1" />
				<input type="hidden" name="server_url" value="<?php echo esc_attr( $o['server_url'] ); ?>" />
				<div class="wave-rows">
					<div class="wave-row">
						<div class="wave-row-main">
							<span class="wave-row-ic"><?php echo wave_icon( 'key', 18 ); ?></span>
							<div>
								<div class="wave-row-label"><?php esc_html_e( 'Lizenzschlüssel', 'wave-access' ); ?></div>
								<div class="wave-row-desc"><?php esc_html_e( 'Schaltet die KI-Funktionen frei.', 'wave-access' ); ?></div>
							</div>
						</div>
						<div class="wave-row-control">
							<?php if ( class_exists( 'Wave_License' ) && Wave_License::key() ) : ?>
								<input type="text" class="wave-field" style="width:230px" value="<?php echo esc_attr( Wave_License::key() ); ?>" readonly
									title="<?php esc_attr_e( 'Wird zentral im Lizenz-Tab verwaltet', 'wave-access' ); ?>" />
								<input type="hidden" name="license_key" value="<?php echo esc_attr( Wave_License::key() ); ?>" />
							<?php else : ?>
								<input type="text" class="wave-field" style="width:230px" name="license_key" value="<?php echo esc_attr( $o['license_key'] ); ?>" placeholder="WAVE-XXXX-XXXX-XXXX" />
							<?php endif; ?>
						</div>
					</div>
					<div class="wave-row">
						<div class="wave-row-main">
							<span class="wave-row-ic"><?php echo wave_icon( 'font', 18 ); ?></span>
							<div>
								<div class="wave-row-label"><?php esc_html_e( 'Sprache der Alt-Texte', 'wave-access' ); ?></div>
								<div class="wave-row-desc"><?php esc_html_e( 'Leer = automatisch aus der Website-Sprache.', 'wave-access' ); ?></div>
							</div>
						</div>
						<div class="wave-row-control"><input type="text" class="wave-field" style="width:80px" name="lang" value="<?php echo esc_attr( $o['lang'] ); ?>" placeholder="de" /></div>
					</div>
					<div class="wave-row">
						<div class="wave-row-main">
							<span class="wave-row-ic"><?php echo wave_icon( 'wand-magic-sparkles', 18 ); ?></span>
							<div>
								<div class="wave-row-label"><?php esc_html_e( 'Alt-Texte automatisch beim Upload', 'wave-access' ); ?> <span class="wave-pill wave-pill-pro"><?php echo wave_icon( 'sparkles', 10 ); ?>Pro</span></div>
								<div class="wave-row-desc"><?php esc_html_e( 'Neu hochgeladene Bilder werden automatisch beschrieben (asynchron).', 'wave-access' ); ?></div>
							</div>
						</div>
						<div class="wave-row-control">
							<label class="wave-switch">
								<input type="checkbox" name="auto_upload" value="1" <?php checked( ! empty( $o['auto_upload'] ) ); ?> />
								<span class="wave-slider"></span>
							</label>
						</div>
					</div>
				</div>
				<div class="wave-actions">
					<button type="submit" class="wave-btn wave-btn-primary"><?php echo wave_icon( 'circle-check', 16 ); ?> <?php esc_html_e( 'Speichern', 'wave-access' ); ?></button>
				</div>
			</form>
		</div>

		<div class="wave-card is-pro">
			<div class="wave-card-head">
				<span class="wave-card-ic"><?php echo wave_icon( 'image', 20 ); ?></span>
				<div>
					<h2 class="wave-card-title"><?php esc_html_e( 'Mediathek: Alt-Texte erzeugen', 'wave-access' ); ?> <span class="wave-pill wave-pill-pro"><?php echo wave_icon( 'sparkles', 10 ); ?>Pro</span></h2>
					<p class="wave-card-desc"><?php esc_html_e( 'Erzeugt Alt-Texte für alle Bilder ohne Alt-Text. Vorhandene bleiben unangetastet.', 'wave-access' ); ?></p>
				</div>
			</div>
			<div class="wave-actions" style="background:transparent;border-top:none;">
				<button class="wave-btn" id="wave-alt-scan" <?php disabled( ! $alt_ok ); ?>><?php echo wave_icon( 'list-check', 16 ); ?> <?php esc_html_e( 'Mediathek prüfen', 'wave-access' ); ?></button>
				<button class="wave-btn wave-btn-primary" id="wave-alt-run" style="display:none;"><?php echo wave_icon( 'wand-magic-sparkles', 16 ); ?> <?php esc_html_e( 'Jetzt erzeugen', 'wave-access' ); ?></button>
				<span id="wave-alt-info" class="wave-row-desc"></span>
			</div>
			<div id="wave-alt-progress" class="wave-progress" style="display:none;"><div id="wave-alt-bar" class="wave-progbar"></div></div>
			<div id="wave-alt-log" class="wave-log"></div>

			<?php
			// Weitere KI-Funktionen: sichtbar für alle, freigeschaltet je nach Lizenz/Server-Feature
			$easy_ok = WAVE_AI_Client::feature_enabled( 'easy_language' );
			$tts_ok  = WAVE_AI_Client::feature_enabled( 'tts' );
			?>
			<div class="wave-card is-pro">
				<div class="wave-card-head">
					<span class="wave-card-ic"><?php echo wave_icon( 'wand-magic-sparkles', 20 ); ?></span>
					<div>
						<h2 class="wave-card-title"><?php esc_html_e( 'Weitere KI-Funktionen', 'wave-access' ); ?> <span class="wave-pill wave-pill-pro"><?php echo wave_icon( 'sparkles', 10 ); ?>Pro AI</span></h2>
						<p class="wave-card-desc"><?php esc_html_e( 'Bestandteil des Pro-AI-Tarifs. Die Freischaltung erfolgt automatisch über deine Lizenz, sobald die Funktion für dein Konto verfügbar ist.', 'wave-access' ); ?></p>
					</div>
				</div>
				<form method="post">
				<?php wp_nonce_field( 'wave_ai_features' ); ?>
				<input type="hidden" name="wave_ai_features_save" value="1" />
				<div class="wave-rows">
					<div class="wave-row <?php echo $easy_ok ? '' : 'is-locked'; ?>">
						<div class="wave-row-main">
							<span class="wave-row-ic"><?php echo wave_icon( 'font', 18 ); ?></span>
							<div>
								<div class="wave-row-label"><?php esc_html_e( 'Leichte Sprache', 'wave-access' ); ?></div>
								<div class="wave-row-desc"><?php esc_html_e( 'Übersetzt Seiteninhalte per KI in Leichte Sprache – Besucher schalten im Widget um.', 'wave-access' ); ?></div>
							</div>
						</div>
						<div class="wave-row-control">
							<?php if ( $easy_ok ) : ?>
								<label class="wave-switch"><input type="checkbox" name="easy_language" value="1" <?php checked( ! empty( $o['easy_language'] ) ); ?> /><span class="wave-slider"></span></label>
							<?php else : ?>
								<span class="wave-lock"><?php echo wave_icon( 'key', 14 ); ?> <?php esc_html_e( 'Nicht freigeschaltet', 'wave-access' ); ?></span>
							<?php endif; ?>
						</div>
					</div>
					<div class="wave-row <?php echo $tts_ok ? '' : 'is-locked'; ?>">
						<div class="wave-row-main">
							<span class="wave-row-ic"><?php echo wave_icon( 'universal-access', 18 ); ?></span>
							<div>
								<div class="wave-row-label"><?php esc_html_e( 'Vorlesen mit KI-Stimmen', 'wave-access' ); ?></div>
								<div class="wave-row-desc"><?php esc_html_e( 'Natürliche Stimmen statt Browser-Synthese – direkt im Eingabehilfen-Widget.', 'wave-access' ); ?></div>
							</div>
						</div>
						<div class="wave-row-control">
							<?php if ( $tts_ok ) : ?>
								<label class="wave-switch"><input type="checkbox" name="tts" value="1" <?php checked( ! empty( $o['tts'] ) ); ?> /><span class="wave-slider"></span></label>
							<?php else : ?>
								<span class="wave-lock"><?php echo wave_icon( 'key', 14 ); ?> <?php esc_html_e( 'Nicht freigeschaltet', 'wave-access' ); ?></span>
							<?php endif; ?>
						</div>
					</div>
				</div>
				<?php if ( $easy_ok || $tts_ok ) : ?>
				<div class="wave-actions">
					<button type="submit" class="wave-btn wave-btn-primary"><?php echo wave_icon( 'circle-check', 16 ); ?> <?php esc_html_e( 'Speichern', 'wave-access' ); ?></button>
				</div>
				<?php endif; ?>
				</form>
			</div>

			<script>
			(function(){
				var nonce = <?php echo wp_json_encode( wp_create_nonce( 'wave_alt_batch' ) ); ?>;
				var ajax = <?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>;
				var total = 0, done = 0;
				var scanBtn = document.getElementById('wave-alt-scan');
				var runBtn  = document.getElementById('wave-alt-run');
				var info    = document.getElementById('wave-alt-info');
				var prog    = document.getElementById('wave-alt-progress');
				var bar     = document.getElementById('wave-alt-bar');
				var log     = document.getElementById('wave-alt-log');

				function post(action, extra){
					var body = 'action='+action+'&_wpnonce='+encodeURIComponent(nonce);
					if(extra) body += extra;
					return fetch(ajax,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body}).then(function(r){return r.json();});
				}
				if(scanBtn) scanBtn.addEventListener('click', function(e){
					e.preventDefault(); info.textContent='Prüfe…';
					post('wave_alt_scan').then(function(j){
						if(!j.success){ info.textContent = (j.data&&j.data.message)||'Fehler.'; return; }
						total = j.data.total; done = 0;
						info.textContent = total>0 ? (total+' Bilder ohne Alt-Text gefunden.') : 'Alle Bilder haben bereits einen Alt-Text.';
						runBtn.style.display = total>0 ? 'inline-block':'none';
					});
				});
				if(runBtn) runBtn.addEventListener('click', function(e){
					e.preventDefault(); runBtn.disabled=true; scanBtn.disabled=true;
					prog.style.display='block';
					step();
				});
				function step(){
					post('wave_alt_batch', '&offset='+done).then(function(j){
						if(!j.success){ log.innerHTML += '<div style="color:#b32d2e">'+((j.data&&j.data.message)||'Fehler')+'</div>'; runBtn.disabled=false; scanBtn.disabled=false; return; }
						done = j.data.done;
						(j.data.log||[]).forEach(function(l){ log.innerHTML += '<div>'+l+'</div>'; });
						log.scrollTop = log.scrollHeight;
						var pct = total>0 ? Math.round(done/total*100) : 100;
						bar.style.width = pct+'%'; bar.textContent = pct+'%';
						if(j.data.finished || done>=total){ info.textContent='Fertig: '+done+' verarbeitet.'; runBtn.disabled=false; scanBtn.disabled=false; }
						else { step(); }
					});
				}
			})();
			</script>
		</div>
		<?php
	}

	/* =====================================================================
	   AJAX – Scan + Batch
	   ===================================================================== */

	/** IDs aller Bilder ohne Alt-Text ermitteln und zwischenspeichern. */
	public function ajax_scan() {
		check_ajax_referer( 'wave_alt_batch' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Keine Berechtigung.' ) );
		}
		$ids = $this->find_missing_alt_ids();
		set_transient( 'wave_alt_candidates', $ids, 2 * HOUR_IN_SECONDS );
		wp_send_json_success( array( 'total' => count( $ids ) ) );
	}

	public function ajax_batch() {
		check_ajax_referer( 'wave_alt_batch' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'Keine Berechtigung.' ) );
		}
		if ( ! WAVE_AI_Client::feature_enabled( 'alt_text' ) ) {
			wp_send_json_error( array( 'message' => 'Add-on nicht freigeschaltet.' ) );
		}
		$ids    = get_transient( 'wave_alt_candidates' );
		$ids    = is_array( $ids ) ? array_values( $ids ) : array();
		$offset = isset( $_POST['offset'] ) ? max( 0, (int) $_POST['offset'] ) : 0;
		$batch  = 3; // pro Request, schont Rate-Limits/Kosten
		$slice  = array_slice( $ids, $offset, $batch );
		$log    = array();

		foreach ( $slice as $id ) {
			$id    = (int) $id;
			$title = wp_basename( (string) get_attached_file( $id ) );
			$res   = $this->generate_for_attachment( $id, false );
			if ( is_wp_error( $res ) ) {
				$log[] = '⚠ ' . esc_html( $title ) . ': ' . esc_html( $res->get_error_message() );
				// Nur kontoweite Fehler brechen den ganzen Lauf ab. Einzelbild-Fehler
				// (z. B. Bildtyp/Größe/Upstream) werden übersprungen, der Lauf geht weiter.
				if ( in_array( $res->get_error_code(), array( 'license', 'feature', 'quota', 'config' ), true ) ) {
					wp_send_json_success( array( 'done' => $offset, 'finished' => true, 'aborted' => true, 'log' => $log ) );
				}
			} elseif ( ! empty( $res['skipped'] ) ) {
				$log[] = '· ' . esc_html( $title ) . ': ' . esc_html__( 'übersprungen', 'wave-access' );
			} elseif ( ! empty( $res['decorative'] ) ) {
				$log[] = '✓ ' . esc_html( $title ) . ': ' . esc_html__( 'als dekorativ markiert', 'wave-access' );
			} else {
				$log[] = '✓ ' . esc_html( $title ) . ': ' . esc_html( isset( $res['alt'] ) ? $res['alt'] : '' );
			}
		}

		$done     = $offset + count( $slice );
		$finished = $done >= count( $ids );
		wp_send_json_success( array( 'done' => $done, 'finished' => $finished, 'log' => $log ) );
	}

	/** Alle Bild-Attachments ohne (nicht leeren) Alt-Text. */
	private function find_missing_alt_ids() {
		global $wpdb;
		$ids = $wpdb->get_col(
			"SELECT p.ID
			 FROM {$wpdb->posts} p
			 LEFT JOIN {$wpdb->postmeta} pm
			   ON p.ID = pm.post_id AND pm.meta_key = '_wp_attachment_image_alt'
			 WHERE p.post_type = 'attachment'
			   AND p.post_mime_type IN ( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml' )
			   AND ( pm.meta_value IS NULL OR pm.meta_value = '' )
			   AND p.ID NOT IN (
			     SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_wave_alt_decorative'
			   )
			 ORDER BY p.ID DESC"
		);
		return array_map( 'intval', (array) $ids );
	}
}

WAVE_Alt_Text::instance();
