<?php
/**
 * File: modules/knowledge-graph-signals-api.php
 * Endpoint: GET /wp-json/myaio/v1/knowledge-graph-signals
 *
 * Optional params:
 *   ?url=https://example.com/page   (default: site home URL)
 *   ?include_html=1                (include detected header/footer HTML snippets if found)
 *   ?include_raw_jsonld=1          (include the raw JSON-LD blocks; can be large)
 *   ?include_links=1               (include all extracted links for debugging)
 *   ?key=YOUR_KEY                  (legacy; prefer header x-api-key)
 *   ?api_key=YOUR_KEY              (legacy; prefer header x-api-key)
 */

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

/**
 * IMPORTANT:
 * - This file is updated to WP.org guidelines.
 * - Uses shared auth utility: myaio_sac_rest_permission_check (or myaio_sac_require_api_key_or_401 if you keep it public).
 * - Output payload structure stays SAME as your current working response.
 *
 * Make sure this file can access your utils:
 * require_once plugin_dir_path( __FILE__ ) . '../includes/sac-utils.php';
 */

/* =============================================================================
 * REST ROUTE
 * ============================================================================= */

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'myaio/v1',
			'/knowledge-graph-signals',
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => 'myaio_kg_signals_api',
				/**
				 * Use shared auth utility (recommended).
				 * This keeps the endpoint protected and WP.org-friendly.
				 */
				'permission_callback' => 'myaio_sac_rest_permission_check',
				'args'                => [
					'url'               => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'esc_url_raw',
					],
					'include_html'       => [
						'required' => false,
					],
					'include_raw_jsonld' => [
						'required' => false,
					],
					'include_links'      => [
						'required' => false,
					],
					// kept for backward compatibility; auth is handled by util via header/key/api_key
					'key'               => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'api_key'            => [
						'required'          => false,
						'type'              => 'string',
						'sanitize_callback' => 'sanitize_text_field',
					],
				],
			]
		);
	}
);

/**
 * Treat "1,true,yes,on" as true; anything else false.
 */
function myaio_bool_param( $v ) {
	if ( is_bool( $v ) ) {
		return $v;
	}
	$v = strtolower( trim( (string) $v ) );
	return in_array( $v, [ '1', 'true', 'yes', 'on' ], true );
}

function myaio_kg_signals_api( WP_REST_Request $request ) {
	$include_html       = myaio_bool_param( $request->get_param( 'include_html' ) );
	$include_raw_jsonld = myaio_bool_param( $request->get_param( 'include_raw_jsonld' ) );
	$include_links      = myaio_bool_param( $request->get_param( 'include_links' ) );

	$url = trim( (string) $request->get_param( 'url' ) );
	if ( '' === $url ) {
		$url = home_url( '/' );
	}

	// Keep SAME output behavior: return ok=false with 200 for invalid url.
	if ( ! preg_match( '#^https?://#i', $url ) ) {
		return new WP_REST_Response(
			[
				'ok'      => false,
				'message' => 'Invalid url. Use absolute http(s) URL.',
				'url'     => $url,
			],
			200
		);
	}

	$resp = wp_remote_get(
		esc_url_raw( $url ),
		[
			'timeout'     => 20,
			'redirection' => 5,
			'headers'     => [
				'User-Agent' => 'Mozilla/5.0 (KnowledgeGraphSignalsBot)',
				'Accept'     => 'text/html',
			],
		]
	);

	if ( is_wp_error( $resp ) ) {
		return new WP_REST_Response(
			[
				'ok'      => false,
				'url'     => $url,
				'message' => 'Could not fetch the page.',
				'error'   => $resp->get_error_message(),
			],
			200
		);
	}

	$html = (string) wp_remote_retrieve_body( $resp );
	if ( '' === $html ) {
		return new WP_REST_Response(
			[
				'ok'      => false,
				'url'     => $url,
				'message' => 'Page HTML was empty.',
			],
			200
		);
	}

	$dom = myaio_dom_from_html( $html );
	if ( ! $dom ) {
		return new WP_REST_Response(
			[
				'ok'      => false,
				'url'     => $url,
				'message' => 'Could not parse HTML.',
			],
			200
		);
	}

	$xpath = new DOMXPath( $dom );

	// Reduce noise from page builders (keep JSON-LD scripts).
	myaio_remove_noise_nodes( $xpath, $dom->documentElement );

	// Detect areas (header/footer).
	$header_node = myaio_detect_header_node( $xpath );
	$footer_node = myaio_detect_footer_node( $xpath );

	$header_context = $header_node ? $header_node : $dom->documentElement;
	$footer_context = $footer_node ? $footer_node : $dom->documentElement;

	// Extract signals.
	$jsonld = myaio_extract_jsonld_blocks( $xpath );

	$entity = myaio_extract_entity_signals( $xpath, $jsonld );
	$logo   = myaio_extract_logo_signals( $xpath, $jsonld, $header_context );
	$nap    = myaio_extract_nap_signals( $xpath, $jsonld, $footer_context );
	$social = myaio_extract_social_signals( $xpath, $jsonld, $footer_context );

	// Keep SAME output shape.
	$response = [
		'ok'   => true,
		'url'  => $url,
		'meta' => [
			'fetched_http_code'     => (int) wp_remote_retrieve_response_code( $resp ),
			'fetched_content_type'  => (string) wp_remote_retrieve_header( $resp, 'content-type' ),
			'generated_at_utc'      => gmdate( 'c' ),
		],
		'signals' => [
			'entity_schema' => $entity,
			'logo'          => $logo,
			'nap'           => $nap,
			'social'        => $social,
		],
		'presence' => [
			'entity_schema' => (bool) ( $entity['present'] ?? false ),
			'logo'          => (bool) ( $logo['present'] ?? false ),
			'nap'           => (bool) ( $nap['present'] ?? false ),
			'social'        => (bool) ( $social['present'] ?? false ),
			'jsonld'        => ! empty( $jsonld ),
		],
		'evidence' => [
			'header_detected' => (bool) $header_node,
			'footer_detected' => (bool) $footer_node,
		],
	];

	if ( $include_html ) {
		$response['evidence']['header_html'] = $header_node ? myaio_outer_html( $header_node ) : null;
		$response['evidence']['footer_html'] = $footer_node ? myaio_outer_html( $footer_node ) : null;
	}

	if ( $include_raw_jsonld ) {
		$response['evidence']['jsonld_raw'] = $jsonld;
	}

	if ( $include_links ) {
		$response['evidence']['all_links'] = myaio_extract_links( $xpath, $dom->documentElement );
	}

	return new WP_REST_Response( $response, 200 );
}

/* =============================================================================
 * JSON-LD
 * ============================================================================= */

function myaio_extract_jsonld_blocks( DOMXPath $xpath ) {
	$out     = [];
	$scripts = $xpath->query( "//script[@type='application/ld+json']" );
	if ( ! $scripts ) {
		return $out;
	}

	foreach ( $scripts as $s ) {
		$raw = trim( (string) $s->textContent );
		if ( '' === $raw ) {
			continue;
		}

		$json = json_decode( $raw, true );
		if ( JSON_ERROR_NONE !== json_last_error() ) {
			continue;
		}
		if ( ! is_array( $json ) ) {
			continue;
		}

		$out[] = $json;
	}

	return $out;
}

/* =============================================================================
 * ENTITY SCHEMA SIGNALS
 * ============================================================================= */

function myaio_extract_entity_signals( DOMXPath $xpath, $jsonld_blocks ) {
	$types              = [];
	$has_name           = false;
	$has_url            = false;
	$has_name_url       = false;
	$has_logo_in_schema = false;

	$name_value        = null;
	$url_value         = null;
	$schema_logo_value = null;

	// Microdata.
	$items = $xpath->query( '//*[@itemscope and @itemtype]' );
	if ( $items && $items->length ) {
		foreach ( $items as $n ) {
			$itemtype = (string) $n->getAttribute( 'itemtype' );
			if ( false !== stripos( $itemtype, 'schema.org' ) ) {
				$types[] = $itemtype;

				$name = myaio_first_text( $xpath, ".//*[@itemprop='name']", $n );
				$url  = myaio_first_attr( $xpath, ".//*[@itemprop='url']/@href | .//*[@itemprop='url']/@content", $n );
				$logo = myaio_first_attr( $xpath, ".//*[@itemprop='logo']/@content | .//*[@itemprop='logo']/@src | .//*[@itemprop='logo']//img/@src | .//*[@itemprop='logo']/@href", $n );

				if ( $name && null === $name_value ) {
					$name_value = $name;
				}
				if ( $url && null === $url_value ) {
					$url_value = $url;
				}
				if ( $logo && null === $schema_logo_value ) {
					$schema_logo_value = $logo;
				}

				if ( $name ) {
					$has_name = true;
				}
				if ( $url ) {
					$has_url = true;
				}
				if ( $name && $url ) {
					$has_name_url = true;
				}
				if ( $logo ) {
					$has_logo_in_schema = true;
				}
			}
		}
	}

	// JSON-LD scan (also tracks @context).
	$scan               = myaio_scan_jsonld_for_entity( $jsonld_blocks );
	$types              = array_merge( $types, $scan['types'] );
	$has_name           = $has_name || $scan['has_name'];
	$has_url            = $has_url || $scan['has_url'];
	$has_name_url       = $has_name_url || $scan['has_name_url'];
	$has_logo_in_schema = $has_logo_in_schema || $scan['has_logo'];
	$contexts           = $scan['contexts'];
	$context_present    = (bool) $scan['has_context'];

	if ( empty( $name_value ) && ! empty( $scan['name'] ) ) {
		$name_value = $scan['name'];
	}
	if ( empty( $url_value ) && ! empty( $scan['url'] ) ) {
		$url_value = $scan['url'];
	}
	if ( empty( $schema_logo_value ) && ! empty( $scan['logo'] ) ) {
		$schema_logo_value = $scan['logo'];
	}

	$types    = array_values( array_unique( array_filter( $types ) ) );
	$contexts = array_values( array_unique( array_filter( $contexts ) ) );

	return [
		'present'               => ! empty( $types ),
		'types'                 => $types,
		'name'                  => $name_value,
		'url'                   => $url_value,
		'logo_in_schema'        => $schema_logo_value,
		'name_present'          => (bool) $has_name,
		'url_present'           => (bool) $has_url,
		'name_url_present'      => (bool) $has_name_url,
		'name_url'              => ( $name_value && $url_value ) ? [ 'name' => $name_value, 'url' => $url_value ] : null,
		'logo_in_schema_present' => (bool) $has_logo_in_schema,
		'context_present'       => $context_present,
		'contexts'              => $contexts,
		'sources'               => [
			'microdata' => (bool) ( $items && $items->length ),
			'jsonld'    => ! empty( $jsonld_blocks ),
		],
	];
}

function myaio_scan_jsonld_for_entity( $json ) {
	$types        = [];
	$has_name     = false;
	$has_url      = false;
	$has_name_url = false;
	$has_logo     = false;
	$contexts     = [];
	$has_context  = false;

	$name_value = null;
	$url_value  = null;
	$logo_value = null;

	if ( ! is_array( $json ) ) {
		return [
			'types'        => [],
			'has_name'     => false,
			'has_url'      => false,
			'has_name_url' => false,
			'has_logo'     => false,
			'contexts'     => [],
			'has_context'  => false,
			'name'         => null,
			'url'          => null,
			'logo'         => null,
		];
	}

	// list of blocks.
	if ( array_keys( $json ) === range( 0, count( $json ) - 1 ) ) {
		foreach ( $json as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			$r = myaio_scan_jsonld_for_entity( $item );

			$types        = array_merge( $types, $r['types'] );
			$has_name     = $has_name || $r['has_name'];
			$has_url      = $has_url || $r['has_url'];
			$has_name_url = $has_name_url || $r['has_name_url'];
			$has_logo     = $has_logo || $r['has_logo'];
			$contexts     = array_merge( $contexts, $r['contexts'] );
			$has_context  = $has_context || $r['has_context'];

			if ( null === $name_value && ! empty( $r['name'] ) ) {
				$name_value = $r['name'];
			}
			if ( null === $url_value && ! empty( $r['url'] ) ) {
				$url_value = $r['url'];
			}
			if ( null === $logo_value && ! empty( $r['logo'] ) ) {
				$logo_value = $r['logo'];
			}
		}

		$types    = array_values( array_unique( array_filter( $types ) ) );
		$contexts = array_values( array_unique( array_filter( $contexts ) ) );

		return [
			'types'        => $types,
			'has_name'     => $has_name,
			'has_url'      => $has_url,
			'has_name_url' => $has_name_url,
			'has_logo'     => $has_logo,
			'contexts'     => $contexts,
			'has_context'  => $has_context,
			'name'         => $name_value,
			'url'          => $url_value,
			'logo'         => $logo_value,
		];
	}

	// @graph support.
	if ( isset( $json['@graph'] ) && is_array( $json['@graph'] ) ) {
		return myaio_scan_jsonld_for_entity( $json['@graph'] );
	}

	// @context.
	if ( ! empty( $json['@context'] ) ) {
		$has_context = true;
		if ( is_array( $json['@context'] ) ) {
			foreach ( $json['@context'] as $ctx ) {
				$contexts[] = (string) $ctx;
			}
		} else {
			$contexts[] = (string) $json['@context'];
		}
	}

	// @type.
	if ( ! empty( $json['@type'] ) ) {
		if ( is_array( $json['@type'] ) ) {
			foreach ( $json['@type'] as $t ) {
				$types[] = (string) $t;
			}
		} else {
			$types[] = (string) $json['@type'];
		}
	}

	if ( ! empty( $json['name'] ) ) {
		$has_name = true;
		if ( null === $name_value && is_scalar( $json['name'] ) ) {
			$name_value = (string) $json['name'];
		}
	}

	if ( ! empty( $json['url'] ) ) {
		$has_url = true;
		if ( null === $url_value && is_scalar( $json['url'] ) ) {
			$url_value = (string) $json['url'];
		}
	}

	if ( ! empty( $json['name'] ) && ! empty( $json['url'] ) ) {
		$has_name_url = true;
	}

	// logo field OR image field.
	if ( ! empty( $json['logo'] ) || ! empty( $json['image'] ) ) {
		$has_logo = true;

		if ( null === $logo_value ) {
			$cand = ! empty( $json['logo'] ) ? $json['logo'] : $json['image'];

			if ( is_string( $cand ) ) {
				$logo_value = $cand;
			} elseif ( is_array( $cand ) ) {
				// common patterns: { "url": "..." } or a list of strings/objects.
				if ( ! empty( $cand['url'] ) && is_string( $cand['url'] ) ) {
					$logo_value = $cand['url'];
				} elseif ( array_keys( $cand ) === range( 0, count( $cand ) - 1 ) ) {
					foreach ( $cand as $it ) {
						if ( is_string( $it ) ) {
							$logo_value = $it;
							break;
						}
						if ( is_array( $it ) && ! empty( $it['url'] ) && is_string( $it['url'] ) ) {
							$logo_value = $it['url'];
							break;
						}
					}
				}
			}
		}
	}

	// recurse.
	foreach ( $json as $v ) {
		if ( is_array( $v ) ) {
			$r = myaio_scan_jsonld_for_entity( $v );
			$types        = array_merge( $types, $r['types'] );
			$has_name     = $has_name || $r['has_name'];
			$has_url      = $has_url || $r['has_url'];
			$has_name_url = $has_name_url || $r['has_name_url'];
			$has_logo     = $has_logo || $r['has_logo'];
			$contexts     = array_merge( $contexts, $r['contexts'] );
			$has_context  = $has_context || $r['has_context'];

			if ( null === $name_value && ! empty( $r['name'] ) ) {
				$name_value = $r['name'];
			}
			if ( null === $url_value && ! empty( $r['url'] ) ) {
				$url_value = $r['url'];
			}
			if ( null === $logo_value && ! empty( $r['logo'] ) ) {
				$logo_value = $r['logo'];
			}
		}
	}

	$types    = array_values( array_unique( array_filter( $types ) ) );
	$contexts = array_values( array_unique( array_filter( $contexts ) ) );

	return [
		'types'        => $types,
		'has_name'     => $has_name,
		'has_url'      => $has_url,
		'has_name_url' => $has_name_url,
		'has_logo'     => $has_logo,
		'contexts'     => $contexts,
		'has_context'  => $has_context,
		'name'         => $name_value,
		'url'          => $url_value,
		'logo'         => $logo_value,
	];
}

/* =============================================================================
 * LOGO SIGNALS
 * ============================================================================= */

function myaio_extract_logo_signals( DOMXPath $xpath, $jsonld_blocks, DOMNode $header_context ) {
	$logo_schema = myaio_find_first_logo_in_jsonld( $jsonld_blocks );

	$logo_html = myaio_first_attr(
		$xpath,
		".//*[contains(@class,'logo') or contains(@id,'logo') or ancestor::header]//img/@src",
		$header_context
	);

	$logo_og = myaio_first_attr( $xpath, "//meta[@property='og:image']/@content" );

	$present = (bool) ( $logo_schema || $logo_html || $logo_og );

	return [
		'present'     => $present,
		'schema_logo' => $logo_schema ? myaio_abs_url( $logo_schema ) : null,
		'html_logo'   => $logo_html ? myaio_abs_url( $logo_html ) : null,
		'og_image'    => $logo_og ? myaio_abs_url( $logo_og ) : null,
		'best_guess'  => myaio_abs_url( $logo_schema ? $logo_schema : ( $logo_html ? $logo_html : $logo_og ) ),
		'sources'     => [
			'schema' => (bool) $logo_schema,
			'html'   => (bool) $logo_html,
			'og'     => (bool) $logo_og,
		],
	];
}

function myaio_find_first_logo_in_jsonld( $json ) {
	if ( ! is_array( $json ) ) {
		return null;
	}

	// list of blocks.
	if ( array_keys( $json ) === range( 0, count( $json ) - 1 ) ) {
		foreach ( $json as $item ) {
			$r = myaio_find_first_logo_in_jsonld( $item );
			if ( $r ) {
				return $r;
			}
		}
		return null;
	}

	// @graph.
	if ( isset( $json['@graph'] ) && is_array( $json['@graph'] ) ) {
		return myaio_find_first_logo_in_jsonld( $json['@graph'] );
	}

	if ( isset( $json['logo'] ) ) {
		if ( is_string( $json['logo'] ) ) {
			return $json['logo'];
		}
		if ( is_array( $json['logo'] ) && ! empty( $json['logo']['url'] ) ) {
			return $json['logo']['url'];
		}
	}

	if ( isset( $json['image'] ) ) {
		if ( is_string( $json['image'] ) ) {
			return $json['image'];
		}
		if ( is_array( $json['image'] ) && ! empty( $json['image']['url'] ) ) {
			return $json['image']['url'];
		}
	}

	foreach ( $json as $v ) {
		if ( is_array( $v ) ) {
			$r = myaio_find_first_logo_in_jsonld( $v );
			if ( $r ) {
				return $r;
			}
		}
	}

	return null;
}

/* =============================================================================
 * NAP SIGNALS (Name/Address/Phone)
 * ============================================================================= */
/**
 * NOTE:
 * Your existing NAP helpers and logic are preserved below (only formatting/safety updated).
 * I kept your behavior and output keys unchanged.
 */

function myaio_extract_map_query_address( $url ) {
	$url = trim( (string) $url );
	if ( '' === $url ) {
		return null;
	}

	$decoded = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5 );

	$keys = [ 'q', 'query', 'destination', 'daddr', 'addr', 'address' ];
	foreach ( $keys as $k ) {
		if ( preg_match( '/[?&]' . preg_quote( $k, '/' ) . '=([^&#]+)/i', $decoded, $m ) ) {
			$val = urldecode( str_replace( '+', ' ', $m[1] ) );
			$val = trim( $val );
			if ( '' !== $val ) {
				return $val;
			}
		}
	}

	// "/place/<text>" or "/search/<text>".
	if ( preg_match( '#/(place|search)/([^/?#]+)#i', $decoded, $m ) ) {
		$val = urldecode( str_replace( '+', ' ', $m[2] ) );
		$val = trim( $val );
		if ( '' !== $val ) {
			return $val;
		}
	}

	return null;
}

function myaio_score_address_candidate( $text ) {
	$t = myaio_clean_text( $text );
	if ( ! $t ) {
		return -999;
	}

	if ( strlen( $t ) > 260 ) {
		return -20;
	}

	if ( preg_match( '/\b(quick links|home|about|services|gallery|contact|menu|privacy|terms|copyright|©|all rights reserved|website developed|powered by)\b/i', $t ) ) {
		return -15;
	}

	$score = 0;

	if ( preg_match( '/\b\d{1,6}\b/', $t ) ) {
		$score += 2;
	}
	if ( substr_count( $t, ',' ) >= 2 ) {
		$score += 1;
	}
	if ( preg_match( '/,\s*\S+/', $t ) ) {
		$score += 1;
	}

	if ( preg_match( '/\b(street|st|st\.|road|rd|rd\.|avenue|ave|ave\.|boulevard|blvd|blvd\.|lane|ln|ln\.|drive|dr|dr\.|court|ct|ct\.|circle|cir|cir\.|highway|hwy|hwy\.|parkway|pkwy|pkwy\.|suite|ste|ste\.|unit|floor|fl|fl\.|building|bldg)\b/i', $t ) ) {
		$score += 2;
	}

	if ( preg_match( '/\b\d{5}(-\d{4})?\b/', $t ) ) {
		$score += 2;
	}

	if ( preg_match( '/\b[A-Z]\d[A-Z]\s?\d[A-Z]\d\b/i', $t ) ) {
		$score += 2;
	}
	if ( preg_match( '/,\s*(AB|BC|MB|NB|NL|NS|NT|NU|ON|PE|QC|SK|YT)\b/i', $t ) ) {
		$score += 1;
	}

	if ( preg_match( '/[.!?]\s/', $t ) ) {
		$score -= 2;
	}

	$words = preg_split( '/\s+/', $t );
	if ( is_array( $words ) && count( $words ) > 22 ) {
		$score -= 2;
	}

	return $score;
}

function myaio_looks_like_address( $text ) {
	return myaio_score_address_candidate( $text ) >= 4;
}

function myaio_collect_footer_address_candidates( DOMXPath $xpath, DOMNode $footer_context, $emails = [], $phones = [] ) {
	$cands = [];

	$contact_links = $xpath->query( ".//a[starts-with(@href,'tel:') or starts-with(@href,'mailto:')]", $footer_context );
	if ( $contact_links && $contact_links->length ) {
		foreach ( $contact_links as $link ) {
			$cur = $link;
			for ( $i = 0; $i < 7; $i++ ) {
				if ( ! $cur || ! $cur->parentNode ) {
					break;
				}
				$cur = $cur->parentNode;

				$txt = myaio_clean_text( $cur->textContent );
				if ( ! $txt ) {
					continue;
				}
				if ( strlen( $txt ) > 650 ) {
					continue;
				}
				if ( ! preg_match( '/@|\\b\\d{3}[-.\\s]?\\d{3}[-.\\s]?\\d{4}\\b|\\b\\+?\\d[\\d\\s().-]{7,}\\b/', $txt ) ) {
					continue;
				}

				$cands[] = $txt;
				break;
			}
		}
	}

	$map_nodes = $xpath->query(
		".//a[contains(@href,'google.com/maps') or contains(@href,'maps.google') or contains(@href,'goo.gl/maps') or contains(@href,'apple.com/maps') or starts-with(@href,'geo:')]",
		$footer_context
	);
	if ( $map_nodes && $map_nodes->length ) {
		foreach ( $map_nodes as $a ) {
			$href    = (string) $a->getAttribute( 'href' );
			$fromurl = myaio_extract_map_query_address( $href );
			if ( $fromurl ) {
				$cands[] = $fromurl;
			}

			$cands[] = myaio_clean_text( $a->textContent );
			if ( $a->parentNode ) {
				$cands[] = myaio_clean_text( $a->parentNode->textContent );
			}
		}
	}

	$iframe_nodes = $xpath->query(
		".//iframe[contains(@src,'google.com/maps') or contains(@src,'maps.google') or contains(@src,'apple.com/maps')]",
		$footer_context
	);
	if ( $iframe_nodes && $iframe_nodes->length ) {
		foreach ( $iframe_nodes as $ifr ) {
			$src     = (string) $ifr->getAttribute( 'src' );
			$fromsrc = myaio_extract_map_query_address( $src );
			if ( $fromsrc ) {
				$cands[] = $fromsrc;
			}
			if ( $ifr->parentNode ) {
				$cands[] = myaio_clean_text( $ifr->parentNode->textContent );
			}
		}
	}

	$nodes = $xpath->query( ".//address | .//li | .//p | .//span | .//div", $footer_context );
	if ( $nodes && $nodes->length ) {
		foreach ( $nodes as $n ) {
			$t = myaio_clean_text( $n->textContent );
			if ( ! $t ) {
				continue;
			}
			if ( strlen( $t ) < 10 || strlen( $t ) > 260 ) {
				continue;
			}
			$cands[] = $t;
		}
	}

	$footer_text_raw = (string) $footer_context->textContent;
	$lines           = preg_split( '/\\r?\\n+|\\|+|•|·|—|–/', $footer_text_raw );
	if ( $lines ) {
		foreach ( $lines as $ln ) {
			$ln = myaio_clean_text( $ln );
			if ( ! $ln ) {
				continue;
			}
			if ( strlen( $ln ) < 10 || strlen( $ln ) > 260 ) {
				continue;
			}
			$cands[] = $ln;
		}
	}

	$cands = array_values( array_unique( array_filter( $cands ) ) );
	return $cands;
}

function myaio_extract_best_footer_address( DOMXPath $xpath, DOMNode $footer_context, $emails = [], $phones = [] ) {
	$cands = myaio_collect_footer_address_candidates( $xpath, $footer_context, $emails, $phones );

	$best      = null;
	$bestScore = -999;

	foreach ( $cands as $c ) {
		$picked = myaio_pick_address_line( $c );
		if ( ! $picked ) {
			continue;
		}

		$score = myaio_score_address_candidate( $picked );
		if ( $score > $bestScore ) {
			$bestScore = $score;
			$best      = $picked;
		}
	}

	if ( $best && myaio_looks_like_address( $best ) ) {
		return $best;
	}
	return null;
}

function myaio_extract_address_substring( $text ) {
	$t = myaio_clean_text( $text );
	if ( ! $t ) {
		return null;
	}

	$t = preg_replace( '/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}/i', ' ', $t );
	$t = preg_replace( '/(\+?\(?\d[\d\s().\-]{7,}\d)/', ' ', $t );

	$t = preg_replace( '/\s+/', ' ', (string) $t );
	$t = trim( $t );

	if ( preg_match( '/\b(\d{1,6}\s+[A-Z0-9#\.\-\' ]+?,\s*[A-Z]{2}\s*\d{5}(?:-\d{4})?(?:,\s*USA)?)\b/i', $t, $m ) ) {
		return trim( $m[1] );
	}

	if ( preg_match( '/\b([A-Z0-9#\.\-\' ]+?,\s*(AB|BC|MB|NB|NL|NS|NT|NU|ON|PE|QC|SK|YT)\s*[A-Z]\d[A-Z]\s?\d[A-Z]\d)\b/i', $t, $m ) ) {
		return trim( $m[1] );
	}

	if ( preg_match( '/\b(\d{1,6}\s+.{5,160}?\b(?:\d{5}(?:-\d{4})?|[A-Z]\d[A-Z]\s?\d[A-Z]\d)(?:,\s*USA)?)\b/i', $t, $m ) ) {
		return trim( $m[1] );
	}

	return null;
}

function myaio_extract_nap_signals( DOMXPath $xpath, $jsonld_blocks, DOMNode $footer_context ) {
	$emails  = [];
	$phones  = [];
	$address = null;

	$mailto_nodes = $xpath->query( ".//a[starts-with(@href,'mailto:')]/@href", $footer_context );
	if ( $mailto_nodes && $mailto_nodes->length ) {
		foreach ( $mailto_nodes as $n ) {
			$emails[] = preg_replace( '/^mailto:/i', '', trim( (string) $n->nodeValue ) );
		}
	}

	$tel_nodes = $xpath->query( ".//a[starts-with(@href,'tel:')]/@href", $footer_context );
	if ( $tel_nodes && $tel_nodes->length ) {
		foreach ( $tel_nodes as $n ) {
			$phones[] = preg_replace( '/^tel:/i', '', trim( (string) $n->nodeValue ) );
		}
	}

	$schema_addr = myaio_first_text( $xpath, "//*[@itemprop='address']" );
	$schema_tel  = myaio_first_attr( $xpath, "//*[@itemprop='telephone']/@content | //*[@itemprop='telephone']/@href" );
	if ( $schema_tel ) {
		$phones[] = preg_replace( '/^tel:/i', '', $schema_tel );
	}

	$jsonld_addr = myaio_find_postal_address_in_jsonld( $jsonld_blocks );
	if ( ! $address && $jsonld_addr && myaio_looks_like_address( $jsonld_addr ) ) {
		$address = $jsonld_addr;
	}

	if ( ! $address ) {
		if ( $schema_addr && myaio_looks_like_address( $schema_addr ) ) {
			$address = $schema_addr;
		} else {
			$address = myaio_extract_best_footer_address( $xpath, $footer_context, $emails, $phones );
		}
	}

	if ( $address && ! myaio_looks_like_address( $address ) ) {
		$address = null;
	}

	if ( $address ) {
		$only = myaio_extract_address_substring( $address );
		if ( $only && myaio_looks_like_address( $only ) ) {
			$address = $only;
		}
	}

	$emails = array_values( array_unique( array_filter( $emails ) ) );
	$phones = array_values( array_unique( array_filter( $phones ) ) );

	$present = (bool) ( $address || ! empty( $phones ) || ! empty( $emails ) || $schema_addr || $schema_tel );

	return [
		'present'        => $present,
		'emails'         => $emails,
		'phones'         => $phones,
		'address'        => ( $address && myaio_looks_like_address( $address ) ) ? $address : null,
		'schema_address' => $schema_addr ? $schema_addr : null,
		'schema_phone'   => $schema_tel ? $schema_tel : null,
		'sources'        => [
			'footer_links' => ( ! empty( $emails ) || ! empty( $phones ) ),
			'microdata'    => (bool) ( $schema_addr || $schema_tel ),
			'jsonld'       => (bool) $jsonld_addr,
			'heuristic'    => (bool) $address && ! $jsonld_addr && ! $schema_addr,
		],
	];
}

function myaio_find_postal_address_in_jsonld( $json ) {
	if ( ! is_array( $json ) ) {
		return null;
	}

	if ( array_keys( $json ) === range( 0, count( $json ) - 1 ) ) {
		foreach ( $json as $item ) {
			$r = myaio_find_postal_address_in_jsonld( $item );
			if ( $r ) {
				return $r;
			}
		}
		return null;
	}

	if ( isset( $json['@graph'] ) && is_array( $json['@graph'] ) ) {
		return myaio_find_postal_address_in_jsonld( $json['@graph'] );
	}

	if ( isset( $json['address'] ) ) {
		$a = $json['address'];

		if ( is_string( $a ) ) {
			return myaio_pick_address_line( $a );
		}

		if ( is_array( $a ) ) {
			$parts = [];
			foreach ( [ 'streetAddress', 'postOfficeBoxNumber' ] as $k ) {
				if ( ! empty( $a[ $k ] ) ) {
					$parts[] = $a[ $k ];
				}
			}
			foreach ( [ 'addressLocality', 'addressRegion', 'postalCode', 'addressCountry' ] as $k ) {
				if ( ! empty( $a[ $k ] ) ) {
					$parts[] = $a[ $k ];
				}
			}
			$joined = trim( implode( ', ', $parts ) );
			$joined = myaio_pick_address_line( $joined );
			if ( $joined ) {
				return $joined;
			}
		}
	}

	foreach ( $json as $v ) {
		if ( is_array( $v ) ) {
			$r = myaio_find_postal_address_in_jsonld( $v );
			if ( $r ) {
				return $r;
			}
		}
	}

	return null;
}

/* =============================================================================
 * SOCIAL SIGNALS
 * ============================================================================= */

function myaio_extract_social_signals( DOMXPath $xpath, $jsonld_blocks, DOMNode $footer_context ) {
	$platforms = [
		'facebook'  => 'facebook.com',
		'linkedin'  => 'linkedin.com',
		'instagram' => 'instagram.com',
		'twitter'   => 'twitter.com',
		'x'         => 'x.com',
		'youtube'   => 'youtube.com',
		'tiktok'    => 'tiktok.com',
		'pinterest' => 'pinterest.com',
	];

	$links  = [];
	$sameas = myaio_collect_sameas_from_jsonld( $jsonld_blocks );

	foreach ( $sameas as $u ) {
		$links[] = $u;
	}

	$a_nodes = $xpath->query( ".//a[@href]", $footer_context );
	if ( $a_nodes && $a_nodes->length ) {
		foreach ( $a_nodes as $a ) {
			$href = trim( (string) $a->getAttribute( 'href' ) );
			if ( '' === $href ) {
				continue;
			}
			foreach ( $platforms as $p => $dom_match ) {
				if ( false !== stripos( $href, $dom_match ) ) {
					$links[] = $href;
				}
			}
		}
	}

	if ( empty( $links ) ) {
		$all = $xpath->query( "//a[@href]" );
		if ( $all && $all->length ) {
			foreach ( $all as $a ) {
				$href = trim( (string) $a->getAttribute( 'href' ) );
				if ( '' === $href ) {
					continue;
				}
				foreach ( $platforms as $p => $dom_match ) {
					if ( false !== stripos( $href, $dom_match ) ) {
						$links[] = $href;
					}
				}
			}
		}
	}

	$links = array_values( array_unique( array_filter( $links ) ) );

	$by_platform = [];
	foreach ( $links as $u ) {
		foreach ( $platforms as $p => $dom_match ) {
			if ( false !== stripos( $u, $dom_match ) ) {
				$by_platform[ $p ][] = $u;
			}
		}
	}
	foreach ( $by_platform as $p => $arr ) {
		$by_platform[ $p ] = array_values( array_unique( $arr ) );
	}

	return [
		'present'     => ! empty( $links ),
		'links'       => $links,
		'by_platform' => $by_platform,
		'sources'     => [
			'jsonld_sameAs' => ! empty( $sameas ),
			'footer_links'  => true,
		],
	];
}

function myaio_collect_sameas_from_jsonld( $json ) {
	$out = [];

	if ( ! is_array( $json ) ) {
		return $out;
	}

	if ( array_keys( $json ) === range( 0, count( $json ) - 1 ) ) {
		foreach ( $json as $item ) {
			if ( is_array( $item ) ) {
				$out = array_merge( $out, myaio_collect_sameas_from_jsonld( $item ) );
			}
		}
		return array_values( array_unique( array_filter( $out ) ) );
	}

	if ( isset( $json['@graph'] ) && is_array( $json['@graph'] ) ) {
		return myaio_collect_sameas_from_jsonld( $json['@graph'] );
	}

	if ( isset( $json['sameAs'] ) ) {
		if ( is_array( $json['sameAs'] ) ) {
			$out = array_merge( $out, $json['sameAs'] );
		} else {
			$out[] = (string) $json['sameAs'];
		}
	}

	foreach ( $json as $v ) {
		if ( is_array( $v ) ) {
			$out = array_merge( $out, myaio_collect_sameas_from_jsonld( $v ) );
		}
	}

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

/* =============================================================================
 * Header/Footer detection (generic)
 * ============================================================================= */

function myaio_detect_header_node( DOMXPath $xpath ) {
	$candidates = [
		'//header',
		"//*[@role='banner']",
		"//*[contains(@id,'masthead')]",
		"//*[contains(concat(' ', normalize-space(@class), ' '), ' site-header ')]",
		"//*[contains(concat(' ', normalize-space(@class), ' '), ' header ')]",
		"//*[contains(concat(' ', normalize-space(@class), ' '), ' elementor-location-header ')]",
		"//*[contains(@id,'header')]",
		"//*[contains(@class,'header')]",
	];

	foreach ( $candidates as $q ) {
		$n = myaio_first_node( $xpath, $q );
		if ( $n ) {
			return $n;
		}
	}

	return null;
}

function myaio_detect_footer_node( DOMXPath $xpath ) {
	$candidates = [
		'//footer',
		"//*[@role='contentinfo']",
		"//*[@id='colophon']",
		"//*[contains(concat(' ', normalize-space(@class), ' '), ' site-footer ')]",
		"//*[contains(concat(' ', normalize-space(@class), ' '), ' footer ')]",
		"//*[contains(concat(' ', normalize-space(@class), ' '), ' elementor-location-footer ')]",
		"//*[contains(@id,'footer')]",
		"//*[contains(@class,'footer')]",
	];

	foreach ( $candidates as $q ) {
		$n = myaio_first_node( $xpath, $q );
		if ( $n ) {
			return $n;
		}
	}

	return null;
}

/* =============================================================================
 * Basic extraction utils
 * ============================================================================= */

function myaio_dom_from_html( $html ) {
	if ( ! class_exists( 'DOMDocument' ) ) {
		return null;
	}

	libxml_use_internal_errors( true );
	$dom = new DOMDocument();
	$ok  = @$dom->loadHTML( '<?xml encoding="utf-8" ?>' . (string) $html );
	libxml_clear_errors();

	return $ok ? $dom : null;
}

function myaio_remove_noise_nodes( DOMXPath $xpath, DOMNode $context ) {
	// Remove style + noscript.
	foreach ( [ './/style', './/noscript' ] as $q ) {
		$nodes = $xpath->query( $q, $context );
		if ( ! $nodes ) {
			continue;
		}
		for ( $i = $nodes->length - 1; $i >= 0; $i-- ) {
			$n = $nodes->item( $i );
			if ( $n && $n->parentNode ) {
				$n->parentNode->removeChild( $n );
			}
		}
	}

	// Remove scripts EXCEPT application/ld+json.
	$scripts = $xpath->query( './/script', $context );
	if ( $scripts ) {
		for ( $i = $scripts->length - 1; $i >= 0; $i-- ) {
			$s = $scripts->item( $i );
			if ( ! $s || ! $s->parentNode ) {
				continue;
			}

			$type = strtolower( trim( (string) $s->getAttribute( 'type' ) ) );
			if ( 'application/ld+json' === $type ) {
				continue;
			}

			$s->parentNode->removeChild( $s );
		}
	}
}

function myaio_first_node( DOMXPath $xpath, $query, DOMNode $context = null ) {
	$nodes = $context ? $xpath->query( $query, $context ) : $xpath->query( $query );
	return ( $nodes && $nodes->length ) ? $nodes->item( 0 ) : null;
}

function myaio_first_attr( DOMXPath $xpath, $query, DOMNode $context = null ) {
	$nodes = $context ? $xpath->query( $query, $context ) : $xpath->query( $query );
	return ( $nodes && $nodes->length ) ? trim( (string) $nodes->item( 0 )->nodeValue ) : null;
}

function myaio_first_text( DOMXPath $xpath, $query, DOMNode $context = null ) {
	$nodes = $context ? $xpath->query( $query, $context ) : $xpath->query( $query );
	if ( $nodes && $nodes->length ) {
		$t = myaio_clean_text( $nodes->item( 0 )->textContent );
		return $t ? $t : null;
	}
	return null;
}

function myaio_clean_text( $text ) {
	$t = trim( preg_replace( '/\s+/', ' ', (string) $text ) );
	return $t ? $t : null;
}

function myaio_outer_html( DOMNode $node ) {
	$doc = $node->ownerDocument;
	return $doc ? $doc->saveHTML( $node ) : null;
}

function myaio_abs_url( $u ) {
	$u = trim( (string) $u );
	if ( '' === $u ) {
		return null;
	}
	if ( preg_match( '#^https?://#i', $u ) ) {
		return $u;
	}
	if ( 0 === strpos( $u, '//' ) ) {
		return ( is_ssl() ? 'https:' : 'http:' ) . $u;
	}
	return home_url( '/' ) . ltrim( $u, '/' );
}

function myaio_pick_address_line( $text ) {
	$text = myaio_clean_text( $text );
	if ( ! $text ) {
		return null;
	}

	$cands = preg_split( '/\s*(?:\||\•|·|—|–|\n)\s*/', $text );
	if ( ! $cands ) {
		$cands = [ $text ];
	}

	$best      = null;
	$bestScore = 0;

	foreach ( $cands as $cand ) {
		$cand = trim( $cand );
		if ( '' === $cand || strlen( $cand ) < 10 ) {
			continue;
		}

		$score      = 0;
		$digitCount = preg_match_all( '/\d/', $cand );
		if ( $digitCount >= 2 ) {
			$score += 3;
		}

		if ( preg_match( '/\b(street|st\.|road|rd\.|ave|avenue|blvd|boulevard|lane|ln\.|drive|dr\.|suite|ste\.|unit|floor|fl\.|building|bldg)\b/i', $cand ) ) {
			$score += 4;
		}

		if ( preg_match( '/,\s*[A-Z]{2}\s*\d{5}(-\d{4})?\b/', $cand ) ) {
			$score += 5;
		}
		if ( preg_match( '/\b\d{4,6}\b/', $cand ) ) {
			$score += 2;
		}

		if ( preg_match( '/\b(copyright|©|all rights reserved)\b/i', $cand ) ) {
			$score -= 5;
		}
		if ( strlen( $cand ) > 180 ) {
			$score -= 3;
		}

		if ( $score > $bestScore ) {
			$bestScore = $score;
			$best      = $cand;
		}
	}

	return $best ? $best : null;
}

function myaio_extract_links( DOMXPath $xpath, DOMNode $context ) {
	$out   = [];
	$nodes = $xpath->query( ".//a[@href]", $context );
	if ( ! $nodes ) {
		return $out;
	}

	foreach ( $nodes as $a ) {
		$href = trim( (string) $a->getAttribute( 'href' ) );
		if ( '' === $href || 0 === stripos( $href, 'javascript:' ) ) {
			continue;
		}

		$out[] = [
			'href'  => myaio_abs_url( $href ),
			'text'  => myaio_clean_text( $a->textContent ) ? myaio_clean_text( $a->textContent ) : '',
			'title' => (string) $a->getAttribute( 'title' ),
		];
	}

	$seen    = [];
	$deduped = [];
	foreach ( $out as $l ) {
		$k = ( $l['href'] ?? '' ) . '|' . ( $l['text'] ?? '' ) . '|' . ( $l['title'] ?? '' );
		if ( isset( $seen[ $k ] ) ) {
			continue;
		}
		$seen[ $k ] = true;
		$deduped[]  = $l;
	}

	return $deduped;
}
