<?php
/**
 * HtmlLinkStripper — DOMDocument-based <a> tag stripper.
 *
 * Replaces matching `<a href="...">...</a>` elements with their inner contents
 * (preserving any inline formatting). No regex on HTML — uses libxml.
 *
 * The class implements URL variant matching (trailing slash, http/https,
 * with/without query string) so it catches every form of a given canonical URL.
 *
 * @package Platform\Helm\Support
 */

declare( strict_types = 1 );

namespace Platform\Helm\Support;

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

final class HtmlLinkStripper {

	/**
	 * Strip <a> tags whose href matches any of $urls (or any variant).
	 *
	 * @param string   $html  Source HTML fragment.
	 * @param string[] $urls  Canonical URLs to match.
	 * @param bool     $dry_run If true, count matches without modifying.
	 * @return array{0:string,1:int,2:string[]} [new_html, removed_count, matched_urls]
	 */
	public function strip( string $html, array $urls, bool $dry_run ): array {
		if ( '' === $html || false === stripos( $html, '<a' ) ) {
			return [ $html, 0, [] ];
		}

		$variant_map = $this->build_variant_lookup( $urls );
		if ( empty( $variant_map ) ) {
			return [ $html, 0, [] ];
		}

		$dom = new \DOMDocument( '1.0', 'UTF-8' );

		// Suppress HTML5 warnings via libxml internal errors.
		$prev_state = libxml_use_internal_errors( true );

		// Wrap the fragment so DOMDocument can parse it without inferring
		// <html><body>. Charset meta forces UTF-8 interpretation.
		$wrapped = '<?xml encoding="UTF-8"?><div id="__platform_helm_wrapper__">' . $html . '</div>';
		$loaded  = $dom->loadHTML( $wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
		libxml_clear_errors();
		libxml_use_internal_errors( $prev_state );

		if ( ! $loaded ) {
			return [ $html, 0, [] ];
		}

		$anchors = $dom->getElementsByTagName( 'a' );
		// Materialize into an array — replaceChild() mutates the live NodeList.
		$nodes = [];
		foreach ( $anchors as $node ) {
			$nodes[] = $node;
		}

		$removed       = 0;
		$matched_urls  = [];

		foreach ( $nodes as $a ) {
			if ( ! $a instanceof \DOMElement ) {
				continue;
			}
			$href = $a->getAttribute( 'href' );
			if ( '' === $href ) {
				continue;
			}
			$normalized = $this->normalize_for_match( $href );
			if ( ! isset( $variant_map[ $normalized ] ) ) {
				continue;
			}
			if ( $dry_run ) {
				$removed++;
				$matched_urls[] = $variant_map[ $normalized ];
				continue;
			}
			$this->unwrap_anchor( $a );
			$removed++;
			$matched_urls[] = $variant_map[ $normalized ];
		}

		if ( 0 === $removed ) {
			return [ $html, 0, [] ];
		}

		if ( $dry_run ) {
			return [ $html, $removed, array_values( array_unique( $matched_urls ) ) ];
		}

		$out = $this->extract_wrapper_inner_html( $dom );
		return [ $out, $removed, array_values( array_unique( $matched_urls ) ) ];
	}

	/**
	 * Replace an <a> node with its child nodes in-place.
	 */
	private function unwrap_anchor( \DOMElement $a ): void {
		$parent = $a->parentNode;
		if ( null === $parent ) {
			return;
		}
		// Move all child nodes from $a into a fragment, then replace $a with it.
		$fragment = $a->ownerDocument->createDocumentFragment();
		while ( $a->firstChild ) {
			$fragment->appendChild( $a->firstChild );
		}
		$parent->replaceChild( $fragment, $a );
	}

	/**
	 * Serialize the wrapper div's innerHTML back to a string.
	 */
	private function extract_wrapper_inner_html( \DOMDocument $dom ): string {
		$wrapper = $dom->getElementById( '__platform_helm_wrapper__' );
		if ( ! $wrapper instanceof \DOMElement ) {
			return $dom->saveHTML();
		}
		$out = '';
		foreach ( $wrapper->childNodes as $child ) {
			$out .= $dom->saveHTML( $child );
		}
		return $out;
	}

	/**
	 * Builds a lookup of `normalized_variant => original_url` for fast O(1)
	 * matching of every href we encounter.
	 *
	 * @param string[] $urls
	 * @return array<string,string>
	 */
	private function build_variant_lookup( array $urls ): array {
		$map = [];
		foreach ( $urls as $url ) {
			foreach ( $this->url_variants( $url ) as $variant ) {
				$normalized = $this->normalize_for_match( $variant );
				if ( '' !== $normalized ) {
					$map[ $normalized ] = $url;
				}
			}
		}
		return $map;
	}

	/**
	 * Generate canonical variants of a URL (trailing slash, protocol swap,
	 * with/without query string).
	 *
	 * @return string[]
	 */
	private function url_variants( string $url ): array {
		$variants = [ $url ];

		$parsed = wp_parse_url( $url );
		if ( is_array( $parsed ) && isset( $parsed['scheme'], $parsed['host'] ) ) {
			$bare = $parsed['scheme'] . '://' . $parsed['host'];
			if ( ! empty( $parsed['path'] ) ) {
				$bare .= $parsed['path'];
			}
			$variants[] = $bare;
		}
		foreach ( $variants as $v ) {
			$variants[] = '/' === substr( $v, -1 ) ? rtrim( $v, '/' ) : $v . '/';
		}
		foreach ( $variants as $v ) {
			if ( 0 === strpos( $v, 'https://' ) ) {
				$variants[] = 'http://' . substr( $v, 8 );
			} elseif ( 0 === strpos( $v, 'http://' ) ) {
				$variants[] = 'https://' . substr( $v, 7 );
			}
		}
		return array_values( array_unique( array_filter( $variants ) ) );
	}

	/**
	 * Normalize a URL for matching: lowercase scheme + host, strip fragment.
	 * Keeps path/query as-is so they're distinguishable.
	 */
	private function normalize_for_match( string $url ): string {
		$url    = trim( $url );
		if ( '' === $url ) {
			return '';
		}
		$parsed = wp_parse_url( $url );
		if ( ! is_array( $parsed ) ) {
			return $url;
		}
		$scheme = isset( $parsed['scheme'] ) ? strtolower( $parsed['scheme'] ) : '';
		$host   = isset( $parsed['host'] )   ? strtolower( $parsed['host'] )   : '';
		$path   = $parsed['path']  ?? '';
		$query  = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';

		if ( '' !== $scheme && '' !== $host ) {
			return $scheme . '://' . $host . $path . $query;
		}
		return $url;
	}
}
