<?php
/**
 * SEO Audit Module - Crawl Pages and Prepare API Endpoint (rendered or wp-meta)
 *
 * ✅ Uses unified auth from: includes/sac-utilities.php
 * - permission_callback => myaio_sac_rest_permission_check
 *
 * IMPORTANT:
 * - Ensure sac-utilities.php is loaded BEFORE this file.
 */

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

/* -------------------------------------------------------------------------
 * REST ROUTE (Unified Auth)
 * ------------------------------------------------------------------------- */
add_action('rest_api_init', function () {
	register_rest_route('myaio/v1', '/crawl-website', [
		'methods'             => 'GET',
		'callback'            => 'myaio_sac_crawl_website',
		'permission_callback' => 'myaio_sac_rest_permission_check',
	]);
});

/* -------------------------------------------------------------------------
 * STRICT Header/Footer Heading Exclusion
 * ------------------------------------------------------------------------- */
if ( ! function_exists('myaio_sac_heading_is_in_header_or_footer') ) {
	function myaio_sac_heading_is_in_header_or_footer(DOMElement $el) {
		$node = $el;

		while ($node && $node->nodeType === XML_ELEMENT_NODE) {
			$tag = strtolower($node->nodeName);

			// Strict: only remove headings inside actual header/footer tags
			if ($tag === 'header' || $tag === 'footer') {
				return true;
			}

			// Elementor: remove headings inside Elementor header/footer template areas
			$cls = strtolower((string) $node->getAttribute('class'));
			if ($cls) {
				if (strpos($cls, 'elementor-location-header') !== false) {
					return true;
				}
				if (strpos($cls, 'elementor-location-footer') !== false) {
					return true;
				}
			}

			$node = ($node->parentNode instanceof DOMElement) ? $node->parentNode : null;
		}

		return false;
	}
}

/* -------------------------------------------------------------------------
 * MAIN CALLBACK
 * ------------------------------------------------------------------------- */
function myaio_sac_crawl_website(WP_REST_Request $request) {
	try {
		// ------- Params -------
		$source = $request->get_param('source') ?: 'rendered'; // rendered | wp
		if (!in_array($source, ['rendered', 'wp'], true)) {
			$source = 'rendered';
		}

		$post_type = $request->get_param('post_type') ?: ['post', 'page'];
		if (!is_array($post_type)) {
			$post_type = in_array($post_type, ['post', 'page'], true) ? [$post_type] : ['post', 'page'];
		}

		$status = $request->get_param('status') ?: ['publish'];
		if (!is_array($status)) {
			$status = [$status];
		}

		$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 25)));
		$page     = max(1, (int) ($request->get_param('page') ?: 1));
		$timeout  = max(3, min(30, (int) ($request->get_param('timeout') ?: 10)));
		$fields   = myaio_sac_parse_fields($request->get_param('fields'));

		// ------- Query with pagination -------
		$q = new WP_Query([
			'post_type'      => $post_type,
			'post_status'    => $status,
			'posts_per_page' => $per_page,
			'paged'          => $page,
			'fields'         => 'ids',
			'orderby'        => 'date',
			'order'          => 'DESC',
			'no_found_rows'  => false,
		]);

		$sitemap_urls = myaio_sac_get_urls_from_check_sitemap_response();

		$items = [];
		foreach ((array) $q->posts as $post_id) {
			if ($source === 'rendered') {
				$items[] = myaio_sac_audit_rendered($post_id, $timeout, $fields, $sitemap_urls);
			} else {
				$items[] = myaio_sac_audit_wpmeta($post_id, $fields);
			}
		}

		return rest_ensure_response([
			'page'        => $page,
			'per_page'    => $per_page,
			'total'       => (int) $q->found_posts,
			'total_pages' => (int) $q->max_num_pages,
			'source'      => $source,
			'results'     => $items,
		]);

	} catch (Throwable $e) {
		return new WP_REST_Response([
			'error'   => 'Internal Server Error',
			'message' => $e->getMessage(),
		], 500);
	}
}

/* -------------------------------------------------------------------------
 * FIELD FILTERING
 * ------------------------------------------------------------------------- */
function myaio_sac_parse_fields($csv) {
	if (!$csv || strtolower((string) $csv) === 'all') {
		return [];
	}
	$arr = array_filter(array_map('trim', explode(',', (string) $csv)));
	return array_flip($arr);
}

function myaio_sac_filter_fields($arr, $fields) {
	if (empty($fields)) {
		return $arr;
	}
	return array_intersect_key($arr, $fields);
}

/* =========================
 * MODE 1: RENDERED
 * ========================= */
function myaio_sac_audit_rendered($post_id, $timeout, $fields, $sitemap_urls = []) {
	$url = get_permalink($post_id);

	$resp = wp_safe_remote_get($url, [
		'timeout'     => $timeout,
		'redirection' => 3,
		'headers'     => [
			'User-Agent' => 'MYAIO-SEO-Audit/1.1 (+WordPress)',
			'Accept'     => 'text/html,*/*;q=0.8',
		],
	]);

	$status = is_wp_error($resp) ? 0 : (int) wp_remote_retrieve_response_code($resp);
	$html   = is_wp_error($resp) ? '' : (string) wp_remote_retrieve_body($resp);

	$doc  = myaio_sac_make_dom($html);
	$head = $doc ? $doc->getElementsByTagName('head')->item(0) : null;
	$body = $doc ? $doc->getElementsByTagName('body')->item(0) : null;

	// RAW title from <title>
	$title_raw = myaio_sac_dom_title($head);

	// CLEAN title for API output
	$title = myaio_sac_clean_document_title($title_raw);

	$meta                  = myaio_sac_dom_basic_meta($head);
	$canon                 = myaio_sac_dom_canonical($head);
	$robots                = myaio_sac_dom_robots($head);
	$normalized_url        = myaio_sac_normalize_url_for_compare($url);
	$check_sitemap_present = ! empty($sitemap_urls);
	$in_sitemap            = in_array($normalized_url, $sitemap_urls, true);
	$og                    = myaio_sac_dom_meta_group($head, 'property', 'og:');
	$tw                    = myaio_sac_dom_meta_group($head, 'name', 'twitter:');
	$hlang                 = myaio_sac_dom_hreflang($head);

	// Headings: exclude header/footer + exclude headings equal to post title
	$content_root = myaio_sac_find_content_root($doc, $body);
	$headings     = myaio_sac_dom_headings($body, get_the_title($post_id));

	$images_all = myaio_sac_dom_images_all($body);

	$imgs_no_alt = array_values(array_unique(array_map(function ($i) {
		return !empty($i['has_alt']) ? null : ($i['src_normalized'] ?: $i['src']);
	}, $images_all)));
	$imgs_no_alt = array_values(array_filter($imgs_no_alt));

	$word_count = myaio_sac_word_count($body);
	$desc       = trim((string) ($meta['description'] ?? ''));

	// issues
	$issues = [];
	if ($status >= 400) {
		$issues[] = "HTTP $status";
	}
	if ($title === '') {
		$issues[] = 'Missing title tag';
	}
	if (mb_strlen($title) > 60) {
		$issues[] = 'Title exceeds 60 characters';
	}
	if ($desc === '') {
		$issues[] = 'Missing meta description';
	} elseif (mb_strlen($desc) > 160) {
		$issues[] = 'Meta description exceeds 160 characters';
	}
	if ((string) ($meta['keywords'] ?? '') === '') {
		$issues[] = 'Missing meta keywords';
	}
	if (trim((string) $canon) === '') {
		$issues[] = 'Missing canonical link';
	}

	// Twitter Card Issues
	if (empty($tw['twitter:card'])) {
		$issues[] = 'Missing Twitter Card';
	}
	if (empty($tw['twitter:title'])) {
		$issues[] = 'Missing Twitter Card Title';
	}
	if (empty($tw['twitter:description'])) {
		$issues[] = 'Missing Twitter Card Description';
	}
	if (empty($tw['twitter:site'])) {
		$issues[] = 'Missing Twitter Site';
	}

	// Open Graph Issues
	if (empty($og['og:title'])) {
		$issues[] = 'Missing Open Graph Title';
	}
	if (empty($og['og:description'])) {
		$issues[] = 'Missing Open Graph Description';
	}
	if (empty($og['og:url'])) {
		$issues[] = 'Missing Open Graph Url';
	}

	$h1_count = count($headings['h1']);
	if ($h1_count === 0) {
		$issues[] = 'Missing h1 tag';
	}
	if ($h1_count > 1) {
		$issues[] = "Multiple h1 tags ($h1_count)";
	}
	foreach ($imgs_no_alt as $src) {
		$issues[] = "Image missing alt: $src";
	}

	$result = [
		'url'                     => $url,
		'type'                    => get_post_type($post_id),
		'http_status'             => $status ?: null,

		// raw vs cleaned
		'title_raw'               => $title_raw,
		'title'                   => $title,

		'title_length'            => mb_strlen((string) $title),
		'meta_description'        => (string) ($meta['description'] ?? ''),
		'meta_description_length' => mb_strlen((string) ($meta['description'] ?? '')),
		'meta_keywords'           => (string) ($meta['keywords'] ?? ''),
		'meta_keywords_length'    => mb_strlen((string) ($meta['keywords'] ?? '')),
		'robots'                  => $robots,
		'in_sitemap'  => $in_sitemap,
		'check_sitemap_present'   => $check_sitemap_present,
		'canonical'               => $canon,
		'og'                      => $og,
		'twitter'                 => $tw,
		'hreflang'                => $hlang,
		'headings'                => $headings,
		'images'                  => $images_all,
		'images_missing_alt'      => $imgs_no_alt,
		'word_count'              => $word_count,
		'issues'                  => $issues,
	];

	return myaio_sac_filter_fields($result, $fields);
}

/* =========================
 * MODE 2: WP META
 * ========================= */
function myaio_sac_audit_wpmeta($post_id, $fields) {
	$post = get_post($post_id);
	if (!$post) {
		return [];
	}

	$url   = get_permalink($post_id);
	$title = myaio_sac_meta_title($post);
	$desc  = myaio_sac_meta_description($post);
	$keys  = myaio_sac_meta_keywords($post);

	// parse rendered content (shortcodes/blocks run)
	$content = apply_filters('myaio_the_content', $post->post_content);
	$doc     = myaio_sac_make_dom($content);

	$_node = $doc ? $doc->getElementsByTagName('body')->item(0) : $doc;

	$headings = myaio_sac_dom_headings($_node, get_the_title($post_id));

	$images_all = myaio_sac_dom_images_all($_node);
	$imgs_no_alt = array_values(array_unique(array_map(function ($i) {
		return !empty($i['has_alt']) ? null : ($i['src_normalized'] ?: $i['src']);
	}, $images_all)));
	$imgs_no_alt = array_values(array_filter($imgs_no_alt));

	$issues = [];
	if ($title === '') {
		$issues[] = 'Missing title tag';
	}
	if (mb_strlen($title) > 60) {
		$issues[] = 'Title exceeds 60 characters';
	}
	if ($desc === '') {
		$issues[] = 'Missing meta description';
	}
	if (mb_strlen($desc) > 160) {
		$issues[] = 'Meta description exceeds 160 characters';
	}
	if ($keys === '') {
		$issues[] = 'Missing meta keywords';
	}

	$h1_count = count($headings['h1']);
	if ($h1_count === 0) {
		$issues[] = 'Missing h1 tag';
	}
	if ($h1_count > 1) {
		$issues[] = "Multiple h1 tags ($h1_count)";
	}
	foreach ($imgs_no_alt as $src) {
		$issues[] = "Image missing alt: $src";
	}

	$result = [
		'url'                     => $url,
		'type'                    => $post->post_type,
		'title'                   => $title,
		'title_length'            => mb_strlen($title),
		'meta_description'        => $desc,
		'meta_description_length' => mb_strlen($desc),
		'meta_keywords'           => $keys,
		'meta_keywords_length'    => mb_strlen((string) $keys),
		'headings'                => $headings,
		'images'                  => $images_all,
		'images_missing_alt'      => $imgs_no_alt,
		'issues'                  => $issues,
	];

	return myaio_sac_filter_fields($result, $fields);
}

/* =========================
 * META RESOLVERS (Yoast / Rank Math / AIOSEO)
 * ========================= */
function myaio_sac_meta_title(WP_Post $post) {
	foreach (['_yoast_wpseo_title', 'rank_math_title', '_aioseo_title'] as $k) {
		$v = get_post_meta($post->ID, $k, true);
		if ($v) {
			return wp_strip_all_tags(myaio_sac_replace_title_vars($v, $post));
		}
	}
	return get_the_title($post);
}

function myaio_sac_meta_description(WP_Post $post) {
	foreach (['_yoast_wpseo_metadesc', 'rank_math_description', '_aioseo_description'] as $k) {
		$v = get_post_meta($post->ID, $k, true);
		if ($v) {
			return wp_strip_all_tags($v);
		}
	}
	if ($post->post_excerpt) {
		return wp_strip_all_tags($post->post_excerpt);
	}
	$content = wp_strip_all_tags($post->post_content);
	return mb_substr(trim(preg_replace('/\s+/', ' ', $content)), 0, 160);
}

function myaio_sac_meta_keywords(WP_Post $post) {
	foreach (['_aioseo_keywords', '_yoast_wpseo_focuskw', 'rank_math_focus_keyword'] as $k) {
		$v = get_post_meta($post->ID, $k, true);
		if ($v) {
			return is_array($v) ? implode(',', $v) : (string) $v;
		}
	}
	return '';
}

function myaio_sac_replace_title_vars($template, WP_Post $post) {
	$site = get_bloginfo('name');
	$sep  = '–';
	$repl = [
		'%%title%%'    => get_the_title($post),
		'%%sitename%%' => $site,
		'%%sep%%'      => $sep,
	];
	return strtr((string) $template, $repl);
}

/* =========================
 * SAFE HELPERS (no redeclare + safe DOMXPath)
 * ========================= */
if ( ! function_exists('myaio_sac_find_content_root') ) {
	/**
	 * Try to find the "real content" container so we don't collect header/footer headings.
	 * Works for common WP themes + Elementor.
	 */
	function myaio_sac_find_content_root($doc, $body) {
		if (!$doc || !$body) {
			return $body;
		}

		if (!class_exists('DOMXPath')) {
			return $body;
		}

		$xpath = new DOMXPath($doc);

		$queries = [
			"//*[self::main][1]",
			"//*[self::article][1]",
			"//*[@id='content'][1]",
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' site-content ')][1]",
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' content-area ')][1]",
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' entry-content ')][1]",
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' wp-block-post-content ')][1]",

			// Elementor
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' elementor-location-single ')][1]",
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' elementor-widget-theme-post-content ')][1]",
			"//*[contains(concat(' ', normalize-space(@class), ' '), ' elementor-section ')][1]",
		];

		foreach ($queries as $q) {
			$nodelist = $xpath->query($q);
			if ($nodelist && $nodelist->length) {
				$node = $nodelist->item(0);
				if ($node instanceof DOMElement) {
					return $node;
				}
			}
		}

		return $body;
	}
}

if ( ! function_exists('myaio_sac_node_is_in_excluded_area') ) {
	/**
	 * Detect if a node is inside header/footer/nav/sidebar-like areas.
	 * Keep regex conservative so we don't exclude real content.
	 */
	function myaio_sac_node_is_in_excluded_area(DOMElement $node) {
		$excluded_tags = ['header', 'footer', 'nav', 'aside'];

		$re = '/\b(site-header|site-footer|header|footer|nav|navbar|sidebar|breadcrumb|pagination|pager|'
			. 'cookie|consent|modal|popup|drawer|offcanvas|topbar|bottombar|masthead)\b/i';

		$cur = $node;
		while ($cur && $cur->nodeType === XML_ELEMENT_NODE) {
			$tag = strtolower($cur->nodeName);
			if (in_array($tag, $excluded_tags, true)) {
				return true;
			}

			$id  = (string) $cur->getAttribute('id');
			$cls = (string) $cur->getAttribute('class');

			if (($id && preg_match($re, $id)) || ($cls && preg_match($re, $cls))) {
				return true;
			}

			$cur = $cur->parentNode instanceof DOMElement ? $cur->parentNode : null;
		}

		return false;
	}
}

/* =========================
 * TITLE CLEANER (remove sitename suffix/prefix)
 * ========================= */
function myaio_sac_clean_document_title($raw_title) {
	$t = trim((string) $raw_title);
	if ($t === '') {
		return '';
	}

	$site = trim((string) get_bloginfo('name'));
	if ($site === '') {
		return $t;
	}

	$site_q = preg_quote($site, '/');

	// Remove: "Page Title | SiteName" OR "Page Title - SiteName" OR "Page Title – SiteName" OR "Page Title — SiteName" OR "Page Title : SiteName"
	$t = preg_replace('/\s*[\|\-–—:]\s*' . $site_q . '\s*$/u', '', $t);

	// Also remove prefix: "SiteName | Page Title"
	$t = preg_replace('/^' . $site_q . '\s*[\|\-–—:]\s*/u', '', $t);

	return trim((string) $t);
}

/* =========================
 * DOM HELPERS
 * ========================= */
function myaio_sac_make_dom($html) {
	if (!$html) {
		return null;
	}
	$doc = new DOMDocument();
	libxml_use_internal_errors(true);
	$doc->loadHTML('<?xml encoding="utf-8" ?>' . $html);
	libxml_clear_errors();
	return $doc;
}

function myaio_sac_dom_inner_html($el) {
	if (!$el) {
		return '';
	}
	$html = '';
	foreach ($el->childNodes as $child) {
		$html .= $el->ownerDocument->saveHTML($child);
	}
	return trim($html);
}

function myaio_sac_dom_title($head) {
	if (!$head) {
		return '';
	}
	$t = $head->getElementsByTagName('title');
	return $t->length ? trim((string) $t->item(0)->textContent) : '';
}

function myaio_sac_dom_basic_meta($head) {
	$out = ['description' => '', 'keywords' => ''];
	if (!$head) {
		return $out;
	}
	foreach ($head->getElementsByTagName('meta') as $m) {
		$name = strtolower((string) $m->getAttribute('name'));
		if ($name === 'description') {
			$out['description'] = trim((string) $m->getAttribute('content'));
		}
		if ($name === 'keywords') {
			$out['keywords'] = trim((string) $m->getAttribute('content'));
		}
	}
	return $out;
}

function myaio_sac_dom_robots($head) {
	if (!$head) {
		return '';
	}

	$all_directives = [];

	foreach ($head->getElementsByTagName('meta') as $m) {
		if (strtolower((string) $m->getAttribute('name')) !== 'robots') {
			continue;
		}

		$content = trim((string) $m->getAttribute('content'));
		if ($content === '') {
			continue;
		}

		$parts = array_map('trim', explode(',', strtolower($content)));
		foreach ($parts as $part) {
			if ($part !== '') {
				$all_directives[] = $part;
			}
		}
	}

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

	return myaio_sac_resolve_robots_directives($all_directives);
}

function myaio_sac_resolve_robots_directives($directives) {
	$directives = array_values(array_unique(array_filter(array_map(function ($d) {
		return strtolower(trim((string) $d));
	}, (array) $directives))));

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

	$has_noindex  = in_array('noindex', $directives, true);
	$has_index    = in_array('index', $directives, true);
	$has_nofollow = in_array('nofollow', $directives, true);
	$has_follow   = in_array('follow', $directives, true);

	$resolved = [];

	// Most restrictive wins
	if ($has_noindex) {
		$resolved[] = 'noindex';
	} elseif ($has_index) {
		$resolved[] = 'index';
	}

	if ($has_nofollow) {
		$resolved[] = 'nofollow';
	} elseif ($has_follow) {
		$resolved[] = 'follow';
	}

	// Keep all other non-conflicting directives
	$conflict_pairs = ['index', 'noindex', 'follow', 'nofollow'];

	foreach ($directives as $directive) {
		if (!in_array($directive, $conflict_pairs, true)) {
			$resolved[] = $directive;
		}
	}

	$resolved = array_values(array_unique($resolved));

	return implode(', ', $resolved);
}

function myaio_sac_dom_canonical($head) {
	if (!$head) {
		return '';
	}
	foreach ($head->getElementsByTagName('link') as $l) {
		if (strtolower((string) $l->getAttribute('rel')) === 'canonical') {
			return trim((string) $l->getAttribute('href'));
		}
	}
	return '';
}

function myaio_sac_dom_meta_group($head, $attr, $prefix) {
	$data = [];
	if (!$head) {
		return $data;
	}
	foreach ($head->getElementsByTagName('meta') as $m) {
		$k = (string) $m->getAttribute($attr);
		if ($k && stripos($k, (string) $prefix) === 0) {
			$data[$k] = (string) $m->getAttribute('content');
		}
	}
	return $data;
}

function myaio_sac_dom_hreflang($head) {
	$arr = [];
	if (!$head) {
		return $arr;
	}
	foreach ($head->getElementsByTagName('link') as $l) {
		if (strtolower((string) $l->getAttribute('rel')) === 'alternate' && $l->hasAttribute('hreflang')) {
			$arr[] = [
				'lang' => (string) $l->getAttribute('hreflang'),
				'href' => (string) $l->getAttribute('href'),
			];
		}
	}
	return $arr;
}

/* =========================
 * HEADING FILTERING
 * - Excludes header/footer areas
 * - Excludes headings equal to post/page title
 * ========================= */
function myaio_sac_dom_headings($node_or_body, $post_title = '') {
	$limits = ['h1' => 60, 'h2' => 70, 'h3' => 70, 'h4' => 70, 'h5' => 70, 'h6' => 70];
	$out    = ['h1' => [], 'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [], 'h6' => []];

	if (!$node_or_body) {
		return $out;
	}

	$post_title_norm = myaio_sac_norm_text($post_title);
	$root = $node_or_body;

	foreach (array_keys($out) as $tag) {
		$nodes = $root->getElementsByTagName($tag);
		$index = 1;

		foreach ($nodes as $n) {
			if (!($n instanceof DOMElement)) {
				continue;
			}

			// Skip header/footer headings
			if (myaio_sac_heading_is_in_header_or_footer($n)) {
				continue;
			}

			$text = trim((string) $n->textContent);
			if ($text === '') {
				continue;
			}

			// Skip heading if it matches page/post title
			$heading_norm = myaio_sac_norm_text($text);
			if ($post_title_norm && $heading_norm === $post_title_norm) {
				continue;
			}

			$len        = mb_strlen($text);
			$inner_html = myaio_sac_dom_inner_html($n);

			$out[$tag][] = [
				'index'        => $index,
				'text'         => $text,
				'html'         => $inner_html,
				'length'       => $len,
				'within_limit' => ($len <= ($limits[$tag] ?? 70)),
			];

			$index++;
		}
	}

	return $out;
}

if ( ! function_exists('myaio_sac_norm_text') ) {
	function myaio_sac_norm_text($s) {
		$s = wp_strip_all_tags((string) $s);
		$s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
		$s = preg_replace('/\s+/u', ' ', $s);
		return mb_strtolower(trim((string) $s));
	}
}

/* =========================
 * IMAGES
 * - parse ONLY meaningful content images (exclude header/footer/decorative/small/svg)
 * ========================= */
function myaio_sac_dom_images_all($node_or_body) {
	if (!$node_or_body) {
		return [];
	}

	$images = [];
	$imgs   = $node_or_body->getElementsByTagName('img');

	foreach ($imgs as $img) {
		if (!($img instanceof DOMElement)) {
			continue;
		}

		// 1) Exclude images located in header/footer/nav/etc.
		if (myaio_sac_img_is_in_excluded_area($img)) {
			continue;
		}

		// 2) Resolve src (supports common lazy-load attributes)
		$src = trim((string) $img->getAttribute('data-src'));
		if (!$src) {
			$src = trim((string) $img->getAttribute('data-lazy-src'));
		}
		if (!$src) {
			$src = trim((string) $img->getAttribute('data-original'));
		}
		if (!$src) {
			$src = trim((string) $img->getAttribute('data-src-large'));
		}
		if (!$src) {
			$src = trim((string) $img->getAttribute('src'));
		}

		// srcset fallbacks
		$srcset_raw = trim((string) $img->getAttribute('srcset'));
		if (!$srcset_raw) {
			$srcset_raw = trim((string) $img->getAttribute('data-srcset'));
		}
		if (!$srcset_raw) {
			$srcset_raw = trim((string) $img->getAttribute('data-lazy-srcset'));
		}

		// if src missing, try first srcset candidate
		if (!$src && $srcset_raw) {
			$first = explode(',', $srcset_raw)[0];
			$src   = trim(explode(' ', trim($first))[0]);
		}
		if (!$src) {
			continue;
		}

		$src_abs = myaio_sac_abs_urls($src);

		// 3) Exclude SVGs
		if (myaio_sac_is_svg_url($src_abs)) {
			continue;
		}

		// 4) Exclude hidden/decorative images (icons/logos/ui)
		if (myaio_sac_img_is_hidden($img)) {
			continue;
		}
		if (myaio_sac_img_is_decorative($img, $src_abs)) {
			continue;
		}

		// 5) Exclude small images (< 100px x 100px) when dims are available
		$dims = myaio_sac_img_get_dimensions_px($img);
		if (myaio_sac_img_is_too_small($dims['width'], $dims['height'], 100, 100)) {
			continue;
		}

		// normalize WP sized filenames -WxH.ext
		$src_normalized = $src_abs
			? preg_replace('/-\d+x\d+(?=\.(jpg|jpeg|png|gif|webp|avif))/i', '', $src_abs)
			: '';

		$alt     = trim((string) $img->getAttribute('alt'));
		$has_alt = ($alt !== '');

		$item = [
			'src'            => $src_abs,
			'src_normalized' => $src_normalized,
			'alt'            => $alt,
			'has_alt'        => $has_alt,
			'size_bytes'     => myaio_sac_image_size_bytes($src_abs),
			'size_kb'        => myaio_sac_image_size_kb($src_abs),
		];

		// Allow consumers to override inclusion/exclusion. Return false to exclude.
		$keep = apply_filters('myaio_audit_keep_image', true, $item, $img);
		if ($keep === false) {
			continue;
		}

		$images[] = $item;
	}

	// de-dup identical src+alt combos
	$images = array_values(array_unique(array_map(function ($i) {
		return wp_json_encode($i, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
	}, $images)));

	$images = array_map(function ($j) {
		return json_decode($j, true);
	}, $images);

	return $images;
}

function myaio_sac_is_svg_url($url) {
	$u = strtolower((string) $url);
	if (strpos($u, 'data:image/svg+xml') !== false) {
		return true;
	}
	$u2 = preg_replace('/\?.*$/', '', $u); // strip query for extension check
	return (substr($u2, -4) === '.svg');
}

// Strict exclusion: only true header/footer/nav/aside (and well-known header/footer wrappers)
function myaio_sac_img_is_in_excluded_area(DOMElement $img) {
	$node = $img;

	$excluded_tags = ['header', 'footer', 'nav', 'aside'];

	$strong_re = '/\b('
		. 'elementor-location-header|elementor-location-footer|'
		. 'site-header|site-footer|'
		. 'wp-block-navigation|'
		. 'ast-header|ast-footer|'
		. 'header-wrapper|footer-wrapper'
		. ')\b/i';

	while ($node && $node->nodeType === XML_ELEMENT_NODE) {
		if (!($node instanceof DOMElement)) {
			break;
		}

		$tag = strtolower($node->nodeName);
		if (in_array($tag, $excluded_tags, true)) {
			return true;
		}

		$role = strtolower((string) $node->getAttribute('role'));
		if (in_array($role, ['navigation', 'banner', 'contentinfo'], true)) {
			return true;
		}

		$id  = (string) $node->getAttribute('id');
		$cls = (string) $node->getAttribute('class');
		if (($id && preg_match($strong_re, $id)) || ($cls && preg_match($strong_re, $cls))) {
			return true;
		}

		$node = ($node->parentNode instanceof DOMElement) ? $node->parentNode : null;
	}

	return false;
}

function myaio_sac_img_is_hidden(DOMElement $img) {
	$style = strtolower((string) $img->getAttribute('style'));
	if ($style) {
		if (strpos($style, 'display:none') !== false) {
			return true;
		}
		if (strpos($style, 'visibility:hidden') !== false) {
			return true;
		}
		if (preg_match('/opacity\s*:\s*0(\.0+)?\b/', $style)) {
			return true;
		}
	}

	$cls = strtolower((string) $img->getAttribute('class'));
	if ($cls && preg_match('/\b(hidden|sr-only|screen-reader-text|visually-hidden|d-none)\b/i', $cls)) {
		return true;
	}

	$aria_hidden = strtolower((string) $img->getAttribute('aria-hidden'));
	if ($aria_hidden === 'true') {
		return true;
	}

	return false;
}

// Safer decorative detection: strong UI/logos always excluded; weak UI terms excluded only when alt is empty
function myaio_sac_img_is_decorative(DOMElement $img, $src_abs) {
	$cls = strtolower((string) $img->getAttribute('class'));
	$id  = strtolower((string) $img->getAttribute('id'));
	$alt = trim((string) $img->getAttribute('alt'));
	$u   = strtolower((string) $src_abs);

	$strong_re = '/\b(site-logo|custom-logo|logo|favicon|sprite|spinner|loader)\b/i';
	if (($cls && preg_match($strong_re, $cls)) || ($id && preg_match($strong_re, $id))) {
		return true;
	}
	if (preg_match('/\b(logo|favicon|sprite|spinner|loader)\b/i', $u)) {
		return true;
	}

	if (preg_match('/\b(16x16|32x32|48x48|64x64|96x96|icon-\d+)\b/i', $u)) {
		return true;
	}

	$weak_re = '/\b(icon|icons|ui|social|share|arrow|chevron|menu|hamburger|search|btn|button|rating|star|cart|payment|visa|mastercard|amex|paypal)\b/i';
	if ($alt === '' && (
		($cls && preg_match($weak_re, $cls)) ||
		($id  && preg_match($weak_re, $id))  ||
		preg_match($weak_re, $u)
	)) {
		return true;
	}

	return false;
}

function myaio_sac_img_get_dimensions_px(DOMElement $img) {
	$w = 0;
	$h = 0;

	if ($img->hasAttribute('width')) {
		$w = (int) preg_replace('/[^\d]/', '', (string) $img->getAttribute('width'));
	}
	if ($img->hasAttribute('height')) {
		$h = (int) preg_replace('/[^\d]/', '', (string) $img->getAttribute('height'));
	}

	$style = strtolower((string) $img->getAttribute('style'));
	if ($style) {
		if (!$w && preg_match('/\bwidth\s*:\s*(\d+)px\b/', $style, $m)) {
			$w = (int) $m[1];
		}
		if (!$h && preg_match('/\bheight\s*:\s*(\d+)px\b/', $style, $m)) {
			$h = (int) $m[1];
		}
	}

	return ['width' => $w, 'height' => $h];
}

function myaio_sac_img_is_too_small($w, $h, $min_w = 100, $min_h = 100) {
	$w = (int) $w;
	$h = (int) $h;

	if ($w > 0 && $h > 0) {
		return ($w < $min_w || $h < $min_h);
	}
	if ($w > 0 && $w < $min_w) {
		return true;
	}
	if ($h > 0 && $h < $min_h) {
		return true;
	}

	// Unknown dimensions -> keep
	return false;
}

// Get image file size in bytes (best effort).
function myaio_sac_image_size_bytes($url) {
	$url = (string) $url;
	if ($url === '') {
		return null;
	}

	// Local attachment: filesystem size
	if (function_exists('attachment_url_to_postid')) {
		$att_id = attachment_url_to_postid($url);
		if ($att_id) {
			$file = get_attached_file($att_id);
			if ($file && file_exists($file)) {
				$size = @filesize($file);
				return ($size !== false) ? (int) $size : null;
			}
		}
	}

	// Fallback: HEAD request for Content-Length
	$resp = wp_safe_remote_head($url, [
		'timeout'     => 5,
		'redirection' => 2,
		'headers'     => [
			'User-Agent' => 'MYAIO-SEO-Audit/1.1 (+WordPress)',
			'Accept'     => '*/*',
		],
	]);

	if (is_wp_error($resp)) {
		return null;
	}

	$len = wp_remote_retrieve_header($resp, 'content-length');
	if ($len !== null && $len !== '') {
		return (int) $len;
	}

	return null;
}

function myaio_sac_image_size_kb($url) {
	$bytes = myaio_sac_image_size_bytes($url);
	if ($bytes === null) {
		return null;
	}
	return round($bytes / 1024, 2);
}

// Keep old helper name for compatibility – derives from filtered image list
function myaio_sac_dom_images_missing_alt($node_or_body) {
	$all     = myaio_sac_dom_images_all($node_or_body);
	$missing = [];

	foreach ($all as $img) {
		if (empty($img['has_alt'])) {
			$missing[] = $img['src_normalized'] ?: $img['src'];
		}
	}

	return array_values(array_unique(array_filter($missing)));
}

function myaio_sac_word_count($node_or_body) {
	if (!$node_or_body) {
		return 0;
	}
	$text  = trim((string) preg_replace('/\s+/', ' ', (string) $node_or_body->textContent));
	$text  = (string) preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text);
	$words = preg_split('/\s+/', $text, -1, PREG_SPLIT_NO_EMPTY);
	return is_array($words) ? count($words) : 0;
}

function myaio_sac_abs_urls($url) {
	$url = (string) $url;

	if (preg_match('#^https?://#i', $url)) {
		return $url;
	}
	if (strpos($url, '//') === 0) {
		return (is_ssl() ? 'https:' : 'http:') . $url;
	}

	$base = rtrim((string) get_site_url(), '/');
	if ($url && $url[0] !== '/') {
		$url = '/' . $url;
	}

	return $base . $url;
}

/* =========================
 * SITEMAP HELPERS
 * ========================= */
function myaio_sac_normalize_url_for_compare($url) {
	$url = trim((string) $url);
	if ($url === '') {
		return '';
	}

	$parts = wp_parse_url($url);
	if (!is_array($parts)) {
		return rtrim($url, '/');
	}

	$scheme = isset($parts['scheme']) ? strtolower((string) $parts['scheme']) : 'http';
	$host   = isset($parts['host']) ? strtolower((string) $parts['host']) : '';
	$port   = isset($parts['port']) ? ':' . $parts['port'] : '';
	$path   = isset($parts['path']) ? (string) $parts['path'] : '';
	$query  = isset($parts['query']) ? '?' . $parts['query'] : '';
	$frag   = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';

	if ($path !== '/') {
		$path = rtrim($path, '/');
	}

	return $scheme . '://' . $host . $port . $path . $query . $frag;
}

function myaio_sac_get_urls_from_check_sitemap_response() {
	$check = myaio_sac_check_sitemap_crawl();

	if (!is_array($check) || empty($check['sitemaps_found']) || !is_array($check['sitemaps_found'])) {
		return [];
	}

	$all_urls = [];

	foreach ($check['sitemaps_found'] as $sitemap_item) {
		if (!is_array($sitemap_item) || empty($sitemap_item['url'])) {
			continue;
		}

		$sitemap_url = esc_url_raw($sitemap_item['url']);
		if ($sitemap_url === '') {
			continue;
		}

		$xml = myaio_sac_fetch_xml_crawl($sitemap_url);
		if (!$xml) {
			continue;
		}

		$type = myaio_sac_parse_sitemap_type_crawl($xml);

		if ($type === 'urlset') {
			$urls = myaio_sac_extract_urlset_urls_crawl($xml);

			if (is_array($urls)) {
				foreach ($urls as $u) {
					$norm = myaio_sac_normalize_url_for_compare($u);
					if ($norm !== '') {
						$all_urls[$norm] = $norm;
					}
				}
			}
		}
	}

	return array_values($all_urls);
}

function myaio_sac_check_sitemap_crawl() {
	$cache_key = 'myaio_sac_crawler_check_sitemap_cache_v1';
	$cached = get_transient($cache_key);

	if (is_array($cached)) {
		return $cached;
	}

	$checked = [];
	$exists  = [];

	$fallbacks = [
		home_url('/sitemap.xml'),
		home_url('/sitemap_index.xml'),
		home_url('/wp-sitemap.xml'),
	];

	foreach ($fallbacks as $u) {
		$checked[] = $u;
		if (myaio_sac_url_exists_crawl($u)) {
			$exists[] = $u;
			break;
		}
	}

	if (empty($exists)) {
		$resp = [
			'available'           => false,
			'sitemap_files_count' => 0,
			'sitemaps_found'      => [],
			'checked'             => $checked,
		];

		set_transient($cache_key, $resp, 10 * MINUTE_IN_SECONDS);
		return $resp;
	}

	$visited    = [];
	$queue      = $exists;
	$sitemaps   = [];
	$safe_loops = 0;
	$max_nodes  = 60;

	while (!empty($queue) && $safe_loops < $max_nodes) {
		$next = array_shift($queue);
		$norm = myaio_sac_normalize_url_for_compare($next);

		if (isset($visited[$norm])) {
			$safe_loops++;
			continue;
		}

		$visited[$norm] = true;

		$xml = myaio_sac_fetch_xml_crawl($next);
		if (!$xml) {
			$sitemaps[] = [
				'url'       => $next,
				'type'      => 'fetch-failed',
				'url_count' => null,
			];
			$safe_loops++;
			continue;
		}

		$type = myaio_sac_parse_sitemap_type_crawl($xml);

		if ($type === 'index') {
			$children = myaio_sac_extract_index_children_crawl($xml);

			$sitemaps[] = [
				'url'       => $next,
				'type'      => 'index',
				'url_count' => null,
			];

			foreach ($children as $child) {
				$cn = myaio_sac_normalize_url_for_compare($child);
				if (!isset($visited[$cn])) {
					$queue[] = $child;
				}
			}
		} elseif ($type === 'urlset') {
			$urls = myaio_sac_extract_urlset_urls_crawl($xml);

			$sitemaps[] = [
				'url'       => $next,
				'type'      => 'urlset',
				'url_count' => count($urls),
			];
		} else {
			$sitemaps[] = [
				'url'       => $next,
				'type'      => 'unknown',
				'url_count' => null,
			];
		}

		$safe_loops++;
	}

	$resp = [
		'available'           => true,
		'sitemap_files_count' => count($sitemaps),
		'sitemaps_found'      => $sitemaps,
		'checked'             => $checked,
	];

	set_transient($cache_key, $resp, 10 * MINUTE_IN_SECONDS);

	return $resp;
}

function myaio_sac_url_exists_crawl($url) {
	$url = trim((string) $url);
	if ($url === '') {
		return false;
	}

	$res = wp_remote_head($url, [
		'timeout'     => 4,
		'redirection' => 2,
		'sslverify'   => true,
		'headers'     => [
			'User-Agent' => 'Mozilla/5.0 (Sitemap Checker)',
			'Accept'     => '*/*',
		],
	]);

	if (!is_wp_error($res)) {
		$code = (int) wp_remote_retrieve_response_code($res);
		if ($code >= 200 && $code < 400) {
			return true;
		}

		if (!in_array($code, [0, 400, 401, 403, 405], true)) {
			return false;
		}
	}

	$res = wp_remote_get($url, [
		'timeout'     => 4,
		'redirection' => 2,
		'sslverify'   => true,
		'headers'     => [
			'User-Agent' => 'Mozilla/5.0 (Sitemap Checker)',
			'Accept'     => 'application/xml,text/xml,text/plain;q=0.9,*/*;q=0.8',
			'Range'      => 'bytes=0-2048',
		],
	]);

	if (is_wp_error($res)) {
		return false;
	}

	$code = (int) wp_remote_retrieve_response_code($res);
	return ($code >= 200 && $code < 400);
}

function myaio_sac_fetch_xml_crawl($url) {
	$res = wp_remote_get($url, [
		'timeout'     => 8,
		'redirection' => 2,
		'sslverify'   => true,
		'headers'     => [
			'User-Agent' => 'Mozilla/5.0 (Sitemap XML Fetch)',
			'Accept'     => 'application/xml,text/xml,text/plain;q=0.9,*/*;q=0.8',
		],
	]);

	if (is_wp_error($res)) {
		return null;
	}

	$code = (int) wp_remote_retrieve_response_code($res);
	if ($code < 200 || $code >= 400) {
		return null;
	}

	$body = (string) wp_remote_retrieve_body($res);
	if ($body === '') {
		return null;
	}

	$is_gz_url = (bool) preg_match('/\.gz(\?|$)/i', (string) $url);
	$looks_gz  = (strlen($body) >= 2 && ord($body[0]) === 0x1f && ord($body[1]) === 0x8b);

	if (($is_gz_url || $looks_gz) && function_exists('gzdecode')) {
		$decoded = @gzdecode($body);
		if ($decoded !== false && $decoded !== '') {
			$body = $decoded;
		}
	}

	libxml_use_internal_errors(true);
	$xml = simplexml_load_string($body);
	libxml_clear_errors();

	return $xml ?: null;
}

function myaio_sac_parse_sitemap_type_crawl($xml) {
	if (!$xml) {
		return null;
	}

	$is_index  = $xml->xpath('/*[local-name()="sitemapindex"]');
	$is_urlset = $xml->xpath('/*[local-name()="urlset"]');

	if ($is_index && !empty($is_index)) {
		return 'index';
	}

	if ($is_urlset && !empty($is_urlset)) {
		return 'urlset';
	}

	return null;
}

function myaio_sac_extract_index_children_crawl($xml) {
	if (!$xml) {
		return [];
	}

	$nodes = $xml->xpath('/*[local-name()="sitemapindex"]/*[local-name()="sitemap"]/*[local-name()="loc"]');
	$out   = [];

	if (is_array($nodes)) {
		foreach ($nodes as $n) {
			$loc = trim((string) $n);
			if ($loc !== '') {
				$out[] = $loc;
			}
		}
	}

	return array_values(array_unique($out));
}

function myaio_sac_extract_urlset_urls_crawl($xml) {
	if (!$xml) {
		return [];
	}

	$nodes = $xml->xpath('/*[local-name()="urlset"]/*[local-name()="url"]/*[local-name()="loc"]');
	$out   = [];

	if (is_array($nodes)) {
		foreach ($nodes as $n) {
			$loc = trim((string) $n);
			if ($loc !== '') {
				$out[] = $loc;
			}
		}
	}

	return array_values(array_unique($out));
}