<?php

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

/**
 * SEO plugin compatibility.
 *
 * Uses each plugin's own filters to replace their og:image output with ours
 * whenever DOIG has a resolved image to emit. Pre-2.4.0 this was always a
 * cached screenshot; from 2.4.0 onward it can be a screenshot, the post's
 * Featured Image, a per-post Custom Image, or nothing (Source = Off, or
 * Featured/Custom with no attachment, or Screenshot mode without a ready
 * capture). The DOIG_Source_Resolver is the single decision point.
 *
 * When the resolver returns null DOIG leaves the SEO plugins entirely
 * untouched - their own fallback chain handles the page.
 *
 * Supported: Yoast SEO, Rank Math, All in One SEO (AIOSEO).
 */
class DOIG_SEO_Compat {

	public static function init() {
		add_action( 'wp', array( __CLASS__, 'register_overrides' ) );
	}

	/**
	 * Runs after WordPress has set up the query.
	 * Only attaches override filters when the resolver has a payload to
	 * emit for the current page - a singular post, or (2.7.0 M4) a
	 * covered surface: homepage, post-type archive, taxonomy page,
	 * author archive.
	 *
	 * Non-singular applicability of the filters below, source-verified
	 * 2026-07-23 (M4 research):
	 * - Yoast fires both image hooks from a generator invoked by the
	 *   SHARED Indexable_Presentation base used by every page type.
	 * - Rank Math's Image::set_images() explicitly branches for front
	 *   page / home / post-type archives / category / tag / tax /
	 *   author, all funnelling through the add_image flow our filter
	 *   rides - including the empty-input add_image() call when a page
	 *   has no image of its own.
	 * - AIOSEO outputs NO social meta on taxonomy terms or author
	 *   archives (Social\Output::isAllowed()), so the filters below
	 *   simply never fire there - harmless - and DOIG_OG_Injector emits
	 *   the full block on those pages instead.
	 */
	public static function register_overrides() {
		$payload = null;

		if ( is_singular() ) {
			// 2.4.0 M4.1: resolver replaces the legacy
			// `get_image_url_for_post()` path. Returns the full payload
			// { url, width, height, type } or null. The width/height/type
			// values drive the dimension filters across all three SEO
			// plugins so non-screenshot modes (Featured / Custom) emit the
			// attachment's real dimensions and MIME type rather than the
			// hardcoded 1200x630 webp that was always-correct for the
			// pre-2.4.0 screenshot-only path.
			$payload = DOIG_Source_Resolver::resolve( get_the_ID() );
		} else {
			$surface_key = DOIG_Surfaces::current_surface_key();
			if ( '' !== $surface_key ) {
				// Read-only lookup - the compat layer never allocates.
				// The injector allocates when it first schedules a
				// capture; until then there is nothing to emit anyway.
				$surface_id = DOIG_Surfaces::id_for_key( $surface_key, false );
				$payload    = $surface_id ? DOIG_Source_Resolver::resolve_surface( $surface_id ) : null;
			}
		}

		if ( ! $payload ) {
			return;
		}

		$image_url    = $payload['url'];
		$image_width  = (int) $payload['width'];
		$image_height = (int) $payload['height'];
		$image_type   = (string) $payload['type'];

		// --- Yoast SEO ---
		// Two coupled problems:
		//
		// (1) wpseo_opengraph_image is rewrite-only (Yoast docs: "If there is no
		//     og:image, nothing is done by the filter"). On pages where Yoast has
		//     no image of its own, og:image was missing entirely. We need to ADD
		//     one to Yoast's image collection.
		//
		// (2) The Yoast Open_Graph_Image_Generator::generate() flow (verified
		//     against the trunk source) is:
		//        a. wpseo_add_opengraph_images           (fires FIRST, container empty)
		//        b. add_from_indexable                   (adds the post's own OG image, if any)
		//        c. wpseo_add_opengraph_additional_images (fires AFTER add_from_indexable)
		//        d. add_from_templates                   (skips if has_images())
		//        e. add_from_default                     (skips if has_images())
		//
		//     The 2.1.2 fix hooked wpseo_add_opengraph_images at step (a),
		//     unconditionally adding our screenshot. On a page where the
		//     indexable then ALSO added an image at step (b), the collection
		//     ended up with two entries. At output time the wpseo_opengraph_image
		//     URL-rewrite below was applied to each entry, producing two og:image
		//     blocks with the same URL but different width/height (caught by SSAA
		//     audit 2026-06-17). Hard regression.
		//
		// Fix: hook wpseo_add_opengraph_additional_images at step (c) instead,
		// and gate it on has_images(). If the indexable already added one, we
		// skip ours and let the URL-rewrite below override it. If the indexable
		// added nothing, we add ours and the downstream add_from_templates /
		// add_from_default both bail because has_images() is true. Exactly one
		// image in the collection in either case.
		//
		// We also add wpseo_opengraph_image_width / _height / _type filters so
		// that when the URL is rewritten on Yoast's existing image, the
		// metadata reflects OUR screenshot's actual dimensions (1200 x 630 webp)
		// rather than whatever Yoast had originally claimed.
		if ( defined( 'WPSEO_VERSION' ) ) {
			add_filter(
				'wpseo_add_opengraph_additional_images',
				function( $image_container ) use ( $image_url ) {
					if (
						is_object( $image_container )
						&& method_exists( $image_container, 'has_images' )
						&& method_exists( $image_container, 'add_image_by_url' )
						&& ! $image_container->has_images()
					) {
						$image_container->add_image_by_url( $image_url );
					}
					return $image_container;
				},
				20
			);
			add_filter( 'wpseo_opengraph_image',        function() use ( $image_url ) { return $image_url; }, 20 );
			add_filter( 'wpseo_twitter_image',          function() use ( $image_url ) { return $image_url; }, 20 );
			add_filter( 'wpseo_opengraph_image_size',   function() { return 'full'; }, 20 );

			// 2.4.0 M4.1: width/height/type values come from the resolver
			// payload, not hardcoded screenshot constants. For screenshot
			// mode the payload's width/height/type ARE 1200/630/image/webp
			// (matches pre-2.4.0 behaviour); for Featured / Custom modes
			// they're the attachment's real dimensions + MIME type pulled
			// from wp_get_attachment_metadata() inside the resolver.
			add_filter( 'wpseo_opengraph_image_width',  function() use ( $image_width )  { return $image_width;  }, 20 );
			add_filter( 'wpseo_opengraph_image_height', function() use ( $image_height ) { return $image_height; }, 20 );
			add_filter( 'wpseo_opengraph_image_type',   function() use ( $image_type )   { return $image_type;   }, 20 );
		}

		// --- Rank Math ---
		// Correct filter names per Rank Math developer docs:
		// rank_math/opengraph/{network}/image - where network is 'facebook' or 'twitter'.
		//
		// Verified safe against Rank Math source (Image::set_images()): when
		// there is no image set for a page AND no Rank Math default OG image,
		// Rank Math explicitly calls $this->add_image() with no argument to
		// give the rank_math/opengraph/{network}/image filter a chance to add
		// one. Our filter callback returns our URL with empty input, which
		// Rank Math accepts as the image to add. So the rewrite-only failure
		// mode that Yoast has does NOT apply to Rank Math.
		//
		// 2.3.0: width/height/type emission via the second
		// `rank_math/opengraph/{network}/image_array` filter. Rank Math has no
		// per-tag filters for og:image:width / og:image:height / og:image:type;
		// instead its Image::image_meta() emission loop reads from the
		// attachment array's width/height/type/alt keys (verified against
		// includes/opengraph/class-image.php master). When our /image filter
		// above swaps in the screenshot URL, Rank Math resets the attachment
		// to ['url' => $new_url] and the dimensions are dropped - the loop
		// then skips empty keys and no dimension tags ship. The
		// /image_array filter fires immediately after the URL swap and lets
		// us return a fully-populated array, after which image_meta() emits
		// og:image:width=1200, og:image:height=630, og:image:type=image/webp
		// alongside the URL.
		if ( defined( 'RANK_MATH_VERSION' ) ) {
			add_filter( 'rank_math/opengraph/facebook/image', function() use ( $image_url ) { return $image_url; }, 20 );
			add_filter( 'rank_math/opengraph/twitter/image',  function() use ( $image_url ) { return $image_url; }, 20 );

			// Enrich the attachment array with the resolver's
			// dimensions + MIME. URL-guarded so we only assert these
			// when the swap won; if Rank Math is still using its own
			// attachment for some reason, we leave it alone rather than
			// asserting dimensions that don't match the image actually
			// being emitted.
			//
			// 2.4.0 M4.1: dimensions come from the resolver payload, not
			// hardcoded screenshot constants. Source = Featured / Custom
			// emit the attachment's real width/height/type; Source =
			// Screenshot still emits 1200/630/image/webp.
			$rm_enrich = function( $attachment ) use ( $image_url, $image_width, $image_height, $image_type ) {
				if ( is_array( $attachment ) && isset( $attachment['url'] ) && $attachment['url'] === $image_url ) {
					$attachment['width']  = $image_width;
					$attachment['height'] = $image_height;
					$attachment['type']   = $image_type;
				}
				return $attachment;
			};
			add_filter( 'rank_math/opengraph/facebook/image_array', $rm_enrich, 20 );
			add_filter( 'rank_math/opengraph/twitter/image_array',  $rm_enrich, 20 );
		}

		// --- All in One SEO (AIOSEO) ---
		// AIOSEO exposes its Open Graph and Twitter output as filterable arrays keyed by
		// property name (e.g. 'og:image', 'twitter:image'). The older URL-only filters
		// (aioseo_og_image_url / aioseo_twitter_image_url) do not exist in current versions.
		//
		// 2.3.0: assert og:image:width / og:image:height / og:image:type
		// explicitly. Previous behaviour unset() them on the grounds that
		// asserting a size AIOSEO did not set was risky. With the screenshot
		// confirmed always 1200 x 630 webp, the explicit assertion is
		// correct - matches the Yoast and Rank Math branches so all three SEO
		// plugins emit consistent dimension metadata.
		// 2.4.0 M4.1: dimensions come from the resolver payload, not
		// hardcoded screenshot constants. Source = Featured / Custom emit
		// the attachment's real width/height/type values; Source =
		// Screenshot still emits 1200/630/image/webp.
		if ( defined( 'AIOSEO_VERSION' ) ) {
			add_filter(
				'aioseo_facebook_tags',
				function( $tags ) use ( $image_url, $image_width, $image_height, $image_type ) {
					$tags['og:image']            = $image_url;
					$tags['og:image:secure_url'] = $image_url;
					$tags['og:image:width']      = (string) $image_width;
					$tags['og:image:height']     = (string) $image_height;
					$tags['og:image:type']       = $image_type;
					return $tags;
				},
				20
			);
			add_filter(
				'aioseo_twitter_tags',
				function( $tags ) use ( $image_url ) {
					$tags['twitter:image'] = $image_url;
					return $tags;
				},
				20
			);
		}
	}
}
