<?php

namespace OpenExternalLinksNewTab;

use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID      = 'open-external-links-new-tab';
	private const MODULE_VERSION = '1.0.3';

	/** @var string|null */
	private ?string $site_host = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Open All External Links in New Tab', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'External links in content open in a new tab with privacy- and SEO-friendly rel attributes.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'frontend';
	}

	protected function registerHooks(): void {
		if ( is_admin() ) {
			return;
		}

		$content_filters = [
			'the_content'       => 999,
			'the_excerpt'       => 999,
			'comment_text'      => 999,
			'widget_text'       => 999,
			'widget_text_content' => 999,
		];

		foreach ( $content_filters as $hook => $priority ) {
			add_filter( $hook, [ $this, 'filterLinks' ], $priority );
		}

		add_action( 'wp_enqueue_scripts', [ $this, 'enqueueAssets' ] );
	}

	public function enqueueAssets(): void {
		if ( is_admin() ) {
			return;
		}

		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		$file = 'open-external-links-new-tab.js';
		$path = __DIR__ . '/assets/' . $file;
		if ( $base === '' || ! is_readable( $path ) ) {
			return;
		}

		wp_enqueue_script(
			'wpps-open-external-links-new-tab',
			$base . 'modules/open-external-links-new-tab/assets/' . $file,
			[],
			$this->assetVersion( $file ),
			true
		);

		wp_localize_script(
			'wpps-open-external-links-new-tab',
			'wppsOpenExternalLinks',
			[
				'siteHost' => $this->getSiteHost(),
			]
		);
	}

	public function filterLinks( string $content ): string {
		if ( $content === '' || stripos( $content, '<a' ) === false ) {
			return $content;
		}

		$processed = $this->filterLinksWithDom( $content );

		return $processed ?? $this->filterLinksWithRegex( $content );
	}

	private function filterLinksWithDom( string $content ): ?string {
		if ( ! class_exists( \DOMDocument::class ) ) {
			return null;
		}

		$site_host = $this->getSiteHost();
		$wrapper   = '<div id="wpps-oelnt-root">' . $content . '</div>';

		$previous = libxml_use_internal_errors( true );
		$doc      = new \DOMDocument();
		$loaded   = $doc->loadHTML(
			'<?xml encoding="utf-8" ?>' . $wrapper,
			LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
		);
		libxml_clear_errors();
		libxml_use_internal_errors( $previous );

		if ( ! $loaded ) {
			return null;
		}

		$root = $doc->getElementById( 'wpps-oelnt-root' );
		if ( ! $root instanceof \DOMElement ) {
			return null;
		}

		/** @var \DOMNodeList<\DOMElement> $anchors */
		$anchors = $root->getElementsByTagName( 'a' );
		foreach ( $anchors as $anchor ) {
			$url = trim( $anchor->getAttribute( 'href' ) );
			if ( ! $this->isExternalUrl( $url, $site_host ) ) {
				continue;
			}

			$target = strtolower( trim( $anchor->getAttribute( 'target' ) ) );
			if ( ! in_array( $target, [ '_blank', '_parent', '_self', '_top' ], true ) ) {
				$anchor->setAttribute( 'target', '_blank' );
			}

			$anchor->setAttribute( 'rel', $this->mergeRelAttribute( $anchor->getAttribute( 'rel' ) ) );
		}

		$html = '';
		foreach ( $root->childNodes as $child ) {
			$html .= $doc->saveHTML( $child );
		}

		return $html;
	}

	private function filterLinksWithRegex( string $content ): string {
		$regexp = '<a\s[^>]*href\s*=\s*(["\'])([^"\']*)\1[^>]*>';

		if ( ! preg_match_all( "/$regexp/siU", $content, $matches, PREG_SET_ORDER ) ) {
			return $content;
		}

		$site_host = $this->getSiteHost();

		foreach ( $matches as $match ) {
			$original_tag = $match[0];
			$tag          = $match[0];
			$url          = $match[2];

			if ( ! $this->isExternalUrl( $url, $site_host ) ) {
				continue;
			}

			if ( preg_match( '/target\s*=\s*(["\'])\s*_(blank|parent|self|top)\s*\1/i', $tag ) !== 1 ) {
				$tag = substr_replace( $tag, ' target="_blank">', -1 );
			}

			$tag = $this->ensureSafeRelTag( $tag );

			if ( $tag !== $original_tag ) {
				$content = str_replace( $original_tag, $tag, $content );
			}
		}

		return $content;
	}

	private function getSiteHost(): string {
		if ( $this->site_host !== null ) {
			return $this->site_host;
		}

		$host = wp_parse_url( home_url(), PHP_URL_HOST );
		$this->site_host = is_string( $host ) ? $host : '';

		return $this->site_host;
	}

	private function isExternalUrl( string $url, string $site_host ): bool {
		$url = trim( $url );
		if ( $url === '' || $url[0] === '#' ) {
			return false;
		}

		if ( $url[0] === '/' && ( ! isset( $url[1] ) || $url[1] !== '/' ) ) {
			return false;
		}

		$url_host = wp_parse_url( $url, PHP_URL_HOST );
		if ( ! is_string( $url_host ) || $url_host === '' ) {
			return false;
		}

		$scheme = wp_parse_url( $url, PHP_URL_SCHEME );
		if ( is_string( $scheme ) && $scheme !== '' && ! in_array( strtolower( $scheme ), [ 'http', 'https' ], true ) ) {
			return false;
		}

		$url_norm  = preg_replace( '/^www\./i', '', strtolower( $url_host ) );
		$site_norm = preg_replace( '/^www\./i', '', strtolower( $site_host ) );

		return (string) $url_norm !== (string) $site_norm;
	}

	private function mergeRelAttribute( string $existing ): string {
		$required = [ 'noopener', 'noreferrer', 'nofollow' ];
		$tokens   = array_values(
			array_filter(
				array_map(
					'strtolower',
					preg_split( '/\s+/', trim( $existing ) ) ?: []
				)
			)
		);

		foreach ( $required as $token ) {
			if ( ! in_array( $token, $tokens, true ) ) {
				$tokens[] = $token;
			}
		}

		return implode( ' ', $tokens );
	}

	private function ensureSafeRelTag( string $tag ): string {
		if ( preg_match( '/rel\s*=\s*("([^"]*)"|\'([^\']*)\')/i', $tag, $match ) ) {
			$existing = ( isset( $match[2] ) && $match[2] !== '' ) ? $match[2] : ( $match[3] ?? '' );
			$new_rel  = 'rel="' . esc_attr( $this->mergeRelAttribute( $existing ) ) . '"';

			return str_replace( $match[0], $new_rel, $tag );
		}

		return substr_replace( $tag, ' rel="noopener noreferrer nofollow">', -1 );
	}

	private function assetVersion( string $filename ): string {
		$path = __DIR__ . '/assets/' . $filename;

		return is_readable( $path ) ? (string) filemtime( $path ) : self::MODULE_VERSION;
	}
}
