<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * REST API: /wp-json/myaio/v1/site-explorer
 *
 * Combined output:
 * - page_explorer_matrix
 * - backlink_explorer
 * - redirects
 * - check_sitemap
 * - check_robots_meta
 * - schema_scan
 * - asset_size_scan
 *
 * Optional query params:
 * - include_schema_json=1
 * - url=https://example.com/about-us
 * - path=/about-us
 *
 * Auth:
 * - Shared permission callback:
 *   myaio_sac_rest_permission_check
 */

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'myaio/v1',
			'/site-explorer',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => 'myaio_site_explorer_combined',
				'permission_callback' => 'myaio_sac_rest_permission_check',
				'args'                => array(
					'include_schema_json' => array(
						'type'              => 'boolean',
						'default'           => false,
						'sanitize_callback' => 'rest_sanitize_boolean',
					),
					'url' => array(
						'type'              => 'string',
						'required'          => false,
						'sanitize_callback' => 'sanitize_text_field',
					),
					'path' => array(
						'type'              => 'string',
						'required'          => false,
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);
	}
);

/**
 * Main combined callback.
 *
 * @param WP_REST_Request $request Request.
 * @return WP_REST_Response
 */
function myaio_site_explorer_combined( WP_REST_Request $request ) {
	$include_schema_json = (bool) $request->get_param( 'include_schema_json' );
	$requested_url       = myaio_site_explorer_normalize_requested_url( $request->get_param( 'url' ) );
	$target_url          = myaio_site_explorer_get_target_url_optional( $request );

	$response_data = array(
		'page_explorer_matrix' => myaio_site_explorer_get_page_explorer_matrix( $include_schema_json, $requested_url ),
		'backlink_explorer'    => myaio_site_explorer_get_backlink_explorer( $requested_url ),
		'redirects'            => myaio_site_explorer_get_redirects_scan( $requested_url ),
		'check_sitemap'        => myaio_site_explorer_check_sitemap( $requested_url ),
		'check_robots_meta'    => myaio_site_explorer_check_robots_meta( $target_url ),
		'schema_scan'          => myaio_site_explorer_schema_scan( $requested_url ),
		'asset_size_scan'      => myaio_site_explorer_asset_size_scan( $requested_url ),
	);

	return rest_ensure_response( $response_data );
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// SHARED URL HELPERS
////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Normalize optional requested URL for scan/filter logic.
 *
 * Supports:
 * - full URL
 * - root relative path like /about-us
 *
 * @param string|null $requested Requested URL.
 * @return string
 */
function myaio_site_explorer_normalize_requested_url( $requested ) {
	$requested = trim( (string) $requested );

	if ( '' === $requested ) {
		return '';
	}

	if ( 0 === strpos( $requested, '/' ) ) {
		$requested = home_url( $requested );
	}

	$requested = esc_url_raw( $requested );

	return myaio_site_explorer_normalize_url_for_compare( $requested );
}

/**
 * Optional target URL resolver.
 *
 * Behavior:
 * - If url/path given => same-host validated URL string
 * - If nothing given => empty string
 *
 * @param WP_REST_Request $request Request.
 * @return string|WP_Error
 */
function myaio_site_explorer_get_target_url_optional( WP_REST_Request $request ) {
	$url  = trim( (string) $request->get_param( 'url' ) );
	$path = trim( (string) $request->get_param( 'path' ) );

	if ( '' === $url && '' === $path ) {
		return '';
	}

	if ( '' !== $path && '' === $url ) {
		$path = ltrim( $path, '/' );
		$url  = home_url( '/' . $path );
	}

	$url = esc_url_raw( $url );

	if ( '' === $url || ! wp_http_validate_url( $url ) ) {
		return new WP_Error( 'invalid_url', 'Invalid URL provided.', array( 'status' => 400 ) );
	}

	$site_host = wp_parse_url( home_url( '/' ), PHP_URL_HOST );
	$url_host  = wp_parse_url( $url, PHP_URL_HOST );

	if ( ! $site_host || ! $url_host || 0 !== strcasecmp( (string) $site_host, (string) $url_host ) ) {
		return new WP_Error( 'forbidden_host', 'Only URLs on this site are allowed.', array( 'status' => 403 ) );
	}

	return rtrim( $url, '/' );
}

/**
 * Normalize URL for comparisons.
 *
 * @param string $url URL.
 * @return string
 */
function myaio_site_explorer_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;
}

/**
 * Convert bytes to KB.
 *
 * @param int|float|null $bytes Bytes.
 * @return float|null
 */
function myaio_site_explorer_bytes_to_kb( $bytes ) {
	if ( null === $bytes || '' === $bytes ) {
		return null;
	}

	$bytes = (float) $bytes;
	if ( $bytes < 0 ) {
		$bytes = 0;
	}

	return round( $bytes / 1024, 2 );
}

function myaio_site_explorer_iso8601( $ts ) {
	$ts = (int) $ts;
	return $ts > 0 ? gmdate( 'c', $ts ) : null;
}


/**
 * Build all sitemap page URLs from check_sitemap() response.
 *
 * Uses the sitemap files listed in check_sitemap()['sitemaps_found'].
 *
 * @param string $requested_url Optional requested URL.
 * @return array
 */
function myaio_site_explorer_get_urls_from_check_sitemap_response( $requested_url = '' ) {
	$check = myaio_site_explorer_check_sitemap( $requested_url );

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

	$all_urls = array();

	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_site_explorer_fetch_xml( $sitemap_url );
		if ( ! $xml ) {
			continue;
		}

		$type = myaio_site_explorer_parse_sitemap_type( $xml );

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

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

	return array_values( $all_urls );
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// SHARED CRAWLABLE URL HELPER
////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Build crawlable URL list, optionally restricted to one URL.
 *
 * Preserves type/category when available.
 *
 * @param string $requested_url Requested URL.
 * @return array
 */
function myaio_site_explorer_get_filtered_crawlable_urls( $requested_url = '' ) {
	if ( function_exists( 'myaio_sac_get_all_crawlable_urls' ) ) {
		$urls = myaio_sac_get_all_crawlable_urls();
	} elseif ( function_exists( 'myaio_sac_get_all_site_urls' ) ) {
		$urls = myaio_sac_get_all_site_urls();
	} else {
		return array();
	}

	if ( ! is_array( $urls ) ) {
		return array();
	}

	if ( '' === $requested_url ) {
		return $urls;
	}

	$matched        = null;
	$requested_norm = myaio_site_explorer_normalize_url_for_compare( $requested_url );

	foreach ( $urls as $item ) {
		if ( ! is_array( $item ) || empty( $item['url'] ) ) {
			continue;
		}

		$item_url = myaio_site_explorer_normalize_url_for_compare( (string) $item['url'] );
		if ( $item_url === $requested_norm ) {
			$matched = $item;
			break;
		}
	}

	if ( null !== $matched ) {
		return array( $matched );
	}

	return array(
		array(
			'url'      => $requested_url,
			'type'     => null,
			'category' => null,
		),
	);
}

/**
 * Load body element from HTML.
 *
 * @param string $html HTML.
 * @return DOMElement|null
 */
function myaio_site_explorer_get_body_dom_element( $html ) {
	if ( ! class_exists( 'DOMDocument' ) || ! class_exists( 'DOMXPath' ) ) {
		return null;
	}

	$html = (string) $html;
	if ( '' === trim( $html ) ) {
		return null;
	}

	libxml_use_internal_errors( true );

	$dom    = new DOMDocument();
	$loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html, LIBXML_NOWARNING | LIBXML_NOERROR );

	if ( ! $loaded ) {
		libxml_clear_errors();
		return null;
	}

	$bodies = $dom->getElementsByTagName( 'body' );
	$body   = ( $bodies->length > 0 ) ? $bodies->item( 0 ) : null;

	libxml_clear_errors();

	return ( $body instanceof DOMElement ) ? $body : null;
}

/**
 * True only if headings exist in valid grouped order:
 * all H1 first, then H2, then H3 ... up to H6.
 * Returns false if:
 * - no headings found
 * - heading level skips backward
 * - heading level jumps with a missing intermediate heading
 *
 * Examples:
 * H1,H1,H2,H2,H3 => true
 * H1,H3 => false
 * H2,H3 => false
 * H1,H2,H1 => false
 *
 * @param string $html HTML.
 * @return bool
 */
function myaio_site_explorer_check_headings_in_order( $html ) {
	$body = myaio_site_explorer_get_body_dom_element( $html );
	if ( ! $body ) {
		return false;
	}

	$heading_levels = array();

	foreach ( $body->getElementsByTagName( '*' ) as $node ) {
		if ( ! $node instanceof DOMElement ) {
			continue;
		}

		$tag = strtolower( $node->tagName );
		if ( preg_match( '/^h([1-6])$/', $tag, $matches ) ) {
			$heading_levels[] = (int) $matches[1];
		}
	}

	if ( empty( $heading_levels ) ) {
		return false;
	}

	$first = $heading_levels[0];
	if ( 1 !== $first ) {
		return false;
	}

	$seen_levels = array( 1 => true );
	$prev_level  = 1;

	for ( $i = 1, $count = count( $heading_levels ); $i < $count; $i++ ) {
		$current = (int) $heading_levels[ $i ];

		if ( $current < $prev_level ) {
			return false;
		}

		if ( $current > $prev_level ) {
			if ( $current !== $prev_level + 1 ) {
				return false;
			}
			$seen_levels[ $current ] = true;
		}

		$prev_level = $current;
	}

	return true;
}

/**
 * Check body text word count against minimum.
 *
 * Excludes non-content and boilerplate sections before counting words.
 *
 * @param string $html HTML.
 * @param int    $minimum Minimum words.
 * @return bool
 */
function myaio_site_explorer_check_body_word_count_minimum( $html, $minimum = 300 ) {
	$body = myaio_site_explorer_get_body_dom_element( $html );
	if ( ! $body ) {
		return false;
	}

	$xpath = new DOMXPath( $body->ownerDocument );

	// Remove non-content / boilerplate nodes before counting text.
	$nodes = $xpath->query(
		'.//script | .//style | .//noscript | .//template | .//svg | .//iframe | .//nav | .//header | .//footer | .//aside',
		$body
	);

	if ( $nodes instanceof DOMNodeList ) {
		$remove = array();

		foreach ( $nodes as $node ) {
			if ( $node instanceof DOMNode ) {
				$remove[] = $node;
			}
		}

		foreach ( $remove as $node ) {
			if ( $node->parentNode ) {
				$node->parentNode->removeChild( $node );
			}
		}
	}

	$text = trim( wp_strip_all_tags( $body->textContent ) );
	if ( '' === $text ) {
		return false;
	}

	$words = preg_split( '/[\pZ\pC]+/u', $text, -1, PREG_SPLIT_NO_EMPTY );
	$count = is_array( $words ) ? count( $words ) : 0;

	return $count >= (int) $minimum;
}

/**
 * Check whether at least one H2 exists in body.
 *
 * @param string $html HTML.
 * @return bool
 */
function myaio_site_explorer_check_h2_present( $html ) {
	$body = myaio_site_explorer_get_body_dom_element( $html );
	if ( ! $body ) {
		return false;
	}

	$h2_tags = $body->getElementsByTagName( 'h2' );

	return ( $h2_tags instanceof DOMNodeList && $h2_tags->length > 0 );
}

/**
 * Check if total body DOM elements are under given limit.
 *
 * Counts all element nodes inside <body>.
 *
 * @param string $html HTML.
 * @param int    $limit Limit.
 * @return bool
 */
function myaio_site_explorer_check_body_dom_size_under_limit( $html, $limit = 1000 ) {
	$body = myaio_site_explorer_get_body_dom_element( $html );
	if ( ! $body ) {
		return false;
	}

	$count = 0;

	foreach ( $body->getElementsByTagName( '*' ) as $node ) {
		if ( $node instanceof DOMElement ) {
			$count++;
		}
	}

	return $count < (int) $limit;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// PAGE EXPLORER MATRIX
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_get_page_explorer_matrix( $include_schema_json = false, $requested_url = '' ) {
	$results = array();

	if ( '' !== $requested_url ) {
		$page_urls = array( $requested_url );
	} else {
		$page_ids = get_posts(
			array(
				'post_type'              => 'page',
				'post_status'            => 'publish',
				'posts_per_page'         => -1,
				'fields'                 => 'ids',
				'orderby'                => 'ID',
				'order'                  => 'ASC',
				'no_found_rows'          => true,
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
				'suppress_filters'       => false,
			)
		);

		$page_ids  = is_array( $page_ids ) ? $page_ids : array();
		$page_urls = array();

		foreach ( $page_ids as $post_id ) {
			$url = get_permalink( $post_id );
			if ( $url ) {
				$page_urls[] = esc_url_raw( $url );
			}
		}
	}

	$sitemap_urls      = myaio_site_explorer_get_urls_from_check_sitemap_response( $requested_url );
	$sitemap_available = ! empty( $sitemap_urls );
	$robots_rules      = myaio_site_explorer_parse_robots_txt();

	foreach ( $page_urls as $url ) {
		if ( ! $url ) {
			continue;
		}

		$url = esc_url_raw( $url );

		$resp = wp_remote_get(
			$url,
			array(
				'timeout'     => 8,
				'redirection' => 3,
				'headers'     => array(
					'User-Agent' => 'MYAIO-SEO-Audit/' . ( defined( 'myaio_sac_VERSION' ) ? myaio_sac_VERSION : '1.0' ) . '; ' . home_url(),
					'Accept'     => 'text/html,application/xhtml+xml',
				),
			)
		);

		if ( is_wp_error( $resp ) ) {
			continue;
		}

		$code = (int) wp_remote_retrieve_response_code( $resp );
		if ( $code < 200 || $code >= 300 ) {
			continue;
		}

		$body = (string) wp_remote_retrieve_body( $resp );
		if ( '' === $body ) {
			continue;
		}

		$schema_analysis = myaio_site_explorer_extract_schema_json( $body );
		$normalized_url  = myaio_site_explorer_normalize_url_for_compare( $url );

		$results[] = array(
			'page_url' => $url,

			'no_noindex_meta_directive'       => myaio_site_explorer_check_noindex( $body ),
			'no_nofollow_meta_directive'      => myaio_site_explorer_check_nofollow( $body ),
			'allowed_by_robots_txt'           => myaio_site_explorer_check_robots_permission( $url, $robots_rules ),
			'all_robots_directives_are_valid' => myaio_site_explorer_check_valid_robots_meta( $body ),

			'canonical_link_exists'               => (bool) myaio_site_explorer_check_canonical_exists( $body ),
			'only_one_canonical_on_page'          => (bool) myaio_site_explorer_check_single_canonical( $body ),
			'canonical_url_matches_main_page_url' => (bool) myaio_site_explorer_check_canonical_match( $body, $url ),

			'no_inline_css'           => myaio_site_explorer_check_no_inline_css( $body ),
			'images_standard_formats' => myaio_site_explorer_check_image_formats( $body ),
			'all_images_has_alt'      => myaio_site_explorer_check_all_images_have_alt( $body ),
			'all_images_under_100KB'  => myaio_site_explorer_check_all_images_under_100kb( $body ),
			'no_deprecated_tags'      => myaio_site_explorer_check_deprecated_tags( $body ),
			'all_links_proper_href'   => myaio_site_explorer_check_links_have_href( $body ),

			'headings_in_order_h1_to_h6' => myaio_site_explorer_check_headings_in_order( $body ),
			'h2_heading_present'         => myaio_site_explorer_check_h2_present( $body ),
			'content_has_300_words'      => myaio_site_explorer_check_body_word_count_minimum( $body, 300 ),
			'body_dom_size_under_1000'   => myaio_site_explorer_check_body_dom_size_under_limit( $body, 1000 ),

			'url_present_in_sitemap' => in_array( $normalized_url, $sitemap_urls, true ),
			'check_sitemap_present'  => $sitemap_available,

			'schema_exists' => (bool) $schema_analysis['exists'],
			'schema_json'   => ! empty( $schema_analysis['exists'] ) ? $schema_analysis['json'] : false,
		);
	}

	return $results;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// BACKLINK EXPLORER
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_get_backlink_explorer( $requested_url = '' ) {
	if ( ! function_exists( 'myaio_sac_get_all_crawlable_urls' ) && ! function_exists( 'myaio_sac_get_all_site_urls' ) ) {
		return array(
			'error' => 'Required crawlable URL provider not found.',
		);
	}

	$all_pages = myaio_site_explorer_get_filtered_crawlable_urls( $requested_url );
	if ( ! is_array( $all_pages ) ) {
		return array(
			'error' => 'Crawlable URL provider did not return an array.',
		);
	}

	$incoming_map = array();
	$results      = array();
	$site_home    = rtrim( home_url(), '/' );

	foreach ( $all_pages as $page ) {
		if ( ! is_array( $page ) || empty( $page['url'] ) ) {
			continue;
		}

		$url = rtrim( (string) $page['url'], '/' );

		$response = wp_remote_get(
			$url,
			array(
				'timeout'     => 15,
				'redirection' => 5,
				'user-agent'  => 'myaio-backlink-explorer/1.1; ' . $site_home,
			)
		);

		if ( is_wp_error( $response ) ) {
			continue;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( $code < 200 || $code >= 400 ) {
			continue;
		}

		$html = wp_remote_retrieve_body( $response );
		if ( empty( $html ) ) {
			continue;
		}

		$outgoing = myaio_site_explorer_extract_links_with_context( $html, $site_home );

		foreach ( $outgoing['all_links'] as $lnk ) {
			$target = rtrim( $lnk['href'], '/' );

			if ( ! isset( $incoming_map[ $target ] ) ) {
				$incoming_map[ $target ] = array();
			}

			$incoming_map[ $target ][] = array(
				'from_url'  => $url,
				'anchor'    => $lnk['anchor'],
				'nofollow'  => $lnk['nofollow'],
				'snippet'   => $lnk['html_snippet'],
				'placement' => $lnk['placement'],
			);
		}

		$results[ $url ] = array(
			'url'      => $url,
			'type'     => $page['type'] ?? null,
			'category' => $page['category'] ?? null,

			'outgoing_internal_links' => $outgoing['internal_links'],
			'outgoing_external_links' => $outgoing['external_links'],

			'outgoing_summary' => array(
				'internal' => count( $outgoing['internal_links'] ),
				'external' => count( $outgoing['external_links'] ),
				'nofollow' => $outgoing['nofollow_count'],
				'dofollow' => $outgoing['dofollow_count'],
			),

			'incoming_internal_links' => array(),
			'incoming_external_links' => array(),
		);
	}

	foreach ( $results as $page_url => &$data ) {
		$normalized = rtrim( (string) $page_url, '/' );
		$incoming   = $incoming_map[ $normalized ] ?? array();

		$data['incoming_internal_links'] = array_values(
			array_filter(
				$incoming,
				function ( $i ) use ( $site_home ) {
					return strpos( rtrim( (string) $i['from_url'], '/' ), $site_home ) === 0;
				}
			)
		);

		$data['incoming_external_links'] = array_values(
			array_filter(
				$incoming,
				function ( $i ) use ( $site_home ) {
					return strpos( rtrim( (string) $i['from_url'], '/' ), $site_home ) !== 0;
				}
			)
		);
	}
	unset( $data );

	return array(
		'total_pages' => count( $results ),
		'pages'       => array_values( $results ),
	);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// REDIRECTS
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_get_redirects_scan( $requested_url = '' ) {
	$http_timeout = 30;
	$max_chain    = 8;

	$urls = myaio_site_explorer_get_filtered_crawlable_urls( $requested_url );

	if ( empty( $urls ) ) {
		return array(
			'error' => 'No crawlable URL provider found (myaio_sac_get_all_crawlable_urls / myaio_sac_get_all_site_urls)',
		);
	}

	$results         = array();
	$total_redirects = 0;

	foreach ( $urls as $url_data ) {
		$page_url = '';
		if ( is_array( $url_data ) && isset( $url_data['url'] ) ) {
			$page_url = (string) $url_data['url'];
		}

		$page_url = esc_url_raw( $page_url );
		if ( '' === $page_url ) {
			continue;
		}

		$type     = ( is_array( $url_data ) && isset( $url_data['type'] ) ) ? sanitize_text_field( (string) $url_data['type'] ) : '';
		$category = ( is_array( $url_data ) && isset( $url_data['category'] ) ) ? sanitize_text_field( (string) $url_data['category'] ) : '';

		$first = wp_remote_get(
			$page_url,
			array(
				'timeout'     => $http_timeout,
				'redirection' => 0,
				'headers'     => array(
					'User-Agent' => 'WP Redirect Scanner/1.0',
				),
			)
		);

		if ( is_wp_error( $first ) ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => $first->get_error_message(),
			);
			continue;
		}

		$status  = (int) wp_remote_retrieve_response_code( $first );
		$headers = wp_remote_retrieve_headers( $first );

		$location = '';
		if ( is_array( $headers ) && isset( $headers['location'] ) ) {
			$location = $headers['location'];
		} elseif ( is_object( $headers ) && isset( $headers->location ) ) {
			$location = $headers->location;
		}

		if ( is_array( $location ) ) {
			$location = reset( $location );
		}
		$location = is_string( $location ) ? trim( $location ) : '';

		$is_redirect = ( $status >= 300 && $status < 400 && '' !== $location );

		$item = array(
			'url'         => $page_url,
			'type'        => $type,
			'category'    => $category,
			'status'      => $status,
			'is_redirect' => $is_redirect,
			'location'    => $is_redirect ? $location : '',
		);

		if ( $is_redirect ) {
			$total_redirects++;

			$chain   = array();
			$current = $page_url;

			for ( $i = 0; $i < $max_chain; $i++ ) {
				$hop = wp_remote_get(
					$current,
					array(
						'timeout'     => $http_timeout,
						'redirection' => 0,
						'headers'     => array(
							'User-Agent' => 'WP Redirect Scanner/1.0',
						),
					)
				);

				if ( is_wp_error( $hop ) ) {
					$chain[] = array(
						'from'   => $current,
						'status' => 'error',
						'to'     => '',
						'error'  => $hop->get_error_message(),
					);
					break;
				}

				$hop_status  = (int) wp_remote_retrieve_response_code( $hop );
				$hop_headers = wp_remote_retrieve_headers( $hop );

				$hop_location = '';
				if ( is_array( $hop_headers ) && isset( $hop_headers['location'] ) ) {
					$hop_location = $hop_headers['location'];
				} elseif ( is_object( $hop_headers ) && isset( $hop_headers->location ) ) {
					$hop_location = $hop_headers->location;
				}

				if ( is_array( $hop_location ) ) {
					$hop_location = reset( $hop_location );
				}
				$hop_location = is_string( $hop_location ) ? trim( $hop_location ) : '';

				$hop_is_redirect = ( $hop_status >= 300 && $hop_status < 400 && '' !== $hop_location );

				$next = $hop_is_redirect ? myaio_site_explorer_resolve_url( $current, $hop_location ) : '';

				$chain[] = array(
					'from'   => $current,
					'status' => $hop_status,
					'to'     => $next,
				);

				if ( ! $hop_is_redirect ) {
					break;
				}

				if ( $next === $current || '' === $next ) {
					break;
				}

				$current = $next;
			}

			$item['redirect_chain'] = $chain;
			$item['final_url']      = $current;
		}

		$results[] = $item;
	}

	$redirect_urls = array();
	foreach ( $results as $r ) {
		if ( is_array( $r ) && ! empty( $r['is_redirect'] ) ) {
			$redirect_urls[] = $r;
		}
	}

	return array(
		'summary' => array(
			'total_pages_scanned' => count( $urls ),
			'total_urls_returned' => count( $results ),
			'total_redirecting'   => $total_redirects,
		),
		'all_urls'      => $results,
		'redirect_urls' => $redirect_urls,
	);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// CHECK SITEMAP
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_get_sitemaps_from_robots( $robots_url ) {
	$out = array(
		'ok'       => false,
		'status'   => 0,
		'sitemaps' => array(),
		'error'    => null,
	);

	$res = wp_remote_get(
		$robots_url,
		array(
			'timeout'     => 5,
			'redirection' => 2,
			'sslverify'   => true,
			'headers'     => array(
				'User-Agent' => 'Mozilla/5.0 (WordPress REST Sitemap Checker)',
				'Accept'     => 'text/plain',
			),
		)
	);

	if ( is_wp_error( $res ) ) {
		$out['error'] = $res->get_error_message();
		return $out;
	}

	$out['ok']     = true;
	$out['status'] = (int) wp_remote_retrieve_response_code( $res );

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

	if ( preg_match_all( '/^\s*sitemap\s*:\s*(\S+)\s*$/im', $body, $m ) ) {
		foreach ( (array) ( $m[1] ?? array() ) as $u ) {
			$u = trim( $u );
			if ( '' === $u ) {
				continue;
			}

			if ( 0 !== strpos( $u, 'http://' ) && 0 !== strpos( $u, 'https://' ) ) {
				$u = home_url( '/' . ltrim( $u, '/' ) );
			}

			$out['sitemaps'][] = esc_url_raw( $u );
		}
	}

	$out['sitemaps'] = array_values( array_unique( array_filter( $out['sitemaps'] ) ) );

	return $out;
}

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

	$res = wp_remote_head(
		$url,
		array(
			'timeout'     => 4,
			'redirection' => 2,
			'sslverify'   => true,
			'headers'     => array(
				'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, array( 0, 400, 401, 403, 405 ), true ) ) {
			return false;
		}
	}

	$res = wp_remote_get(
		$url,
		array(
			'timeout'     => 4,
			'redirection' => 2,
			'sslverify'   => true,
			'headers'     => array(
				'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_site_explorer_fetch_xml( $url ) {
	$res = wp_remote_get(
		$url,
		array(
			'timeout'     => 8,
			'redirection' => 2,
			'sslverify'   => true,
			'headers'     => array(
				'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 ( false !== $decoded && '' !== $decoded ) {
			$body = $decoded;
		}
	}

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

	return $xml ?: null;
}

function myaio_site_explorer_parse_sitemap_type( $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_site_explorer_extract_index_children( $xml ) {
	if ( ! $xml ) {
		return array();
	}

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

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

	return array_values( array_unique( $out ) );
}

function myaio_site_explorer_extract_urlset_urls( $xml ) {
	if ( ! $xml ) {
		return array();
	}

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

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

	return array_values( array_unique( $out ) );
}

function myaio_site_explorer_resolve_sitemaps_dir() {
	global $wp_filesystem;

	if ( ! function_exists( 'WP_Filesystem' ) ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
	}
	WP_Filesystem();

	$root_dir = trailingslashit( ABSPATH ) . 'sitemaps/';
	$root_url = home_url( '/sitemaps/' );

	if ( ! $wp_filesystem->is_dir( $root_dir ) ) {
		$wp_filesystem->mkdir( $root_dir );
	}

	if ( $wp_filesystem->is_writable( $root_dir ) ) {
		return array(
			'dir' => $root_dir,
			'url' => trailingslashit( $root_url ),
		);
	}

	$uploads = wp_get_upload_dir();
	$u_dir   = trailingslashit( $uploads['basedir'] ) . 'sitemaps/';
	$u_url   = trailingslashit( $uploads['baseurl'] ) . 'sitemaps/';

	if ( ! $wp_filesystem->is_dir( $u_dir ) ) {
		$wp_filesystem->mkdir( $u_dir );
	}

	return array(
		'dir' => $u_dir,
		'url' => $u_url,
	);
}

function myaio_site_explorer_locate_sitemap_file() {
	$where   = myaio_site_explorer_resolve_sitemaps_dir();
	$root    = $where['dir'] . 'sitemap.xml';
	$uploads = wp_get_upload_dir();
	$u_path  = trailingslashit( $uploads['basedir'] ) . 'sitemap.xml';

	$path = null;

	if ( is_readable( $root ) ) {
		$path = $root;
	} elseif ( is_readable( $u_path ) ) {
		$path = $u_path;
	}

	return array(
		'path'  => $path,
		'mtime' => ( $path && file_exists( $path ) ) ? filemtime( $path ) : 0,
		'url'   => home_url( '/sitemap.xml' ),
	);
}

function myaio_site_explorer_check_sitemap( $requested_url = '' ) {
	$cache_key = 'myaio_sac_check_sitemap_cache_v1';
	$cached    = get_transient( $cache_key );

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

	$canonical_url = home_url( '/sitemap.xml' );
	$saved         = get_option( 'myaio_sac_last_sitemap_url' );

	$checked          = array();
	$exists           = array();
	$guessed_children = array();

	$robots_url  = home_url( '/robots.txt' );
	$robots_info = myaio_site_explorer_get_sitemaps_from_robots( $robots_url );

	if ( ! empty( $robots_info['sitemaps'] ) ) {
		foreach ( $robots_info['sitemaps'] as $u ) {
			$checked[] = $u;
			if ( myaio_site_explorer_url_exists( $u ) ) {
				$exists[] = $u;
			}
		}
	}

	if ( empty( $exists ) ) {
		$fallbacks = array(
			home_url( '/sitemap.xml' ),
			home_url( '/sitemap_index.xml' ),
			home_url( '/wp-sitemap.xml' ),
		);

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

	if ( empty( $exists ) ) {
		$checked[] = $canonical_url;
		if ( myaio_site_explorer_url_exists( $canonical_url ) ) {
			$exists[] = $canonical_url;
		}
	}

	$max_candidate_checks = count( $checked );

	if ( empty( $exists ) ) {
		$resp = array(
			'available'               => false,
			'message'                 => 'No sitemap found at known locations (including robots.txt).',
			'sitemap_url'             => $canonical_url,
			'previous_sitemap_url'    => get_option( 'myaio_sac_prev_sitemap_url' ) ?: ( $saved ?: null ),
			'last_modified'           => null,
			'previous_last_modified'  => myaio_site_explorer_iso8601( (int) ( get_option( 'myaio_sac_prev_sitemap_mtime' ) ?: get_option( 'myaio_sac_last_sitemap_mtime' ) ) ),
			'checked'                 => $checked,
			'sitemap_files_count'     => 0,
			'sitemaps_found'          => array(),
			'total_url_count'         => 0,
			'unique_url_count'        => 0,
			'folder_children_count'   => count( $guessed_children ),
			'folder_children_samples' => array_slice( $guessed_children, 0, 20 ),
			'robots'                  => array(
				'url'      => $robots_url,
				'ok'       => (bool) ( $robots_info['ok'] ?? false ),
				'status'   => (int) ( $robots_info['status'] ?? 0 ),
				'sitemaps' => array_values( array_unique( $robots_info['sitemaps'] ?? array() ) ),
				'error'    => $robots_info['error'] ?? null,
			),
		);

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

	$visited    = array();
	$queue      = $exists;
	$sitemaps   = array();
	$all_urls   = array();
	$safe_loops = 0;

	$max_nodes = 60;
	$max_urls  = 20000;
	$partial   = false;

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

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

		$visited[ $norm ] = true;

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

		$type = myaio_site_explorer_parse_sitemap_type( $xml );

		if ( 'index' === $type ) {
			$children   = myaio_site_explorer_extract_index_children( $xml );
			$sitemaps[] = array(
				'url'       => $next,
				'type'      => 'index',
				'url_count' => null,
			);

			foreach ( $children as $child ) {
				$cn = myaio_site_explorer_normalize_url_for_compare( $child );
				if ( ! isset( $visited[ $cn ] ) ) {
					$queue[] = $child;
				}
			}
		} elseif ( 'urlset' === $type ) {
			$urls       = myaio_site_explorer_extract_urlset_urls( $xml );
			$sitemaps[] = array(
				'url'       => $next,
				'type'      => 'urlset',
				'url_count' => count( $urls ),
			);

			foreach ( $urls as $u ) {
				$all_urls[ myaio_site_explorer_normalize_url_for_compare( $u ) ] = true;
				if ( count( $all_urls ) >= $max_urls ) {
					$partial = true;
					break;
				}
			}

			if ( $partial ) {
				break;
			}
		} else {
			$sitemaps[] = array(
				'url'       => $next,
				'type'      => 'unknown',
				'url_count' => null,
			);
		}

		$safe_loops++;
	}

	if ( $safe_loops >= $max_nodes ) {
		$partial = true;
	}

	$sum_counts   = 0;
	$file_count   = count( $sitemaps );
	$unique_total = count( $all_urls );

	foreach ( $sitemaps as $s ) {
		if ( 'urlset' === $s['type'] && isset( $s['url_count'] ) ) {
			$sum_counts += (int) $s['url_count'];
		}
	}

	$loc        = myaio_site_explorer_locate_sitemap_file();
	$curr_mtime = (int) ( get_option( 'myaio_sac_last_sitemap_mtime' ) ?: 0 );
	if ( ! empty( $loc['mtime'] ) && (int) $loc['mtime'] > 0 ) {
		$curr_mtime = (int) $loc['mtime'];
	}

	$prev_url   = get_option( 'myaio_sac_prev_sitemap_url' ) ?: null;
	$prev_mtime = (int) ( get_option( 'myaio_sac_prev_sitemap_mtime' ) ?: 0 );

	$resp = array(
		'available'               => true,
		'partial'                 => $partial,
		'message'                 => $partial
			? sprintf( 'Found %d sitemap file(s) (partial crawl due to safety limits); total URLs (non-deduped across files): %d; unique URLs: %d.', $file_count, $sum_counts, $unique_total )
			: sprintf( 'Found %d sitemap file(s); total URLs (non-deduped across files): %d; unique URLs: %d.', $file_count, $sum_counts, $unique_total ),
		'sitemap_url'             => $canonical_url,
		'previous_sitemap_url'    => $prev_url ?: ( $saved ?: null ),
		'last_modified'           => myaio_site_explorer_iso8601( $curr_mtime ),
		'previous_last_modified'  => myaio_site_explorer_iso8601( $prev_mtime ?: (int) get_option( 'myaio_sac_last_sitemap_mtime' ) ),
		'sitemap_files_count'     => $file_count,
		'sitemaps_found'          => $sitemaps,
		'total_url_count'         => $sum_counts,
		'unique_url_count'        => $unique_total,
		'folder_children_count'   => count( $guessed_children ),
		'folder_children_samples' => array_slice( $guessed_children, 0, 20 ),
		'checked'                 => $checked,
		'robots'                  => array(
			'url'      => $robots_url,
			'ok'       => (bool) ( $robots_info['ok'] ?? false ),
			'status'   => (int) ( $robots_info['status'] ?? 0 ),
			'sitemaps' => array_values( array_unique( $robots_info['sitemaps'] ?? array() ) ),
			'error'    => $robots_info['error'] ?? null,
		),
		'limits'                  => array(
			'max_candidate_checks' => $max_candidate_checks,
			'max_nodes'            => $max_nodes,
			'max_urls'             => $max_urls,
		),
	);

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

	return $resp;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// CHECK ROBOTS META
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_check_robots_meta( $target_url = '' ) {
	if ( is_wp_error( $target_url ) ) {
		return array(
			'error'   => 'URL unreachable',
			'details' => $target_url->get_error_message(),
		);
	}

	if ( '' === $target_url ) {
		$target_url = home_url( '/' );
	}

	$response = wp_remote_get(
		$target_url,
		array(
			'timeout'     => 15,
			'redirection' => 5,
			'user-agent'  => 'myaio-robots-check/1.1.0',
			'headers'     => array(
				'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
			),
		)
	);

	if ( is_wp_error( $response ) ) {
		return array(
			'error'   => 'URL unreachable',
			'details' => $response->get_error_message(),
		);
	}

	$status       = (int) wp_remote_retrieve_response_code( $response );
	$html         = (string) wp_remote_retrieve_body( $response );
	$content_type = (string) wp_remote_retrieve_header( $response, 'content-type' );
	$x_robots_tag = (string) wp_remote_retrieve_header( $response, 'x-robots-tag' );

	if ( $status < 200 || $status >= 400 ) {
		return array(
			'url'            => $target_url,
			'meta_available' => false,
			'message'        => 'Non-success HTTP status returned.',
			'http_status'    => $status,
			'x_robots_tag'   => '' !== $x_robots_tag ? $x_robots_tag : null,
		);
	}

	if ( '' !== $content_type && false === stripos( $content_type, 'text/html' ) ) {
		return array(
			'url'            => $target_url,
			'meta_available' => false,
			'message'        => 'Response is not HTML, cannot reliably scan <meta> tags.',
			'http_status'    => $status,
			'content_type'   => $content_type,
			'x_robots_tag'   => '' !== $x_robots_tag ? $x_robots_tag : null,
		);
	}

	libxml_use_internal_errors( true );
	$dom = new DOMDocument();

	// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
	@$dom->loadHTML( $html );

	$metas    = $dom->getElementsByTagName( 'meta' );
	$found    = false;
	$contents = array();

	foreach ( $metas as $meta ) {
		if ( 0 === strcasecmp( $meta->getAttribute( 'name' ), 'robots' ) ) {
			$found = true;

			$meta_content = trim( (string) $meta->getAttribute( 'content' ) );
			if ( '' !== $meta_content ) {
				$contents[] = $meta_content;
			}
		}
	}

	if ( $found && ! empty( $contents ) ) {
		$directives = array();

		foreach ( $contents as $content_string ) {
			foreach ( explode( ',', (string) $content_string ) as $part ) {
				$d = trim( (string) $part );
				if ( '' !== $d ) {
					$directives[] = $d;
				}
			}
		}

		$directives     = array_values( array_unique( $directives ) );
		$merged_content = implode( ', ', $directives );

		return array(
			'url'            => $target_url,
			'meta_available' => true,
			'content'        => $merged_content,
			'http_status'    => $status,
			'x_robots_tag'   => '' !== $x_robots_tag ? $x_robots_tag : null,
		);
	}

	return array(
		'url'            => $target_url,
		'meta_available' => false,
		'message'        => 'No robots meta tag found in this page <head>.',
		'http_status'    => $status,
		'x_robots_tag'   => '' !== $x_robots_tag ? $x_robots_tag : null,
	);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// SCHEMA SCAN
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_schema_scan( $requested_url = '' ) {
	$http_timeout = 30;
	$all_urls     = myaio_site_explorer_get_filtered_crawlable_urls( $requested_url );

	if ( ! is_array( $all_urls ) ) {
		$all_urls = array();
	}

	$results           = array();
	$total_with_schema = 0;

	if ( ! class_exists( 'DOMDocument' ) || ! class_exists( 'DOMXPath' ) ) {
		return array(
			'error' => 'DOM extension not available',
		);
	}

	foreach ( $all_urls as $url_data ) {
		$page_url = '';
		if ( is_array( $url_data ) && isset( $url_data['url'] ) ) {
			$page_url = (string) $url_data['url'];
		}

		$page_url = esc_url_raw( trim( $page_url ) );
		if ( '' === $page_url ) {
			continue;
		}

		$type     = ( is_array( $url_data ) && isset( $url_data['type'] ) ) ? sanitize_text_field( (string) $url_data['type'] ) : '';
		$category = ( is_array( $url_data ) && isset( $url_data['category'] ) ) ? sanitize_text_field( (string) $url_data['category'] ) : '';

		$response = wp_remote_get(
			$page_url,
			array(
				'timeout'     => $http_timeout,
				'redirection' => 5,
				'headers'     => array(
					'User-Agent' => 'MYAIO-Schema-Scanner/1.0; ' . home_url(),
					'Accept'     => 'text/html,application/xhtml+xml',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => $response->get_error_message(),
			);
			continue;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( $code < 200 || $code >= 300 ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => 'HTTP ' . $code,
			);
			continue;
		}

		$html = (string) wp_remote_retrieve_body( $response );
		if ( '' === $html ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => 'Empty HTML',
			);
			continue;
		}

		libxml_use_internal_errors( true );

		$dom    = new DOMDocument();
		$loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html, LIBXML_NOWARNING | LIBXML_NOERROR );

		if ( ! $loaded ) {
			libxml_clear_errors();
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => 'Failed to parse HTML',
			);
			continue;
		}

		$jsonld_blocks = array();
		$scripts       = $dom->getElementsByTagName( 'script' );

		foreach ( $scripts as $script ) {
			if ( ! $script instanceof DOMElement ) {
				continue;
			}

			$type_attr = trim( (string) $script->getAttribute( 'type' ) );
			if ( 'application/ld+json' === strtolower( $type_attr ) ) {
				$content = trim( (string) $script->textContent );
				if ( '' !== $content ) {
					$jsonld_blocks[] = $content;
				}
			}
		}

		$xpath = new DOMXPath( $dom );

		$microdata_nodes = $xpath->query( '//*[@itemscope or @itemtype]' );
		$has_microdata   = ( $microdata_nodes && $microdata_nodes->length > 0 );

		$rdfa_nodes = $xpath->query( '//*[@typeof or @vocab]' );
		$has_rdfa   = ( $rdfa_nodes && $rdfa_nodes->length > 0 );

		libxml_clear_errors();

		$has_schema = ( ! empty( $jsonld_blocks ) || $has_microdata || $has_rdfa );

		if ( $has_schema ) {
			$total_with_schema++;
		}

		$results[] = array(
			'url'           => $page_url,
			'type'          => $type,
			'category'      => $category,
			'has_schema'    => $has_schema,
			'has_jsonld'    => ! empty( $jsonld_blocks ),
			'has_microdata' => $has_microdata,
			'has_rdfa'      => $has_rdfa,
			'jsonld'        => $jsonld_blocks,
		);
	}

	$total_available = count( $all_urls );

	return array(
		'summary' => array(
			'total_urls_available' => $total_available,
			'limit'                => $total_available,
			'offset'               => 0,
			'scanned_this_call'    => $total_available,
			'pages_with_schema'    => $total_with_schema,
		),
		'pages'   => $results,
		'next'    => array(
			'offset' => null,
			'limit'  => $total_available,
		),
	);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// ASSET SIZE SCAN
////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Scan HTML, CSS, and JS sizes for one URL or all crawlable URLs.
 *
 * Returns both bytes and KB values.
 *
 * @param string $requested_url Requested URL.
 * @return array
 */
function myaio_site_explorer_asset_size_scan( $requested_url = '' ) {
	$all_urls = myaio_site_explorer_get_filtered_crawlable_urls( $requested_url );

	if ( ! is_array( $all_urls ) || empty( $all_urls ) ) {
		return array(
			'summary' => array(
				'total_urls_scanned' => 0,
				'total_html_bytes'   => 0,
				'total_css_bytes'    => 0,
				'total_js_bytes'     => 0,
				'total_html_kb'      => 0,
				'total_css_kb'       => 0,
				'total_js_kb'        => 0,
			),
			'pages'   => array(),
		);
	}

	$results          = array();
	$total_html_bytes = 0;
	$total_css_bytes  = 0;
	$total_js_bytes   = 0;

	$asset_size_cache = array();

	foreach ( $all_urls as $url_data ) {
		$page_url = '';
		if ( is_array( $url_data ) && isset( $url_data['url'] ) ) {
			$page_url = trim( (string) $url_data['url'] );
		}

		if ( '' === $page_url ) {
			continue;
		}

		$page_url = esc_url_raw( $page_url );
		$type     = ( is_array( $url_data ) && isset( $url_data['type'] ) ) ? sanitize_text_field( (string) $url_data['type'] ) : '';
		$category = ( is_array( $url_data ) && isset( $url_data['category'] ) ) ? sanitize_text_field( (string) $url_data['category'] ) : '';

		$response = wp_remote_get(
			$page_url,
			array(
				'timeout'     => 20,
				'redirection' => 5,
				'headers'     => array(
					'User-Agent' => 'MYAIO-Asset-Size-Scanner/1.0; ' . home_url(),
					'Accept'     => 'text/html,application/xhtml+xml',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => $response->get_error_message(),
			);
			continue;
		}

		$status = (int) wp_remote_retrieve_response_code( $response );
		if ( $status < 200 || $status >= 300 ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => 'HTTP ' . $status,
			);
			continue;
		}

		$html = (string) wp_remote_retrieve_body( $response );
		if ( '' === $html ) {
			$results[] = array(
				'url'      => $page_url,
				'type'     => $type,
				'category' => $category,
				'error'    => 'Empty HTML',
			);
			continue;
		}

		$html_size_bytes = strlen( $html );

		$assets = myaio_site_explorer_extract_css_js_assets( $html, $page_url );

		$css_files            = array();
		$js_files             = array();
		$page_css_total_bytes = 0;
		$page_js_total_bytes  = 0;

		foreach ( $assets['css'] as $css_url ) {
			$css_url = esc_url_raw( $css_url );
			if ( '' === $css_url ) {
				continue;
			}

			if ( array_key_exists( $css_url, $asset_size_cache ) ) {
				$size = $asset_size_cache[ $css_url ];
			} else {
				$size = myaio_site_explorer_get_remote_file_size_bytes( $css_url );
				$asset_size_cache[ $css_url ] = $size;
			}

			$css_files[] = array(
				'url'        => $css_url,
				'size_bytes' => $size,
				'size_kb'    => myaio_site_explorer_bytes_to_kb( $size ),
			);

			if ( is_int( $size ) && $size >= 0 ) {
				$page_css_total_bytes += $size;
			}
		}

		foreach ( $assets['js'] as $js_url ) {
			$js_url = esc_url_raw( $js_url );
			if ( '' === $js_url ) {
				continue;
			}

			if ( array_key_exists( $js_url, $asset_size_cache ) ) {
				$size = $asset_size_cache[ $js_url ];
			} else {
				$size = myaio_site_explorer_get_remote_file_size_bytes( $js_url );
				$asset_size_cache[ $js_url ] = $size;
			}

			$js_files[] = array(
				'url'        => $js_url,
				'size_bytes' => $size,
				'size_kb'    => myaio_site_explorer_bytes_to_kb( $size ),
			);

			if ( is_int( $size ) && $size >= 0 ) {
				$page_js_total_bytes += $size;
			}
		}

		$total_html_bytes += $html_size_bytes;
		$total_css_bytes  += $page_css_total_bytes;
		$total_js_bytes   += $page_js_total_bytes;

		$results[] = array(
			'url'                  => $page_url,
			'type'                 => $type,
			'category'             => $category,

			'html_size_bytes'      => $html_size_bytes,
			'html_size_kb'         => myaio_site_explorer_bytes_to_kb( $html_size_bytes ),

			'css_total_size_bytes' => $page_css_total_bytes,
			'css_total_size_kb'    => myaio_site_explorer_bytes_to_kb( $page_css_total_bytes ),

			'js_total_size_bytes'  => $page_js_total_bytes,
			'js_total_size_kb'     => myaio_site_explorer_bytes_to_kb( $page_js_total_bytes ),

			'css_files'            => $css_files,
			'js_files'             => $js_files,
		);
	}

	return array(
		'summary' => array(
			'total_urls_scanned' => count( $results ),

			'total_html_bytes'   => $total_html_bytes,
			'total_css_bytes'    => $total_css_bytes,
			'total_js_bytes'     => $total_js_bytes,

			'total_html_kb'      => myaio_site_explorer_bytes_to_kb( $total_html_bytes ),
			'total_css_kb'       => myaio_site_explorer_bytes_to_kb( $total_css_bytes ),
			'total_js_kb'        => myaio_site_explorer_bytes_to_kb( $total_js_bytes ),
		),
		//'pages'   => $results,
	);
}

/**
 * Extract CSS and JS asset URLs from HTML.
 *
 * @param string $html HTML.
 * @param string $page_url Current page URL.
 * @return array
 */
function myaio_site_explorer_extract_css_js_assets( $html, $page_url ) {
	$css = array();
	$js  = array();

	libxml_use_internal_errors( true );
	$dom = new DOMDocument();

	// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
	@$dom->loadHTML( '<?xml encoding="UTF-8">' . $html );

	$links = $dom->getElementsByTagName( 'link' );
	foreach ( $links as $link ) {
		$rel  = strtolower( trim( (string) $link->getAttribute( 'rel' ) ) );
		$href = trim( (string) $link->getAttribute( 'href' ) );

		if ( '' === $href ) {
			continue;
		}

		if ( false !== strpos( $rel, 'stylesheet' ) ) {
			$resolved = myaio_site_explorer_resolve_asset_url( $page_url, $href );
			if ( '' !== $resolved ) {
				$css[] = $resolved;
			}
		}
	}

	$scripts = $dom->getElementsByTagName( 'script' );
	foreach ( $scripts as $script ) {
		$src = trim( (string) $script->getAttribute( 'src' ) );

		if ( '' === $src ) {
			continue;
		}

		$resolved = myaio_site_explorer_resolve_asset_url( $page_url, $src );
		if ( '' !== $resolved ) {
			$js[] = $resolved;
		}
	}

	libxml_clear_errors();

	return array(
		'css' => array_values( array_unique( array_filter( $css ) ) ),
		'js'  => array_values( array_unique( array_filter( $js ) ) ),
	);
}

/**
 * Resolve relative/protocol-relative asset URL to absolute URL.
 *
 * @param string $base_url Base page URL.
 * @param string $asset_url Asset URL.
 * @return string
 */
function myaio_site_explorer_resolve_asset_url( $base_url, $asset_url ) {
	$asset_url = trim( (string) $asset_url );
	if ( '' === $asset_url ) {
		return '';
	}

	if ( preg_match( '/^(data:|blob:|javascript:|mailto:|tel:)/i', $asset_url ) ) {
		return '';
	}

	if ( preg_match( '/^https?:\/\//i', $asset_url ) ) {
		return esc_url_raw( $asset_url );
	}

	if ( 0 === strpos( $asset_url, '//' ) ) {
		$scheme = wp_parse_url( $base_url, PHP_URL_SCHEME );
		$scheme = $scheme ? $scheme : 'https';
		return esc_url_raw( $scheme . ':' . $asset_url );
	}

	$base_parts = wp_parse_url( $base_url );
	if ( ! is_array( $base_parts ) || empty( $base_parts['host'] ) ) {
		return '';
	}

	$scheme = ! empty( $base_parts['scheme'] ) ? $base_parts['scheme'] : 'https';
	$host   = $base_parts['host'];
	$port   = ! empty( $base_parts['port'] ) ? ':' . $base_parts['port'] : '';

	if ( 0 === strpos( $asset_url, '/' ) ) {
		return esc_url_raw( $scheme . '://' . $host . $port . $asset_url );
	}

	$path = ! empty( $base_parts['path'] ) ? $base_parts['path'] : '/';
	$dir  = preg_replace( '#/[^/]*$#', '/', $path );

	return esc_url_raw( $scheme . '://' . $host . $port . $dir . $asset_url );
}

/**
 * Get remote file size in bytes.
 *
 * Returns integer size, or null if not determinable.
 *
 * @param string $url File URL.
 * @return int|null
 */
function myaio_site_explorer_get_remote_file_size_bytes( $url ) {
	$url = trim( (string) $url );
	if ( '' === $url ) {
		return null;
	}

	$head = wp_remote_head(
		$url,
		array(
			'timeout'     => 10,
			'redirection' => 3,
			'headers'     => array(
				'User-Agent' => 'MYAIO-Asset-Size-Scanner/1.0; ' . home_url(),
			),
		)
	);

	if ( ! is_wp_error( $head ) ) {
		$len = wp_remote_retrieve_header( $head, 'content-length' );
		if ( '' !== $len && null !== $len ) {
			$len_int = (int) $len;
			if ( $len_int >= 0 ) {
				return $len_int;
			}
		}
	}

	$get = wp_remote_get(
		$url,
		array(
			'timeout'     => 15,
			'redirection' => 3,
			'headers'     => array(
				'User-Agent' => 'MYAIO-Asset-Size-Scanner/1.0; ' . home_url(),
			),
		)
	);

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

	$body = wp_remote_retrieve_body( $get );
	if ( null === $body || '' === $body ) {
		return 0;
	}

	return strlen( (string) $body );
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// HELPERS — PAGE EXPLORER
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_get_sitemap_urls() {
	$sitemap  = home_url( '/sitemap.xml' );
	$response = wp_remote_get(
		$sitemap,
		array(
			'timeout'     => 5,
			'redirection' => 3,
			'headers'     => array(
				'Accept' => 'application/xml,text/xml,*/*',
			),
		)
	);

	if ( is_wp_error( $response ) ) {
		return array();
	}

	$xml = (string) wp_remote_retrieve_body( $response );
	if ( '' === $xml ) {
		return array();
	}

	$urls = array();

	if ( preg_match_all( '/<loc>(.*?)<\/loc>/i', $xml, $matches ) ) {
		foreach ( $matches[1] as $loc ) {
			$loc = trim( (string) $loc );
			if ( '' === $loc ) {
				continue;
			}
			$urls[] = rtrim( esc_url_raw( $loc ), '/' );
		}
	}

	return array_values( array_unique( $urls ) );
}

function myaio_site_explorer_parse_robots_txt() {
	$robots_url = home_url( '/robots.txt' );
	$response   = wp_remote_get(
		$robots_url,
		array(
			'timeout'     => 5,
			'redirection' => 3,
			'headers'     => array(
				'Accept' => 'text/plain,*/*',
			),
		)
	);

	if ( is_wp_error( $response ) ) {
		return array();
	}

	$body = strtolower( (string) wp_remote_retrieve_body( $response ) );
	if ( '' === $body ) {
		return array();
	}

	$lines = preg_split( '/\r\n|\r|\n/', $body );
	if ( ! is_array( $lines ) ) {
		return array();
	}

	return $lines;
}

function myaio_site_explorer_check_robots_permission( $url, $rules ) {
	$url  = (string) $url;
	$path = wp_parse_url( $url, PHP_URL_PATH );

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

	foreach ( (array) $rules as $line ) {
		$line = trim( (string) $line );

		if ( '' === $line || strpos( $line, '#' ) === 0 ) {
			continue;
		}

		if ( strpos( $line, 'disallow:' ) === 0 ) {
			$blocked = trim( substr( $line, 9 ) );
			if ( '' !== $blocked && stripos( $path, $blocked ) === 0 ) {
				return false;
			}
		}
	}

	return true;
}

function myaio_site_explorer_check_noindex( $html ) {
	$html = (string) $html;
	return ! preg_match( '/<meta[^>]+robots[^>]+noindex/i', $html );
}

function myaio_site_explorer_check_nofollow( $html ) {
	$html = (string) $html;
	return ! preg_match( '/<meta[^>]+robots[^>]+nofollow/i', $html );
}

function myaio_site_explorer_check_valid_robots_meta( $html ) {
	$html = (string) $html;

	if ( ! preg_match( '/<meta[^>]+name=["\']robots["\'][^>]+content=["\']([^"\']+)["\']/i', $html, $m ) ) {
		return true;
	}

	$directives = array_map( 'trim', explode( ',', strtolower( (string) $m[1] ) ) );
	$valid      = array( 'index', 'noindex', 'follow', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex' );

	foreach ( $directives as $d ) {
		if ( '' === $d ) {
			continue;
		}
		if ( ! in_array( $d, $valid, true ) ) {
			return false;
		}
	}

	return true;
}

function myaio_site_explorer_check_canonical_exists( $html ) {
	return (bool) preg_match( '/<link[^>]+rel=["\']canonical["\']/i', (string) $html );
}

function myaio_site_explorer_check_single_canonical( $html ) {
	return (int) preg_match_all( '/<link[^>]+rel=["\']canonical["\']/i', (string) $html, $m ) === 1;
}

function myaio_site_explorer_check_canonical_match( $html, $url ) {
	$html = (string) $html;
	$url  = (string) $url;

	if ( ! preg_match( '/<link[^>]+rel=["\']canonical["\'][^>]+href=["\']([^"\']+)["\']/i', $html, $m ) ) {
		return false;
	}

	$canon = esc_url_raw( (string) $m[1] );

	return rtrim( $url, '/' ) === rtrim( $canon, '/' );
}

function myaio_site_explorer_check_no_inline_css( $html ) {
	return ! preg_match( '/style=["\']/i', (string) $html );
}

function myaio_site_explorer_check_image_formats( $html ) {
	$html = (string) $html;

	if ( preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $m ) ) {
		foreach ( $m[1] as $src ) {
			$src = (string) $src;

			if ( stripos( $src, 'data:' ) === 0 ) {
				continue;
			}

			$clean = strtok( $src, '?' );
			if ( ! preg_match( '/\.(jpg|jpeg|png|svg|webp)$/i', (string) $clean ) ) {
				return false;
			}
		}
	}

	return true;
}

function myaio_site_explorer_check_deprecated_tags( $html ) {
	$deprecated = array( '<center', '<font', '<bgsound', '<marquee' );
	$html       = (string) $html;

	foreach ( $deprecated as $tag ) {
		if ( stripos( $html, $tag ) !== false ) {
			return false;
		}
	}

	return true;
}

function myaio_site_explorer_check_links_have_href( $html ) {
	$html = (string) $html;

	if ( preg_match_all( '/<a\b([^>]*)>/i', $html, $matches ) ) {
		foreach ( $matches[1] as $attr ) {
			$attr = (string) $attr;
			if ( ! preg_match( '/href=["\']([^"\']+)/i', $attr ) ) {
				return false;
			}
		}
	}

	return true;
}

function myaio_site_explorer_check_all_images_have_alt( $html ) {
	$html = (string) $html;

	if ( ! preg_match_all( '/<img\b([^>]*)>/i', $html, $matches ) ) {
		return true;
	}

	foreach ( $matches[1] as $attrs ) {
		if ( ! preg_match( '/\balt\s*=\s*(["\'])(.*?)\1/i', (string) $attrs, $m ) ) {
			return false;
		}
		if ( '' === trim( (string) $m[2] ) ) {
			return false;
		}
	}

	return true;
}

function myaio_site_explorer_check_all_images_under_100kb( $html ) {
	$html = (string) $html;

	if ( ! preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $m ) ) {
		return true;
	}

	$max_bytes = 100 * 1024;

	foreach ( $m[1] as $src ) {
		$bytes = myaio_site_explorer_get_image_size_bytes( (string) $src );

		if ( null === $bytes ) {
			return false;
		}

		if ( $bytes > $max_bytes ) {
			return false;
		}
	}

	return true;
}

function myaio_site_explorer_get_image_size_bytes( $src ) {
	$src = trim( (string) $src );
	if ( '' === $src ) {
		return null;
	}

	if ( stripos( $src, 'data:' ) === 0 ) {
		$comma_pos = strpos( $src, ',' );
		if ( false === $comma_pos ) {
			return null;
		}

		$meta = substr( $src, 0, $comma_pos );
		$data = substr( $src, $comma_pos + 1 );

		if ( stripos( $meta, ';base64' ) !== false ) {
			$raw_len = (int) floor( strlen( $data ) * 3 / 4 );
			return $raw_len > 0 ? $raw_len : null;
		}

		$len = strlen( urldecode( $data ) );
		return $len > 0 ? $len : null;
	}

	if ( strpos( $src, '//' ) === 0 ) {
		$src = ( is_ssl() ? 'https:' : 'http:' ) . $src;
	} elseif ( strpos( $src, 'http://' ) !== 0 && strpos( $src, 'https://' ) !== 0 ) {
		if ( strpos( $src, '/' ) === 0 ) {
			$src = home_url( $src );
		} else {
			$src = home_url( '/' . ltrim( $src, '/' ) );
		}
	}

	$src = esc_url_raw( $src );

	$head = wp_remote_head(
		$src,
		array(
			'timeout'     => 8,
			'redirection' => 3,
		)
	);

	if ( ! is_wp_error( $head ) ) {
		$len = wp_remote_retrieve_header( $head, 'content-length' );
		if ( '' !== $len && null !== $len ) {
			$len_int = (int) $len;
			if ( $len_int > 0 ) {
				return $len_int;
			}
		}
	}

	$get = wp_remote_get(
		$src,
		array(
			'timeout'     => 10,
			'redirection' => 3,
		)
	);

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

	$body = wp_remote_retrieve_body( $get );
	if ( '' === $body || null === $body ) {
		return null;
	}

	return strlen( (string) $body );
}

function myaio_site_explorer_extract_schema_json( $html ) {
	$result = array(
		'exists' => false,
		'json'   => null,
	);

	$html = (string) $html;

	if ( preg_match_all( '/<script[^>]+application\/ld\+json[^>]*>(.*?)<\/script>/is', $html, $matches ) ) {
		$schemas = array();

		foreach ( $matches[1] as $json_block ) {
			$json_block = trim( (string) $json_block );
			if ( '' === $json_block ) {
				continue;
			}

			$decoded = json_decode( $json_block, true );
			if ( ! empty( $decoded ) ) {
				$schemas[] = $decoded;
			}
		}

		if ( ! empty( $schemas ) ) {
			$result['exists'] = true;
			$result['json']   = $schemas;
		}
	}

	return $result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// HELPERS — BACKLINK EXPLORER
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_extract_links_with_context( $html, $site_home ) {
	libxml_use_internal_errors( true );

	$dom = new DOMDocument();

	// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
	@$dom->loadHTML( '<?xml encoding="UTF-8">' . $html );

	$links = $dom->getElementsByTagName( 'a' );

	$internal = array();
	$external = array();
	$all      = array();

	$nofollow_count = 0;
	$dofollow_count = 0;

	foreach ( $links as $link ) {
		$href = trim( (string) $link->getAttribute( 'href' ) );
		if ( ! $href ) {
			continue;
		}

		$l = strtolower( $href );
		if ( preg_match( '/^(mailto:|tel:|javascript:|#)/i', $l ) ) {
			continue;
		}

		if ( strpos( $href, '/' ) === 0 ) {
			$href = home_url( $href );
		}

		if ( ! preg_match( '/^https?:\/\//i', $href ) ) {
			continue;
		}

		$href = rtrim( $href, '/' );

		$snippet_dom = $dom->saveHTML( $link );

		$raw_rel = null;
		if ( preg_match( '/<a[^>]+href=["\']' . preg_quote( $href, '/' ) . '["\'][^>]*>/i', $html, $match_a ) ) {
			if ( preg_match( '/rel=["\']([^"\']+)["\']/i', $match_a[0], $rel_match ) ) {
				$raw_rel = strtolower( $rel_match[1] );
			}
		}

		$nofollow = false;
		if ( ! empty( $raw_rel ) ) {
			$rel_parts = preg_split( '/\s+/', $raw_rel );
			$nofollow  = in_array( 'nofollow', $rel_parts, true );
		}

		if ( $nofollow ) {
			$nofollow_count++;
		} else {
			$dofollow_count++;
		}

		$anchor    = trim( (string) $link->textContent );
		$placement = myaio_site_explorer_detect_link_location( $link );

		$info = array(
			'href'         => $href,
			'anchor'       => $anchor,
			'nofollow'     => $nofollow,
			'dofollow'     => ! $nofollow,
			'html_snippet' => $snippet_dom,
			'placement'    => $placement,
		);

		if ( strpos( $href, $site_home ) === 0 ) {
			$internal[] = $info;
		} else {
			$external[] = $info;
		}

		$all[] = $info;
	}

	return array(
		'internal_links' => $internal,
		'external_links' => $external,
		'all_links'      => $all,
		'nofollow_count' => $nofollow_count,
		'dofollow_count' => $dofollow_count,
	);
}

function myaio_site_explorer_detect_link_location( DOMNode $node ) {
	while ( $node && $node->nodeName ) {
		$tag = strtolower( (string) $node->nodeName );

		if ( 'header' === $tag || myaio_site_explorer_node_has_class( $node, array( 'header', 'site-header', 'main-header' ) ) ) {
			return 'header';
		}
		if ( 'nav' === $tag || myaio_site_explorer_node_has_class( $node, array( 'menu', 'navbar', 'main-menu' ) ) ) {
			return 'navigation';
		}
		if ( 'footer' === $tag || myaio_site_explorer_node_has_class( $node, array( 'footer', 'site-footer' ) ) ) {
			return 'footer';
		}
		if ( 'aside' === $tag || myaio_site_explorer_node_has_class( $node, array( 'sidebar', 'widget-area' ) ) ) {
			return 'sidebar';
		}

		if ( myaio_site_explorer_node_has_class( $node, array( 'elementor-section', 'elementor-widget', 'elementor-column' ) ) ) {
			return 'elementor-section';
		}
		if ( myaio_site_explorer_node_has_class( $node, array( 'wp-block', 'wp-block-group' ) ) ) {
			return 'gutenberg-block';
		}

		if ( myaio_site_explorer_node_has_class( $node, array( 'entry-content', 'post-content', 'page-content', 'content' ) ) ) {
			return 'content';
		}

		$node = $node->parentNode;
	}

	return 'body';
}

function myaio_site_explorer_node_has_class( $node, array $classes ) {
	if ( ! $node->attributes ) {
		return false;
	}

	$attr = $node->attributes->getNamedItem( 'class' );
	if ( ! $attr ) {
		return false;
	}

	$val = strtolower( (string) $attr->nodeValue );

	foreach ( $classes as $check ) {
		if ( strpos( $val, strtolower( (string) $check ) ) !== false ) {
			return true;
		}
	}

	return false;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// HELPERS — REDIRECTS
////////////////////////////////////////////////////////////////////////////////////////////////////

function myaio_site_explorer_resolve_url( $base, $location ) {
	$base     = (string) $base;
	$location = (string) $location;

	if ( preg_match( '/^https?:\/\//i', $location ) ) {
		return $location;
	}

	if ( 0 === strpos( $location, '//' ) ) {
		$scheme = wp_parse_url( $base, PHP_URL_SCHEME );
		$scheme = $scheme ? $scheme : 'https';
		return $scheme . ':' . $location;
	}

	$base_parts = wp_parse_url( $base );
	if ( ! is_array( $base_parts ) || empty( $base_parts['host'] ) ) {
		return $location;
	}

	$scheme = ! empty( $base_parts['scheme'] ) ? $base_parts['scheme'] : 'https';
	$host   = $base_parts['host'];
	$port   = ! empty( $base_parts['port'] ) ? ':' . $base_parts['port'] : '';

	if ( 0 === strpos( $location, '/' ) ) {
		return $scheme . '://' . $host . $port . $location;
	}

	$path = ! empty( $base_parts['path'] ) ? $base_parts['path'] : '/';
	$dir  = preg_replace( '#/[^/]*$#', '/', $path );

	return $scheme . '://' . $host . $port . $dir . $location;
}