<?php
/**
 * Scan post content for images missing alt attributes.
 *
 * @package AIAltTextGenerator
 * @since 1.7.0
 */

namespace AIAltTextGenerator;

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

/**
 * @since 1.7.0
 */
class ContentScanner {

	public const CRON_HOOK         = 'wpps_ai_alt_content_scan_batch';
	private const CACHE_KEY        = 'wpps_ai_alt_content_scan_v1';
	private const PROGRESS_OPTION  = 'wpps_ai_alt_content_scan_progress';
	private const CACHE_TTL        = HOUR_IN_SECONDS;
	private const QUICK_POST_LIMIT = 2000;
	private const BATCH_SIZE       = 50;

	/**
	 * @return array<string, mixed>
	 */
	public static function getSummary( bool $refresh = false ): array {
		$progress = self::getProgress();
		if ( ( $progress['status'] ?? '' ) === 'running' ) {
			return self::formatSummary( $progress );
		}

		if ( ! $refresh ) {
			$cached = get_transient( self::CACHE_KEY );
			if ( is_array( $cached ) ) {
				return $cached;
			}
		}

		$result = Performance::measure(
			'content_scan_quick',
			static fn() => self::runQuickScan(),
			[]
		);
		set_transient( self::CACHE_KEY, $result, self::CACHE_TTL );

		return $result;
	}

	public static function invalidateCache(): void {
		delete_transient( self::CACHE_KEY );
	}

	/**
	 * Start a full-site batched content scan (Pro).
	 *
	 * @return array<string, mixed>
	 */
	public static function startFullScan(): array {
		if ( ! FeatureGate::featureEnabled( 'full_content_scan' ) ) {
			return [
				'status'  => 'failed',
				'message' => __( 'Full-site content scans require a Pro license.', 'wp-powersuite' ),
			];
		}

		$post_types = self::getPostTypes();
		$total      = self::countPosts( $post_types );

		$progress = [
			'status'              => 'running',
			'scan_mode'           => 'full',
			'offset'              => 0,
			'total_posts'         => $total,
			'posts_scanned'       => 0,
			'posts_with_issues'   => 0,
			'images_missing_alt'  => 0,
			'issue_attachment_ids'=> [],
			'sample_issues'       => [],
			'started_at'          => gmdate( 'Y-m-d H:i:s' ),
			'last_scan'           => gmdate( 'Y-m-d H:i:s' ),
		];

		update_option( self::PROGRESS_OPTION, $progress, false );
		self::invalidateCache();
		self::scheduleBatch();

		return [
			'status'  => 'started',
			'message' => __( 'Full-site content scan started.', 'wp-powersuite' ),
			'progress'=> $progress,
		];
	}

	/**
	 * @return array{processed: int, remaining: int, complete: bool}
	 */
	public static function processBatch(): array {
		$progress = self::getProgress();
		if ( ( $progress['status'] ?? '' ) !== 'running' ) {
			return [
				'processed' => 0,
				'remaining' => 0,
				'complete'  => true,
			];
		}

		$post_types = self::getPostTypes();
		$posts      = get_posts(
			[
				'post_type'              => $post_types,
				'post_status'            => [ 'publish', 'draft', 'pending', 'future', 'private' ],
				'posts_per_page'         => self::BATCH_SIZE,
				'offset'                 => (int) ( $progress['offset'] ?? 0 ),
				'orderby'                => 'ID',
				'order'                  => 'ASC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			]
		);

		if ( $posts === [] ) {
			$progress['status'] = 'complete';
			update_option( self::PROGRESS_OPTION, $progress, false );
			$summary = self::formatSummary( $progress );
			set_transient( self::CACHE_KEY, $summary, self::CACHE_TTL );
			wp_clear_scheduled_hook( self::CRON_HOOK );

			return [
				'processed' => 0,
				'remaining' => 0,
				'complete'  => true,
			];
		}

		$batch_start = microtime( true );
		$id_set = [];
		foreach ( (array) ( $progress['issue_attachment_ids'] ?? [] ) as $existing_id ) {
			$id_set[ (int) $existing_id ] = true;
		}

		foreach ( $posts as $post ) {
			if ( ! $post instanceof \WP_Post ) {
				continue;
			}

			$post_issues = self::findIssuesInPost( $post );
			++$progress['posts_scanned'];

			if ( $post_issues === [] ) {
				continue;
			}

			$progress['images_missing_alt'] += count( $post_issues );
			++$progress['posts_with_issues'];

			foreach ( $post_issues as $issue ) {
				$id = (int) ( $issue['attachment_id'] ?? 0 );
				if ( $id > 0 ) {
					$id_set[ $id ] = true;
				}
			}

			if ( count( $progress['sample_issues'] ) < 20 ) {
				$progress['sample_issues'][] = [
					'post_id'  => $post->ID,
					'title'    => $post->post_title,
					'edit_url' => get_edit_post_link( $post->ID, 'raw' ) ?: '',
					'issues'   => $post_issues,
				];
			}
		}

		$progress['offset'] = (int) ( $progress['offset'] ?? 0 ) + count( $posts );
		$progress['issue_attachment_ids'] = array_map( 'intval', array_keys( $id_set ) );
		$progress['last_scan'] = gmdate( 'Y-m-d H:i:s' );

		$total   = (int) ( $progress['total_posts'] ?? 0 );
		$done    = (int) $progress['posts_scanned'];
		$complete = $total > 0 && $done >= $total;

		if ( $complete || count( $posts ) < self::BATCH_SIZE ) {
			$progress['status'] = 'complete';
			$summary            = self::formatSummary( $progress );
			set_transient( self::CACHE_KEY, $summary, self::CACHE_TTL );
			wp_clear_scheduled_hook( self::CRON_HOOK );
		} else {
			self::scheduleBatch();
		}

		update_option( self::PROGRESS_OPTION, $progress, false );

		Performance::record(
			'content_scan_batch',
			( microtime( true ) - $batch_start ) * 1000,
			[ 'processed' => count( $posts ) ]
		);

		return [
			'processed' => count( $posts ),
			'remaining' => max( 0, $total - $done ),
			'complete'  => $complete,
		];
	}

	/** @return array<string, mixed> */
	public static function getProgress(): array {
		$progress = get_option( self::PROGRESS_OPTION, [] );

		return is_array( $progress ) ? $progress : [];
	}

	public static function cancelFullScan(): void {
		delete_option( self::PROGRESS_OPTION );
		wp_clear_scheduled_hook( self::CRON_HOOK );
	}

	public static function scheduleBatch(): void {
		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_single_event( time() + 15, self::CRON_HOOK );
		}
	}

	/**
	 * @return array<string, mixed>
	 */
	private static function runQuickScan(): array {
		$post_types = self::getPostTypes();
		$posts      = self::getPostsLikelyContainingImages( $post_types, self::QUICK_POST_LIMIT );
		$summary    = self::scanPosts( $posts, 'quick' );
		$summary['posts_eligible'] = self::countPosts( $post_types );
		$summary['scan_limited']     = count( $posts ) >= self::QUICK_POST_LIMIT;

		return $summary;
	}

	/**
	 * Find posts that likely contain images (content, blocks, or page builders).
	 *
	 * @param list<string> $post_types
	 * @return list<\WP_Post>
	 */
	private static function getPostsLikelyContainingImages( array $post_types, int $limit ): array {
		global $wpdb;

		if ( $post_types === [] || $limit <= 0 ) {
			return [];
		}

		$type_place = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
		$statuses   = [ 'publish', 'draft', 'pending', 'future', 'private' ];
		$status_sql = "'" . implode( "','", array_map( 'esc_sql', $statuses ) ) . "'";

		$content_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT ID FROM {$wpdb->posts}
				WHERE post_type IN ($type_place)
				AND post_status IN ($status_sql)
				AND (
					post_content LIKE %s
					OR post_content LIKE %s
					OR post_content LIKE %s
					OR post_content LIKE %s
				)
				ORDER BY post_modified DESC
				LIMIT %d",
				...array_merge( $post_types, [ '%<img%', '%<!-- wp:image%', '%wp-image-%', '%core/image%', $limit ] )
			)
		);

		$elementor_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT p.ID
				FROM {$wpdb->posts} p
				INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = %s
				WHERE p.post_type IN ($type_place)
				AND p.post_status IN ($status_sql)
				AND (
					pm.meta_value LIKE %s
					OR pm.meta_value LIKE %s
				)
				ORDER BY p.post_modified DESC
				LIMIT %d",
				...array_merge(
					[ '_elementor_data' ],
					$post_types,
					[ '%"widgetType":"image"%', '%"url":%', $limit ]
				)
			)
		);

		$ordered = [];
		foreach ( array_merge( $content_ids ?: [], $elementor_ids ?: [] ) as $id ) {
			$id = (int) $id;
			if ( $id > 0 && ! isset( $ordered[ $id ] ) ) {
				$ordered[ $id ] = true;
				if ( count( $ordered ) >= $limit ) {
					break;
				}
			}
		}

		$posts = [];
		foreach ( array_keys( $ordered ) as $post_id ) {
			$post = get_post( (int) $post_id );
			if ( $post instanceof \WP_Post ) {
				$posts[] = $post;
			}
		}

		return $posts;
	}

	/**
	 * @param list<\WP_Post> $posts
	 * @return array<string, mixed>
	 */
	private static function scanPosts( array $posts, string $mode ): array {
		$issues              = [];
		$images_missing      = 0;
		$images_in_content   = 0;
		$posts_with_images   = 0;
		$attachment_ids      = [];

		foreach ( $posts as $post ) {
			if ( ! $post instanceof \WP_Post ) {
				continue;
			}

			$image_count = self::countImagesInPost( $post );
			if ( $image_count > 0 ) {
				++$posts_with_images;
				$images_in_content += $image_count;
			}

			$post_issues = self::findIssuesInPost( $post );
			if ( $post_issues === [] ) {
				continue;
			}

			$images_missing += count( $post_issues );
			foreach ( $post_issues as $issue ) {
				$id = (int) ( $issue['attachment_id'] ?? 0 );
				if ( $id > 0 ) {
					$attachment_ids[ $id ] = true;
				}
			}

			$issues[] = [
				'post_id'  => $post->ID,
				'title'    => $post->post_title,
				'edit_url' => get_edit_post_link( $post->ID, 'raw' ) ?: '',
				'issues'   => $post_issues,
			];
		}

		return [
			'scan_mode'            => $mode,
			'status'               => 'complete',
			'posts_scanned'        => count( $posts ),
			'posts_with_images'    => $posts_with_images,
			'posts_with_issues'    => count( $issues ),
			'images_in_content'    => $images_in_content,
			'images_missing_alt'   => $images_missing,
			'issue_attachment_ids' => array_map( 'intval', array_keys( $attachment_ids ) ),
			'sample_issues'        => array_slice( $issues, 0, 20 ),
			'last_scan'            => gmdate( 'Y-m-d H:i:s' ),
			'posts_eligible'       => 0,
			'scan_limited'         => false,
		];
	}

	/** @param array<string, mixed> $progress */
	private static function formatSummary( array $progress ): array {
		$ids = $progress['issue_attachment_ids'] ?? [];
		if ( is_array( $ids ) && ! array_is_list( $ids ) ) {
			$ids = array_map( 'intval', array_keys( $ids ) );
		}

		return [
			'scan_mode'            => (string) ( $progress['scan_mode'] ?? 'quick' ),
			'status'               => (string) ( $progress['status'] ?? 'complete' ),
			'total_posts'          => (int) ( $progress['total_posts'] ?? 0 ),
			'posts_scanned'        => (int) ( $progress['posts_scanned'] ?? 0 ),
			'posts_with_images'    => (int) ( $progress['posts_with_images'] ?? 0 ),
			'posts_with_issues'    => (int) ( $progress['posts_with_issues'] ?? 0 ),
			'images_in_content'    => (int) ( $progress['images_in_content'] ?? 0 ),
			'images_missing_alt'   => (int) ( $progress['images_missing_alt'] ?? 0 ),
			'issue_attachment_ids' => is_array( $ids ) ? array_map( 'intval', $ids ) : [],
			'sample_issues'        => is_array( $progress['sample_issues'] ?? null ) ? $progress['sample_issues'] : [],
			'last_scan'            => (string) ( $progress['last_scan'] ?? '' ),
			'started_at'           => (string) ( $progress['started_at'] ?? '' ),
			'posts_eligible'       => (int) ( $progress['posts_eligible'] ?? $progress['total_posts'] ?? 0 ),
			'scan_limited'         => ! empty( $progress['scan_limited'] ),
		];
	}

	/** @return list<string> */
	private static function getPostTypes(): array {
		$builtin = [ 'post', 'page' ];
		$custom  = [];

		foreach ( get_post_types( [ 'public' => true ], 'names' ) as $post_type ) {
			if ( in_array( $post_type, [ 'attachment', 'revision', 'nav_menu_item' ], true ) ) {
				continue;
			}
			$custom[] = $post_type;
		}

		$post_types = apply_filters(
			'wpps_ai_alt_text_content_scan_post_types',
			array_values( array_unique( array_merge( $builtin, $custom ) ) )
		);
		$post_types = array_values( array_filter( array_map( 'sanitize_key', (array) $post_types ) ) );

		return $post_types !== [] ? $post_types : [ 'post', 'page' ];
	}

	/** @param list<string> $post_types */
	private static function countPosts( array $post_types ): int {
		$counts = wp_count_posts();
		$total  = 0;

		foreach ( $post_types as $post_type ) {
			$type_counts = wp_count_posts( $post_type );
			if ( ! is_object( $type_counts ) ) {
				continue;
			}
			foreach ( [ 'publish', 'draft', 'pending', 'future', 'private' ] as $status ) {
				$total += (int) ( $type_counts->$status ?? 0 );
			}
		}

		return $total;
	}

	/**
	 * @return list<array{attachment_id: int, block: string, context: string}>
	 */
	private static function findIssuesInPost( \WP_Post $post ): array {
		$issues = [];

		if ( has_blocks( $post->post_content ) ) {
			$issues = array_merge( $issues, self::findIssuesInBlocks( parse_blocks( $post->post_content ) ) );
		}

		$issues = array_merge( $issues, self::findIssuesInClassicHtml( $post->post_content ) );
		$issues = array_merge( $issues, self::findIssuesInElementorMeta( (int) $post->ID ) );

		$unique = [];
		foreach ( $issues as $issue ) {
			$key = (int) $issue['attachment_id'] . ':' . (string) $issue['block'];
			if ( ! isset( $unique[ $key ] ) ) {
				$unique[ $key ] = $issue;
			}
		}

		return array_values( $unique );
	}

	private static function countImagesInPost( \WP_Post $post ): int {
		$count = 0;

		if ( has_blocks( $post->post_content ) ) {
			$count += self::countImagesInBlocks( parse_blocks( $post->post_content ) );
		}

		if ( stripos( $post->post_content, '<img' ) !== false && preg_match_all( '/<img\b[^>]*>/i', $post->post_content, $matches ) ) {
			$count += count( $matches[0] );
		}

		$count += self::countImagesInElementorMeta( (int) $post->ID );

		return $count;
	}

	/**
	 * @param list<array<string, mixed>> $blocks
	 */
	private static function countImagesInBlocks( array $blocks ): int {
		$count = 0;

		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}

			$name  = (string) ( $block['blockName'] ?? '' );
			$attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : [];

			if ( in_array( $name, [ 'core/image', 'core/cover', 'core/media-text' ], true ) && (int) ( $attrs['id'] ?? 0 ) > 0 ) {
				++$count;
			}

			if ( $name === 'core/gallery' && ! empty( $attrs['ids'] ) && is_array( $attrs['ids'] ) ) {
				$count += count( array_filter( array_map( 'intval', $attrs['ids'] ) ) );
			}

			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$count += self::countImagesInBlocks( $block['innerBlocks'] );
			}
		}

		return $count;
	}

	private static function countImagesInElementorMeta( int $post_id ): int {
		$raw = get_post_meta( $post_id, '_elementor_data', true );
		if ( ! is_string( $raw ) || $raw === '' ) {
			return 0;
		}

		$data = json_decode( $raw, true );

		return is_array( $data ) ? self::countImagesInElementorNodes( $data ) : 0;
	}

	/**
	 * @param list<array<string, mixed>> $nodes
	 */
	private static function countImagesInElementorNodes( array $nodes ): int {
		$count = 0;

		foreach ( $nodes as $node ) {
			if ( ! is_array( $node ) ) {
				continue;
			}

			$widget   = (string) ( $node['widgetType'] ?? '' );
			$settings = is_array( $node['settings'] ?? null ) ? $node['settings'] : [];

			if ( in_array( $widget, [ 'image', 'image-box', 'image-gallery', 'image-carousel', 'media-carousel' ], true ) ) {
				$id = (int) ( $settings['image']['id'] ?? $settings['image_id'] ?? 0 );
				if ( $id > 0 ) {
					++$count;
				}
			}

			if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
				$count += self::countImagesInElementorNodes( $node['elements'] );
			}
		}

		return $count;
	}

	/**
	 * @return list<array{attachment_id: int, block: string, context: string}>
	 */
	private static function findIssuesInElementorMeta( int $post_id ): array {
		$raw = get_post_meta( $post_id, '_elementor_data', true );
		if ( ! is_string( $raw ) || $raw === '' ) {
			return [];
		}

		$data = json_decode( $raw, true );
		if ( ! is_array( $data ) ) {
			return [];
		}

		return self::findIssuesInElementorNodes( $data );
	}

	/**
	 * @param list<array<string, mixed>> $nodes
	 * @return list<array{attachment_id: int, block: string, context: string}>
	 */
	private static function findIssuesInElementorNodes( array $nodes ): array {
		$issues = [];

		foreach ( $nodes as $node ) {
			if ( ! is_array( $node ) ) {
				continue;
			}

			$widget = (string) ( $node['widgetType'] ?? '' );
			$settings = is_array( $node['settings'] ?? null ) ? $node['settings'] : [];

			if ( in_array( $widget, [ 'image', 'image-box', 'image-gallery', 'image-carousel', 'media-carousel' ], true ) ) {
				$id  = (int) ( $settings['image']['id'] ?? $settings['image_id'] ?? 0 );
				$alt = trim( (string) ( $settings['image']['alt'] ?? $settings['alt'] ?? '' ) );
				if ( $id > 0 && $alt === '' ) {
					$issues[] = [
						'attachment_id' => $id,
						'block'         => 'elementor-' . $widget,
						'context'       => 'elementor',
					];
				}
			}

			if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
				$issues = array_merge( $issues, self::findIssuesInElementorNodes( $node['elements'] ) );
			}
		}

		return $issues;
	}

	/**
	 * @param list<array<string, mixed>> $blocks
	 * @return list<array{attachment_id: int, block: string, context: string}>
	 */
	private static function findIssuesInBlocks( array $blocks ): array {
		$issues = [];

		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}

			$name  = (string) ( $block['blockName'] ?? '' );
			$attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : [];

			if ( in_array( $name, [ 'core/image', 'core/cover', 'core/media-text' ], true ) ) {
				$id  = (int) ( $attrs['id'] ?? 0 );
				$alt = trim( (string) ( $attrs['alt'] ?? '' ) );
				if ( $id > 0 && $alt === '' ) {
					$issues[] = [
						'attachment_id' => $id,
						'block'         => $name,
						'context'       => 'gutenberg',
					];
				}
			}

			if ( $name === 'core/gallery' && ! empty( $attrs['ids'] ) && is_array( $attrs['ids'] ) ) {
				foreach ( $attrs['ids'] as $id ) {
					$id = (int) $id;
					if ( $id > 0 ) {
						$library_alt = trim( (string) get_post_meta( $id, '_wp_attachment_image_alt', true ) );
						if ( $library_alt === '' ) {
							$issues[] = [
								'attachment_id' => $id,
								'block'         => $name,
								'context'       => 'gutenberg-gallery',
							];
						}
					}
				}
			}

			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$issues = array_merge( $issues, self::findIssuesInBlocks( $block['innerBlocks'] ) );
			}
		}

		return $issues;
	}

	/**
	 * @return list<array{attachment_id: int, block: string, context: string}>
	 */
	private static function findIssuesInClassicHtml( string $content ): array {
		if ( $content === '' || stripos( $content, '<img' ) === false ) {
			return [];
		}

		$issues = [];

		if ( preg_match_all( '/<img\b[^>]*>/i', $content, $matches ) ) {
			foreach ( $matches[0] as $tag ) {
				$attachment_id = 0;
				if ( preg_match( '/\bclass=["\'][^"\']*\bwp-image-(\d+)\b/i', $tag, $class_match ) ) {
					$attachment_id = (int) $class_match[1];
				}

				$alt = '';
				if ( preg_match( '/\balt=["\']([^"\']*)["\']/', $tag, $alt_match ) ) {
					$alt = trim( $alt_match[1] );
				}

				if ( $alt !== '' ) {
					continue;
				}

				if ( $attachment_id <= 0 && preg_match( '/\bsrc=["\']([^"\']+)["\']/', $tag, $src_match ) ) {
					$attachment_id = self::attachmentIdFromUrl( (string) $src_match[1] );
				}

				$settings = Settings::get();
				$lazy_attr = trim( (string) ( $settings['lazy_load_src_attr'] ?? '' ) );
				if ( $attachment_id <= 0 && $lazy_attr !== '' && preg_match( '/\b' . preg_quote( $lazy_attr, '/' ) . '=["\']([^"\']+)["\']/i', $tag, $lazy_match ) ) {
					$attachment_id = self::attachmentIdFromUrl( (string) $lazy_match[1] );
				}

				if ( $attachment_id > 0 ) {
					$issues[] = [
						'attachment_id' => $attachment_id,
						'block'         => 'img',
						'context'       => 'classic',
					];
				} elseif ( ! empty( Settings::get()['refresh_external_images'] ) && preg_match( '/\bsrc=["\']([^"\']+)["\']/', $tag, $src_match ) ) {
					$url = trim( (string) $src_match[1] );
					if ( $url !== '' && self::attachmentIdFromUrl( $url ) <= 0 ) {
						$issues[] = [
							'attachment_id' => 0,
							'block'         => 'img-external',
							'context'       => 'classic-external',
							'image_url'     => $url,
						];
					}
				}
			}
		}

		return $issues;
	}

	public static function attachmentIdFromUrl( string $url ): int {
		$url = trim( $url );
		if ( $url === '' ) {
			return 0;
		}

		$id = (int) attachment_url_to_postid( $url );
		if ( $id > 0 ) {
			return $id;
		}

		$path = wp_parse_url( $url, PHP_URL_PATH );
		if ( ! is_string( $path ) || $path === '' ) {
			return 0;
		}

		$filename = basename( $path );
		if ( $filename === '' ) {
			return 0;
		}

		global $wpdb;
		$found = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT post_id FROM {$wpdb->postmeta}
				WHERE meta_key = '_wp_attached_file' AND meta_value LIKE %s
				ORDER BY post_id DESC LIMIT 1",
				'%' . $wpdb->esc_like( $filename )
			)
		);

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