<?php
/**
 * Image SEO — Auto ALT & Title Attributes
 *
 * Filters the_content and post_thumbnail_html to add missing alt/title
 * attributes on images. Uses the ReplaceVariables engine for format strings.
 * Web-app-provided image alt data takes priority over auto-generated values.
 *
 * @package SEOvaultSEO\Frontend
 */

namespace SEOvaultSEO\Frontend;

use SEOvaultSEO\Core\Options;
use SEOvaultSEO\Core\PostMeta;
use SEOvaultSEO\ReplaceVariables\Manager as ReplaceVarsManager;

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

/**
 * Adds generated attributes to rendered images without changing stored content.
 */
class ImageSeo {

	/**
	 * Whether to auto-add ALT attributes.
	 *
	 * @var string|false
	 */
	private $alt_format = false;

	/**
	 * Whether to auto-add title attributes.
	 *
	 * @var string|false
	 */
	private $title_format = false;

	/**
	 * Whether to override existing empty ALT attributes.
	 *
	 * @var bool
	 */
	private $override_empty_alt = true;

	/**
	 * Image counter within the current post content.
	 *
	 * @var int
	 */
	private $image_counter = 0;

	/**
	 * Initialize hooks.
	 */
	public static function init() {
		if ( ! \SEOvaultSEO\Compatibility\Detector::should_output_seo() ) {
			return;
		}

		$instance = new self();
		add_action( 'wp', array( $instance, 'setup' ), 9999 );
	}

	/**
	 * Setup filters if image SEO is enabled.
	 */
	public function setup() {
		if ( ! Options::is_image_seo_enabled() ) {
			return;
		}

		$this->alt_format         = Options::get_image_seo_alt_format();
		$this->title_format       = Options::get_image_seo_title_format();
		$this->override_empty_alt = Options::is_image_seo_override_empty_alt();

		if ( ! $this->alt_format && ! $this->title_format ) {
			return;
		}

		add_filter( 'the_content', array( $this, 'process_content' ), 11 );
		add_filter( 'post_thumbnail_html', array( $this, 'process_thumbnail' ), 11, 2 );

		if ( $this->alt_format ) {
			// WooCommerce galleries and product blocks use wp_get_attachment_image()
			// instead of the content/thumbnail filters above.
			add_filter( 'wp_get_attachment_image_attributes', array( $this, 'process_attachment_attributes' ), 11, 3 );
			add_filter( 'woocommerce_gallery_image_html_attachment_image_params', array( $this, 'process_product_gallery_attributes' ), 11, 4 );
			add_filter( 'woocommerce_single_product_image_thumbnail_html', array( $this, 'process_product_gallery_html' ), 11, 2 );
		}
	}

	/**
	 * Process post content to add missing image attributes.
	 *
	 * @param string $content Post content.
	 * @return string
	 */
	public function process_content( $content ) {
		if ( empty( $content ) ) {
			return $content;
		}

		$this->image_counter = 0;

		$stripped = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
		preg_match_all( '/<img ([^>]+)\/?>/iU', $stripped, $matches, PREG_SET_ORDER );

		if ( empty( $matches ) ) {
			return $content;
		}

		$post = get_post();

		foreach ( $matches as $image ) {
			$attrs = $this->extract_attributes( $image[0] );

			if ( ! isset( $attrs['src'] ) && ! isset( $attrs['data-src'] ) && ! isset( $attrs['data-ct-lazy'] ) ) {
				continue;
			}

			++$this->image_counter;

			$is_dirty = false;

			// Handle ALT attribute.
			if ( $this->alt_format ) {
				$existing_alt = isset( $attrs['alt'] ) ? trim( $attrs['alt'] ) : '';

				if ( '' === $existing_alt ) {
					$generated_alt = $this->generate_attribute_value( $this->alt_format, $attrs, $post );
					if ( '' !== $generated_alt ) {
						$attrs['alt'] = $generated_alt;
						$is_dirty     = true;
					}
				}
			}

			// Handle title attribute.
			if ( $this->title_format ) {
				$existing_title = isset( $attrs['title'] ) ? trim( $attrs['title'] ) : '';

				if ( '' === $existing_title ) {
					$generated_title = $this->generate_attribute_value( $this->title_format, $attrs, $post );
					if ( '' !== $generated_title ) {
						$attrs['title'] = $generated_title;
						$is_dirty       = true;
					}
				}
			}

			if ( $is_dirty ) {
				$new_img = '<img' . $this->attributes_to_string( $attrs ) . '>';
				$content = str_replace( $image[0], $new_img, $content );
			}
		}

		return $content;
	}

	/**
	 * Process featured image HTML to add missing attributes.
	 *
	 * @param string $html    Thumbnail HTML.
	 * @param int    $post_id Post ID.
	 * @return string
	 */
	public function process_thumbnail( $html, $post_id ) {
		if ( empty( $html ) ) {
			return $html;
		}

		$this->image_counter = 0;

		preg_match_all( '/<img ([^>]+)\/?>/iU', $html, $matches, PREG_SET_ORDER );

		if ( empty( $matches ) ) {
			return $html;
		}

		$post = get_post( $post_id );

		foreach ( $matches as $image ) {
			$attrs    = $this->extract_attributes( $image[0] );
			$is_dirty = false;

			if ( $this->alt_format ) {
				$existing_alt = isset( $attrs['alt'] ) ? trim( $attrs['alt'] ) : '';
				if ( '' === $existing_alt ) {
					$generated_alt = $this->get_product_thumbnail_alt( $post, $attrs );
					if ( '' === $generated_alt ) {
						$generated_alt = $this->generate_attribute_value( $this->alt_format, $attrs, $post );
					}
					if ( '' !== $generated_alt ) {
						$attrs['alt'] = $generated_alt;
						$is_dirty     = true;
					}
				}
			}

			if ( $this->title_format ) {
				$existing_title = isset( $attrs['title'] ) ? trim( $attrs['title'] ) : '';
				if ( '' === $existing_title ) {
					$generated_title = $this->generate_attribute_value( $this->title_format, $attrs, $post );
					if ( '' !== $generated_title ) {
						$attrs['title'] = $generated_title;
						$is_dirty       = true;
					}
				}
			}

			if ( $is_dirty ) {
				$new_img = '<img' . $this->attributes_to_string( $attrs ) . '>';
				$html    = str_replace( $image[0], $new_img, $html );
			}
		}

		return $html;
	}

	/**
	 * Fill missing ALT text on WordPress-generated product images.
	 *
	 * WooCommerce can supply the product title as a fallback before this filter
	 * runs. The stored attachment ALT is therefore checked directly: when it is
	 * empty, product images use their own filename rather than the parent title.
	 *
	 * @param array    $attr       Image attributes.
	 * @param \WP_Post $attachment Attachment post.
	 * @param string   $size       Requested image size.
	 * @return array
	 */
	public function process_attachment_attributes( $attr, $attachment, $size ) {
		unset( $size );

		if ( ! $this->alt_format || ! $attachment instanceof \WP_Post || ! $this->is_product_image_attachment( $attachment ) ) {
			return $attr;
		}

		$stored_alt = trim( wp_strip_all_tags( (string) get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ) ) );
		if ( '' !== $stored_alt ) {
			return $attr;
		}

		$generated_alt = $this->get_attachment_filename( $attachment->ID );
		if ( '' !== $generated_alt ) {
			$attr['alt'] = $generated_alt;
		}

		return $attr;
	}

	/**
	 * Replace WooCommerce's product-title fallback for gallery images.
	 *
	 * @param array        $params        Gallery image attributes.
	 * @param int          $attachment_id Attachment ID.
	 * @param string|array $image_size    Requested image size.
	 * @param bool         $main_image    Whether this is the main image.
	 * @return array
	 */
	public function process_product_gallery_attributes( $params, $attachment_id, $image_size, $main_image ) {
		unset( $image_size, $main_image );

		if ( ! $this->alt_format ) {
			return $params;
		}

		$stored_alt = trim( wp_strip_all_tags( (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
		if ( '' !== $stored_alt ) {
			return $params;
		}

		$generated_alt = $this->get_attachment_filename( $attachment_id );
		if ( '' !== $generated_alt ) {
			$params['alt'] = $generated_alt;
		}

		return $params;
	}

	/**
	 * Keep the rendered gallery image and thumbnail ALT values filename-based.
	 *
	 * WooCommerce calculates data-thumb-alt before filtering image parameters,
	 * so the final HTML filter is needed to keep both values consistent.
	 *
	 * @param string $html          Product gallery image HTML.
	 * @param int    $attachment_id Attachment ID.
	 * @return string
	 */
	public function process_product_gallery_html( $html, $attachment_id ) {
		if ( ! $this->alt_format || '' === $html ) {
			return $html;
		}

		$stored_alt = trim( wp_strip_all_tags( (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
		if ( '' !== $stored_alt ) {
			return $html;
		}

		$generated_alt = $this->get_attachment_filename( $attachment_id );
		if ( '' === $generated_alt ) {
			return $html;
		}

		if ( preg_match( '/\sdata-thumb-alt\s*=/i', $html ) ) {
			$html = $this->replace_or_add_attribute( $html, 'data-thumb-alt', $generated_alt );
		}

		return (string) preg_replace_callback(
			'/<img\b[^>]*>/i',
			function ( $matches ) use ( $generated_alt ) {
				return $this->replace_or_add_attribute( $matches[0], 'alt', $generated_alt );
			},
			$html
		);
	}

	/**
	 * Generate an attribute value from a format string using replace variables.
	 *
	 * @param string        $format Format string (e.g. "%title%").
	 * @param array         $attrs  Image attributes.
	 * @param \WP_Post|null $post   Current post.
	 * @return string
	 */
	private function generate_attribute_value( $format, $attrs, $post ) {
		$format = trim( $format );
		if ( '' === $format ) {
			return '';
		}

		// Resolve the image source for %filename% variable.
		$src      = $this->get_image_source( $attrs );
		$filename = $this->get_filename_from_source( $src );

		// Build context args for variable replacement.
		$args = array(
			'filename' => $filename,
		);

		if ( $post ) {
			$args = array_merge(
				$args,
				array(
					'ID'            => $post->ID,
					'post_author'   => $post->post_author,
					'post_content'  => $post->post_content,
					'post_date'     => $post->post_date,
					'post_excerpt'  => $post->post_excerpt,
					'post_modified' => $post->post_modified,
					'post_title'    => $post->post_title,
					'post_type'     => $post->post_type,
				)
			);
		}

		// Use the replace variables engine.
		$manager = ReplaceVarsManager::get_instance();
		$value   = $manager->replace( $format, $args );

		return trim( $value );
	}

	/**
	 * Get a filename ALT for a product featured image.
	 *
	 * @param \WP_Post|null $post  Thumbnail owner.
	 * @param array         $attrs Image attributes.
	 * @return string
	 */
	private function get_product_thumbnail_alt( $post, $attrs ) {
		if ( ! $post || 'product' !== $post->post_type ) {
			return '';
		}

		$attachment_id = (int) get_post_thumbnail_id( $post->ID );
		if ( $attachment_id > 0 ) {
			$filename = $this->get_attachment_filename( $attachment_id );
			if ( '' !== $filename ) {
				return $filename;
			}
		}

		return $this->get_filename_from_source( $this->get_image_source( $attrs ) );
	}

	/**
	 * Determine whether an attachment belongs to the current product context.
	 *
	 * @param \WP_Post $attachment Attachment post.
	 * @return bool
	 */
	private function is_product_image_attachment( $attachment ) {
		if ( $attachment->post_parent > 0 && 'product' === get_post_type( $attachment->post_parent ) ) {
			return true;
		}

		$product = isset( $GLOBALS['product'] ) ? $GLOBALS['product'] : null;
		if ( is_object( $product ) && method_exists( $product, 'get_image_id' ) && method_exists( $product, 'get_gallery_image_ids' ) ) {
			$product_image_ids = array_merge(
				array( (int) $product->get_image_id() ),
				array_map( 'intval', (array) $product->get_gallery_image_ids() )
			);
			if ( in_array( (int) $attachment->ID, $product_image_ids, true ) ) {
				return true;
			}
		}

		$current_post = get_post();
		if ( ! $current_post || 'product' !== $current_post->post_type ) {
			return false;
		}

		$product_image_ids = array( (int) get_post_thumbnail_id( $current_post->ID ) );
		$gallery_ids       = array_filter( array_map( 'intval', explode( ',', (string) get_post_meta( $current_post->ID, '_product_image_gallery', true ) ) ) );

		return in_array( (int) $attachment->ID, array_merge( $product_image_ids, $gallery_ids ), true );
	}

	/**
	 * Build readable ALT text from an attachment's original filename.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return string
	 */
	private function get_attachment_filename( $attachment_id ) {
		$file = (string) get_post_meta( $attachment_id, '_wp_attached_file', true );
		if ( '' !== $file ) {
			$filename = $this->get_filename_from_source( $file );
			if ( '' !== $filename ) {
				return $filename;
			}
		}

		return $this->get_filename_from_source( (string) wp_get_attachment_url( $attachment_id ) );
	}

	/**
	 * Resolve an image source from standard and lazy-loading attributes.
	 *
	 * @param array $attrs Image attributes.
	 * @return string
	 */
	private function get_image_source( $attrs ) {
		if ( ! empty( $attrs['data-src'] ) ) {
			return $attrs['data-src'];
		}
		if ( ! empty( $attrs['data-ct-lazy'] ) ) {
			return $attrs['data-ct-lazy'];
		}
		return isset( $attrs['src'] ) ? $attrs['src'] : '';
	}

	/**
	 * Convert a file path or URL basename into readable text.
	 *
	 * @param string $source File path or URL.
	 * @return string
	 */
	private function get_filename_from_source( $source ) {
		if ( '' === $source ) {
			return '';
		}

		$path     = wp_parse_url( $source, PHP_URL_PATH );
		$basename = $path ? wp_basename( rawurldecode( $path ) ) : '';
		$filename = $basename ? pathinfo( $basename, PATHINFO_FILENAME ) : '';
		$filename = preg_replace( '/[-_+\s]+/', ' ', $filename );
		$filename = trim( (string) $filename );

		return function_exists( 'mb_convert_case' )
			? mb_convert_case( $filename, MB_CASE_TITLE )
			: ucwords( $filename );
	}

	/**
	 * Replace an HTML attribute, or add it before a tag's closing bracket.
	 *
	 * @param string $html  HTML string or individual tag.
	 * @param string $name  Attribute name.
	 * @param string $value Attribute value.
	 * @return string
	 */
	private function replace_or_add_attribute( $html, $name, $value ) {
		$pattern     = '/\s' . preg_quote( $name, '/' ) . '\s*=\s*(["\']).*?\1/i';
		$replacement = ' ' . $name . '="' . esc_attr( $value ) . '"';

		if ( preg_match( $pattern, $html ) ) {
			return (string) preg_replace( $pattern, $replacement, $html, 1 );
		}

		return (string) preg_replace( '/(\s*\/?>)$/', $replacement . '$1', $html, 1 );
	}

	/**
	 * Extract attributes from an HTML tag string.
	 *
	 * @param string $tag HTML tag string.
	 * @return array Associative array of attribute name => value.
	 */
	private function extract_attributes( $tag ) {
		$attrs = array();

		preg_match_all( '/([a-zA-Z0-9_\-:]+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/', $tag, $attr_matches, PREG_SET_ORDER );

		foreach ( $attr_matches as $match ) {
			$name  = strtolower( $match[1] );
			$value = '';
			if ( isset( $match[2] ) && '' !== $match[2] ) {
				$value = $match[2];
			} elseif ( isset( $match[3] ) && '' !== $match[3] ) {
				$value = $match[3];
			} elseif ( isset( $match[4] ) && '' !== $match[4] ) {
				$value = $match[4];
			}
			$attrs[ $name ] = $value;
		}

		return $attrs;
	}

	/**
	 * Convert an associative array of attributes back to an HTML attribute string.
	 *
	 * @param array $attrs Attributes array.
	 * @return string
	 */
	private function attributes_to_string( $attrs ) {
		$result = '';

		foreach ( $attrs as $name => $value ) {
			if ( '' === $value ) {
				$result .= ' ' . esc_attr( $name );
			} else {
				$result .= ' ' . esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
			}
		}

		return $result;
	}
}
