<?php
/**
 * Safe Image Attributes - No Lazy Version
 *
 * Purpose:
 * - Do NOT add or remove loading="lazy".
 * - Add decoding="async" where missing.
 * - Alt priority:
 *   1. Media Library image alt text.
 *   2. Existing/static/ACF alt already present in image HTML.
 *   3. Attachment title, caption, or clean filename fallback.
 * - Preserve existing image URLs, dimensions, classes, srcset, sizes, and attributes.
 */

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

/**
 * Emergency disable.
 *
 * To disable quickly, add this before including this file:
 * define( 'SAFE_IMAGE_ATTRIBUTES_DISABLE', true );
 */
if ( ! defined( 'SAFE_IMAGE_ATTRIBUTES_DISABLE' ) ) {
	define( 'SAFE_IMAGE_ATTRIBUTES_DISABLE', false );
}

if ( SAFE_IMAGE_ATTRIBUTES_DISABLE ) {
	return;
}

/**
 * Clean text for safe alt usage.
 *
 * @param string $text Text to clean.
 * @return string
 */
function sia_clean_alt_text( $text ) {
	$text = wp_strip_all_tags( html_entity_decode( (string) $text, ENT_QUOTES, get_bloginfo( 'charset' ) ) );
	$text = preg_replace( '/\s+/', ' ', trim( $text ) );

	if ( function_exists( 'mb_substr' ) ) {
		return mb_substr( $text, 0, 160 );
	}

	return substr( $text, 0, 160 );
}

/**
 * Avoid bad alt text like IMG_1234, DSC_9999, image-1, etc.
 *
 * @param string $text Alt candidate.
 * @return bool
 */
function sia_is_bad_alt( $text ) {
	$text = sia_clean_alt_text( $text );

	if ( '' === $text ) {
		return true;
	}

	return (bool) preg_match(
		'/^(img|dsc|image|photo|picture|screenshot|whatsapp image|screen shot|group)[\s\-_]?\d*$|^\d+$|^[a-f0-9]{8,}$|^untitled$/i',
		$text
	);
}

/**
 * Useful filename fallback.
 *
 * Bad filenames return the default fallback: WareGo WMS.
 *
 * @param string $src Image URL or file path.
 * @return string
 */
function sia_filename_alt( $src ) {
	$path = wp_parse_url( (string) $src, PHP_URL_PATH );

	if ( ! $path ) {
		return '';
	}

	$name = urldecode( basename( $path ) );
	$name = preg_replace( '/\.[a-z0-9]{2,5}$/i', '', $name );
	$name = preg_replace( '/-\d+x\d+$/', '', $name );
	$name = preg_replace( '/-scaled$/', '', $name );
	$name = str_replace( array( '-', '_' ), ' ', $name );
	$name = sia_clean_alt_text( $name );

	return sia_is_bad_alt( $name ) ? 'WareGo WMS' : $name;
}

/**
 * Get only Media Library alt text.
 *
 * This is the highest priority and may replace static/ACF alt text
 * if the image can be matched to a WordPress attachment.
 *
 * @param int $attachment_id Attachment ID.
 * @return string
 */
function sia_media_library_alt( $attachment_id ) {
	static $cache = array();

	$attachment_id = absint( $attachment_id );

	if ( ! $attachment_id ) {
		return '';
	}

	if ( isset( $cache[ $attachment_id ] ) ) {
		return $cache[ $attachment_id ];
	}

	$cache[ $attachment_id ] = '';

	$alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );

	if ( ! sia_is_bad_alt( $alt ) ) {
		$cache[ $attachment_id ] = sia_clean_alt_text( $alt );
	}

	return $cache[ $attachment_id ];
}

/**
 * Get safe fallback alt from attachment data.
 *
 * Priority:
 * 1. Media Library alt
 * 2. Attachment title
 * 3. Caption
 * 4. Clean filename
 *
 * @param int $attachment_id Attachment ID.
 * @return string
 */
function sia_attachment_alt( $attachment_id ) {
	static $cache = array();

	$attachment_id = absint( $attachment_id );

	if ( ! $attachment_id ) {
		return '';
	}

	if ( isset( $cache[ $attachment_id ] ) ) {
		return $cache[ $attachment_id ];
	}

	$cache[ $attachment_id ] = '';

	$media_alt = sia_media_library_alt( $attachment_id );

	if ( '' !== $media_alt ) {
		$cache[ $attachment_id ] = $media_alt;
		return $cache[ $attachment_id ];
	}

	$title = get_the_title( $attachment_id );

	if ( ! sia_is_bad_alt( $title ) ) {
		$cache[ $attachment_id ] = sia_clean_alt_text( $title );
		return $cache[ $attachment_id ];
	}

	$post = get_post( $attachment_id );

	if ( $post && ! sia_is_bad_alt( $post->post_excerpt ) ) {
		$cache[ $attachment_id ] = sia_clean_alt_text( $post->post_excerpt );
		return $cache[ $attachment_id ];
	}

	$file = get_post_meta( $attachment_id, '_wp_attached_file', true );

	if ( $file ) {
		$cache[ $attachment_id ] = sia_filename_alt( $file );
	}

	return $cache[ $attachment_id ];
}

/**
 * Normalize uploads-relative path from image URL.
 *
 * Example:
 * /wp-content/uploads/2025/01/image.png
 * becomes:
 * 2025/01/image.png
 *
 * @param string $src Image URL.
 * @return string
 */
function sia_uploads_relative_path_from_src( $src ) {
	$src  = trim( (string) $src );
	$path = wp_parse_url( $src, PHP_URL_PATH );

	if ( empty( $path ) ) {
		return '';
	}

	/**
	 * WebP Express can output URLs like:
	 * /wp-content/webp-express/webp-images/doc-root/wp-content/uploads/2025/01/image.png.webp
	 *
	 * Convert that internal cache path back to:
	 * /wp-content/uploads/2025/01/image.png
	 *
	 * This is only used to find the Media Library attachment.
	 * It does not change the frontend image URL.
	 */
	if ( false !== strpos( $path, '/webp-express/webp-images/doc-root/' ) ) {
		$parts = explode( '/webp-express/webp-images/doc-root/', $path );

		if ( ! empty( $parts[1] ) ) {
			$path = '/' . ltrim( $parts[1], '/' );
		}
	}

	/**
	 * If WebP Express appended .webp after the original extension,
	 * remove only the final .webp.
	 *
	 * Examples:
	 * image.png.webp  -> image.png
	 * image.jpg.webp  -> image.jpg
	 * image.jpeg.webp -> image.jpeg
	 */
	$path = preg_replace( '/\.(png|jpe?g|gif|svg)\.webp$/i', '.$1', $path );

	$upload_dir = wp_get_upload_dir();
	$base_path  = wp_parse_url( $upload_dir['baseurl'], PHP_URL_PATH );

	if ( $base_path && false !== strpos( $path, $base_path ) ) {
		$relative_path = ltrim( str_replace( $base_path, '', $path ), '/' );
	} else {
		$parts         = explode( '/uploads/', $path );
		$relative_path = isset( $parts[1] ) ? ltrim( $parts[1], '/' ) : '';
	}

	if ( '' === $relative_path ) {
		return '';
	}

	/**
	 * Remove common WordPress generated size suffix.
	 *
	 * Example:
	 * image-name-634x284.png -> image-name.png
	 */
	$relative_path = preg_replace( '/-\d+x\d+(\.[a-z0-9]{2,5})$/i', '$1', $relative_path );

	return $relative_path;
}


/**
 * Find attachment ID from:
 * 1. wp-image-123 class.
 * 2. attachment_url_to_postid().
 * 3. _wp_attached_file fallback DB match.
 *
 * Important:
 * - Does not scan the full media library.
 * - Avoids lookup for theme/plugin asset images.
 *
 * @param string $class Image class attribute.
 * @param string $src   Image src attribute.
 * @return int
 */
function sia_img_attachment_id( $class = '', $src = '' ) {
	static $cache = array();

	if ( preg_match( '/(?:^|\s)wp-image-(\d+)(?:\s|$)/', (string) $class, $matches ) ) {
		return absint( $matches[1] );
	}

	$src = trim( (string) $src );

	if ( '' === $src || 0 === strpos( $src, 'data:' ) || 0 === strpos( $src, 'blob:' ) ) {
		return 0;
	}

	/**
	 * Only uploaded Media Library images should be resolved.
	 * Also allow WebP Express converted upload URLs.
	 * Theme/plugin images should use filename fallback instead.
	 */
	if (
		false === strpos( $src, '/uploads/' ) &&
		false === strpos( $src, '/webp-express/webp-images/doc-root/' )
	) {
		return 0;
	}

	$key = md5( $src );

	if ( isset( $cache[ $key ] ) ) {
		return $cache[ $key ];
	}

	/**
	 * First try WordPress native resolver.
	 */
	$cache[ $key ] = absint( attachment_url_to_postid( $src ) );

	if ( $cache[ $key ] ) {
		return $cache[ $key ];
	}

	/**
	 * Fallback: find attachment by _wp_attached_file relative path.
	 *
	 * This helps when the image has no wp-image-ID class and
	 * attachment_url_to_postid() fails due to local domain/CDN/WebP rewrite differences.
	 */
	$relative_path = sia_uploads_relative_path_from_src( $src );

	if ( '' === $relative_path ) {
		$cache[ $key ] = 0;
		return 0;
	}

	global $wpdb;

	$possible_paths = array( $relative_path );

	/**
	 * WebP plugins can output .webp URLs while WordPress stores the original
	 * attachment as .png, .jpg, .jpeg, .gif, or .svg in _wp_attached_file.
	 */
	if ( preg_match( '/\.webp$/i', $relative_path ) ) {
		$possible_paths[] = preg_replace( '/\.webp$/i', '.png', $relative_path );
		$possible_paths[] = preg_replace( '/\.webp$/i', '.jpg', $relative_path );
		$possible_paths[] = preg_replace( '/\.webp$/i', '.jpeg', $relative_path );
		$possible_paths[] = preg_replace( '/\.webp$/i', '.gif', $relative_path );
		$possible_paths[] = preg_replace( '/\.webp$/i', '.svg', $relative_path );
	}

	$possible_paths = array_unique( array_filter( $possible_paths ) );

	foreach ( $possible_paths as $possible_path ) {
		$attachment_id = $wpdb->get_var(
			$wpdb->prepare(
				"
				SELECT post_id
				FROM {$wpdb->postmeta}
				WHERE meta_key = '_wp_attached_file'
				AND meta_value = %s
				LIMIT 1
				",
				$possible_path
			)
		);

		if ( $attachment_id ) {
			$cache[ $key ] = absint( $attachment_id );
			return $cache[ $key ];
		}
	}

	$cache[ $key ] = 0;

	return 0;
}


/**
 * WordPress-generated attachment images.
 *
 * Alt priority:
 * 1. Media Library alt replaces existing/static/ACF alt if available.
 * 2. If Media Library alt is empty, keep existing alt.
 * 3. If existing alt is missing, use safe attachment fallback.
 *
 * @param array   $attr       Image attributes.
 * @param WP_Post $attachment Attachment object.
 * @return array
 */
function sia_attachment_image_attributes( $attr, $attachment ) {
	if ( is_admin() || is_feed() ) {
		return $attr;
	}

	if ( empty( $attr['decoding'] ) ) {
		$attr['decoding'] = 'async';
	}

	if ( ! empty( $attachment->ID ) ) {
		$media_alt = sia_media_library_alt( $attachment->ID );

		if ( '' !== $media_alt ) {
			$attr['alt'] = $media_alt;
			return $attr;
		}

		if ( empty( $attr['alt'] ) ) {
			$fallback_alt = sia_attachment_alt( $attachment->ID );

			if ( '' !== $fallback_alt ) {
				$attr['alt'] = $fallback_alt;
			}
		}
	}

	return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'sia_attachment_image_attributes', 20, 2 );

/**
 * Process HTML <img> tags.
 *
 * @param string $html HTML content.
 * @return string
 */
function sia_process_img_html( $html ) {
	if (
		is_admin() ||
		is_feed() ||
		wp_doing_ajax() ||
		! is_string( $html ) ||
		false === stripos( $html, '<img' )
	) {
		return $html;
	}

	if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) {
		return $html;
	}

	$processor = new WP_HTML_Tag_Processor( $html );

	while ( $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
		$class = (string) $processor->get_attribute( 'class' );
		$src   = (string) $processor->get_attribute( 'src' );

		if ( ! $processor->get_attribute( 'decoding' ) ) {
			$processor->set_attribute( 'decoding', 'async' );
		}

		$alt         = $processor->get_attribute( 'alt' );
		$role        = strtolower( (string) $processor->get_attribute( 'role' ) );
		$aria_hidden = strtolower( (string) $processor->get_attribute( 'aria-hidden' ) );

		/**
		 * Skip decorative/hidden images.
		 */
		if ( 'presentation' === $role || 'none' === $role || 'true' === $aria_hidden ) {
			continue;
		}

		$attachment_id = sia_img_attachment_id( $class, $src );

		/**
		 * Priority 1:
		 * If Media Library alt exists, use it even if image already has static/ACF alt.
		 */
		if ( $attachment_id ) {
			$media_alt = sia_media_library_alt( $attachment_id );

			if ( '' !== $media_alt ) {
				$processor->set_attribute( 'alt', $media_alt );
				continue;
			}
		}

		/**
		 * Priority 2:
		 * If image already has static/ACF alt, keep it.
		 */
		if ( null !== $alt && '' !== trim( (string) $alt ) ) {
			continue;
		}

		/**
		 * Priority 3:
		 * If alt is missing/empty, use attachment fallback or filename fallback.
		 */
		$new_alt = '';

		if ( $attachment_id ) {
			$new_alt = sia_attachment_alt( $attachment_id );
		}

		if ( '' === $new_alt ) {
			$new_alt = sia_filename_alt( $src );
		}

		if ( '' !== $new_alt ) {
			$processor->set_attribute( 'alt', $new_alt );
		}
	}

	return $processor->get_updated_html();
}

/**
 * Full-page fallback for hardcoded header/footer/theme images.
 *
 * This runs once on the final HTML.
 * It does NOT add lazy loading.
 * It only adds decoding="async" and safe alt text.
 *
 * @return void
 */
function sia_start_output_processing() {
	if (
		is_admin() ||
		is_feed() ||
		wp_doing_ajax() ||
		( function_exists( 'wp_is_json_request' ) && wp_is_json_request() ) ||
		( defined( 'REST_REQUEST' ) && REST_REQUEST )
	) {
		return;
	}

	ob_start( 'sia_process_img_html' );
}
add_action( 'template_redirect', 'sia_start_output_processing', 0 );