<?php
namespace DefaultFeaturedImage;

use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID      = 'default-featured-image';
	private const MODULE_VERSION = '1.0.5';

	/** Resolved default attachment ID cache (-1 = not yet resolved). */
	private int $default_attachment_id = -1;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Default Featured Image', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Default thumbnail when a post has none—consistent grids, cards, and share images.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'frontend', 'rest' ];
	}

	protected function registerHooks(): void {
		if ( $this->getDefaultAttachmentId() <= 0 ) {
			return;
		}

		add_filter( 'get_post_metadata', [ $this, 'filterThumbnailMeta' ], 10, 4 );
		add_filter( 'post_thumbnail_id', [ $this, 'defaultThumbnailId' ], 10, 2 );
		add_filter( 'post_thumbnail_html', [ $this, 'injectDefaultThumbnail' ], 10, 5 );

		add_filter( 'wpseo_opengraph_image', [ $this, 'filterSeoImage' ] );
		add_filter( 'wpseo_twitter_image', [ $this, 'filterSeoImage' ] );
		add_filter( 'rank_math/opengraph/facebook/image', [ $this, 'filterSeoImage' ] );
		add_filter( 'rank_math/opengraph/twitter/image', [ $this, 'filterSeoImage' ] );
		add_filter( 'aioseo_opengraph_image', [ $this, 'filterSeoImage' ] );
		add_filter( 'aioseo_twitter_image', [ $this, 'filterSeoImage' ] );
		add_filter( 'seopress_social_og_thumb', [ $this, 'filterSeoImage' ] );
		add_filter( 'jetpack_open_graph_image_default', [ $this, 'filterSeoImage' ] );
		add_action( 'wp_head', [ $this, 'outputFallbackSocialMeta' ], 99 );
	}

	/**
	 * Short-circuit `_thumbnail_id` only when the post has no real featured image.
	 *
	 * WordPress always passes null as `$value` into `get_{$meta_type}_metadata`;
	 * a non-null return replaces the DB/cache lookup. We must read the stored
	 * meta ourselves before injecting the default.
	 *
	 * @param mixed  $value     Short-circuit value from an earlier filter, or null.
	 * @param int    $object_id Post ID.
	 * @param string $meta_key  Meta key.
	 * @param bool   $single    Whether a single value was requested.
	 */
	public function filterThumbnailMeta( $value, int $object_id, string $meta_key, bool $single ) {
		if ( null !== $value || $meta_key !== '_thumbnail_id' || ! $single ) {
			return $value;
		}

		$default_id = $this->getDefaultAttachmentId();
		if ( $default_id <= 0 ) {
			return $value;
		}

		// Post already has a featured image — let WordPress return the real value.
		if ( $this->getStoredThumbnailId( $object_id ) > 0 ) {
			return $value;
		}

		$post = get_post( $object_id );
		if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'thumbnail' ) ) {
			return $value;
		}

		return [ (string) $default_id ];
	}

	/**
	 * @param int|false|string     $thumbnail_id Current thumbnail ID or false when none.
	 * @param int|\WP_Post|null    $post         Post ID or post object.
	 */
	public function defaultThumbnailId( $thumbnail_id, $post ): int {
		if ( (int) $thumbnail_id > 0 ) {
			return (int) $thumbnail_id;
		}

		$post = get_post( $post );
		if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'thumbnail' ) ) {
			return 0;
		}

		$default_id = $this->getDefaultAttachmentId();

		return $default_id > 0 ? $default_id : 0;
	}

	/**
	 * @param string               $html              Post thumbnail HTML.
	 * @param int                  $post_id           Post ID.
	 * @param int                  $post_thumbnail_id Thumbnail attachment ID, or 0.
	 * @param string|int[]         $size              Requested size.
	 * @param string|array         $attr              Attributes.
	 */
	public function injectDefaultThumbnail( $html, int $post_id, int $post_thumbnail_id, $size, $attr ): string {
		$html = is_string( $html ) ? $html : '';

		if ( $post_thumbnail_id > 0 || $html !== '' ) {
			return $html;
		}

		$default_id = $this->getDefaultAttachmentId();
		if ( $default_id <= 0 ) {
			return $html;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'thumbnail' ) ) {
			return $html;
		}

		$attr = is_array( $attr ) ? $attr : [];

		return (string) wp_get_attachment_image( $default_id, $size, false, $attr );
	}

	/**
	 * @param mixed $image Image URL from an SEO or social plugin.
	 */
	public function filterSeoImage( $image ) {
		if ( is_string( $image ) && $image !== '' ) {
			return $image;
		}

		if ( ! is_singular() ) {
			return $image;
		}

		$post_id = (int) get_queried_object_id();
		if ( $post_id <= 0 || $this->getStoredThumbnailId( $post_id ) > 0 ) {
			return $image;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'thumbnail' ) ) {
			return $image;
		}

		$url = $this->getDefaultImageUrlForSharing();

		return $url !== '' ? $url : $image;
	}

	public function outputFallbackSocialMeta(): void {
		if ( ! $this->shouldOutputFallbackSocialMeta() || ! is_singular() ) {
			return;
		}

		$post_id = (int) get_queried_object_id();
		if ( $post_id <= 0 || $this->getStoredThumbnailId( $post_id ) > 0 ) {
			return;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post || ! post_type_supports( $post->post_type, 'thumbnail' ) ) {
			return;
		}

		$url = $this->getDefaultImageUrlForSharing();
		if ( $url === '' ) {
			return;
		}

		echo '<meta property="og:image" content="' . esc_url( $url ) . '" />' . "\n";
		echo '<meta name="twitter:card" content="summary_large_image" />' . "\n";
		echo '<meta name="twitter:image" content="' . esc_url( $url ) . '" />' . "\n";

		$meta = wp_get_attachment_metadata( $this->getDefaultAttachmentId() );
		if ( is_array( $meta ) && ! empty( $meta['width'] ) && ! empty( $meta['height'] ) ) {
			echo '<meta property="og:image:width" content="' . esc_attr( (string) (int) $meta['width'] ) . '" />' . "\n";
			echo '<meta property="og:image:height" content="' . esc_attr( (string) (int) $meta['height'] ) . '" />' . "\n";
		}
	}

	private function shouldOutputFallbackSocialMeta(): bool {
		return ! defined( 'WPSEO_VERSION' )
			&& ! class_exists( 'RankMath', false )
			&& ! defined( 'AIOSEO_VERSION' )
			&& ! defined( 'SEOPRESS_VERSION' );
	}

	private function getDefaultImageUrlForSharing(): string {
		$attachment_id = $this->getDefaultAttachmentId();
		if ( $attachment_id <= 0 ) {
			return '';
		}

		foreach ( [ 'full', 'large' ] as $size ) {
			$url = wp_get_attachment_image_url( $attachment_id, $size );
			if ( is_string( $url ) && $url !== '' ) {
				return $url;
			}
		}

		return '';
	}

	/**
	 * Read the real `_thumbnail_id` without going through our get_post_metadata filter.
	 * Prefers the post meta object cache; falls back to a direct DB read.
	 */
	private function getStoredThumbnailId( int $post_id ): int {
		$meta_cache = wp_cache_get( $post_id, 'post_meta' );

		if ( false === $meta_cache ) {
			$cached = update_meta_cache( 'post', [ $post_id ] );
			$meta_cache = is_array( $cached ) ? ( $cached[ $post_id ] ?? [] ) : [];
		}

		if ( is_array( $meta_cache ) && isset( $meta_cache['_thumbnail_id'][0] ) ) {
			return (int) $meta_cache['_thumbnail_id'][0];
		}

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$value = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_thumbnail_id' LIMIT 1",
				$post_id
			)
		);

		return is_numeric( $value ) ? (int) $value : 0;
	}

	private function getDefaultAttachmentId(): int {
		if ( $this->default_attachment_id !== -1 ) {
			return $this->default_attachment_id;
		}

		$settings = $this->moduleSettings()->all( [] );
		$id       = isset( $settings['attachment_id'] ) ? (int) $settings['attachment_id'] : 0;

		$this->default_attachment_id = self::resolveAttachmentId( $id );

		return $this->default_attachment_id;
	}

	public static function resolveAttachmentId( int $attachment_id ): int {
		if ( $attachment_id <= 0 ) {
			return 0;
		}

		if ( get_post_type( $attachment_id ) !== 'attachment' ) {
			return 0;
		}

		$mime = get_post_mime_type( $attachment_id );
		if ( ! is_string( $mime ) || ! str_starts_with( $mime, 'image/' ) ) {
			return 0;
		}

		$status = get_post_status( $attachment_id );
		if ( ! is_string( $status ) || ! in_array( $status, [ 'inherit', 'publish', 'private' ], true ) ) {
			return 0;
		}

		return $attachment_id;
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array{attachment_id: int}
	 */
	public static function normalizeSettings( array $settings ): array {
		$id = isset( $settings['attachment_id'] ) ? (int) $settings['attachment_id'] : 0;

		return [
			'attachment_id' => self::resolveAttachmentId( max( 0, $id ) ),
		];
	}
}
