<?php
/**
 * Extract hyperlinks from post content and common builder meta.
 *
 * @package BrokenLinkChecker
 * @since 1.1.0
 */

namespace BrokenLinkChecker;

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

final class LinkExtractor {

	private const CONTENT_LIMIT = 500000;

	/** @var list<string> */
	private const META_KEYS = [
		'_elementor_data',
		'_elementor_css',
	];

	/**
	 * @return list<string>
	 */
	public static function fromPost( \WP_Post $post ): array {
		$links = self::fromContent( (string) ( $post->post_content ?? '' ) );
		$links = array_merge( $links, self::fromPostMeta( (int) $post->ID ) );
		return array_values( array_unique( $links ) );
	}

	/**
	 * @return list<string>
	 */
	public static function fromContent( string $content ): array {
		if ( $content === '' ) {
			return [];
		}
		$content = substr( $content, 0, self::CONTENT_LIMIT );
		$links   = [];

		if ( preg_match_all( '/<a[^>]+href=["\']([^"\']+)["\'][^>]*>/i', $content, $matches ) ) {
			$links = array_merge( $links, $matches[1] );
		}
		if ( preg_match_all( '/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $content, $matches ) ) {
			$links = array_merge( $links, $matches[1] );
		}
		if ( preg_match_all( '/\ssrcset=["\']([^"\']+)["\']/i', $content, $matches ) ) {
			foreach ( $matches[1] as $srcset ) {
				foreach ( preg_split( '/\s*,\s*/', (string) $srcset ) ?: [] as $part ) {
					$part = trim( (string) preg_replace( '/\s+\d+[wx]$/i', '', trim( $part ) ) );
					if ( $part !== '' ) {
						$links[] = $part;
					}
				}
			}
		}
		if ( preg_match_all( '/"(?:url|link|href)":"((?:\\\\.|[^"\\\\])*)"/i', $content, $matches ) ) {
			foreach ( $matches[1] as $raw ) {
				$decoded = stripcslashes( (string) $raw );
				if ( $decoded !== '' ) {
					$links[] = $decoded;
				}
			}
		}

		return array_values( array_unique( $links ) );
	}

	/**
	 * @return list<string>
	 */
	public static function fromPostMeta( int $post_id ): array {
		if ( $post_id <= 0 ) {
			return [];
		}
		$links = [];
		foreach ( self::META_KEYS as $meta_key ) {
			$value = get_post_meta( $post_id, $meta_key, true );
			if ( ! is_string( $value ) || $value === '' ) {
				continue;
			}
			$links = array_merge( $links, self::fromContent( $value ) );
			if ( preg_match_all( '#https?://[^\s"\'<>]+#i', $value, $matches ) ) {
				$links = array_merge( $links, $matches[0] );
			}
		}
		return array_values( array_unique( $links ) );
	}

	public static function sanitizeCandidate( string $url ): string|false {
		$url = trim( $url );
		if ( $url === '' || $url === '#' || str_starts_with( $url, '#' ) ) {
			return false;
		}
		foreach ( [ 'javascript:', 'mailto:', 'tel:', 'data:', 'blob:', 'vbscript:' ] as $scheme ) {
			if ( str_starts_with( strtolower( $url ), $scheme ) ) {
				return false;
			}
		}
		if ( str_contains( $url, '{{' ) || preg_match( '/\{[a-z_]+\}/i', $url ) ) {
			return false;
		}
		$url = html_entity_decode( $url, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$url = rawurldecode( $url );
		if ( str_starts_with( $url, './' ) || ( ! str_contains( $url, '://' ) && ! str_starts_with( $url, '/' ) && ! str_starts_with( $url, '//' ) ) ) {
			$url = home_url( '/' . ltrim( $url, '/' ) );
		}
		if ( str_starts_with( $url, '//' ) ) {
			$url = ( is_ssl() ? 'https:' : 'http:' ) . $url;
		}

		return self::canonicalUrl( $url );
	}

	/**
	 * Normalize a URL for deduplication and cache keys.
	 */
	public static function canonicalUrl( string $url ): string|false {
		$url = trim( $url );
		if ( $url === '' ) {
			return false;
		}

		$normalized = esc_url_raw( $url );
		if ( $normalized === '' ) {
			return false;
		}

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

		$scheme = strtolower( (string) ( $parsed['scheme'] ?? 'http' ) );
		$host   = strtolower( (string) $parsed['host'] );
		if ( str_starts_with( $host, 'www.' ) ) {
			$host = substr( $host, 4 );
		}

		$path = (string) ( $parsed['path'] ?? '' );
		if ( $path !== '/' && str_ends_with( $path, '/' ) ) {
			$path = rtrim( $path, '/' );
		}

		$canonical = $scheme . '://' . $host . $path;
		if ( ! empty( $parsed['query'] ) ) {
			$canonical .= '?' . (string) $parsed['query'];
		}
		if ( ! empty( $parsed['fragment'] ) ) {
			$canonical .= '#' . (string) $parsed['fragment'];
		}

		return $canonical;
	}

	/**
	 * @return list<string>
	 */
	public static function uniqueCanonicalFromPost( \WP_Post $post ): array {
		$links = [];
		$seen  = [];
		foreach ( self::fromPost( $post ) as $raw_url ) {
			$canonical = self::sanitizeCandidate( $raw_url );
			if ( ! $canonical || isset( $seen[ $canonical ] ) ) {
				continue;
			}
			$seen[ $canonical ]      = true;
			$links[]                 = $canonical;
		}
		return $links;
	}
}
