<?php
/**
 * Frontend email obfuscation for HTML content strings.
 *
 * @package WP_PowerSuite\Modules\EmailObfuscator
 */

namespace EmailObfuscator;

class ContentObfuscator {

	private const SKIP_TAG_PATTERN = '/<(script|style|code|pre|textarea|select|form|noscript|svg|math|iframe)\b[^>]*>.*?<\/\1>/is';

	private const VOID_TAG_PATTERN = '/<(input)\b[^>]*\/?>/is';

	private const HTML_TAG_PATTERN = '/<[^>]+>/';

	private const EMAIL_PATTERN = '/(?<![\w\-.])([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/';

	private const MAILTO_LINK_PATTERN = '/<a\b([^>]*?)\bhref\s*=\s*(["\'])(mailto:[^"\']*)\2([^>]*)>(.*?)<\/a>/is';

	/** @var list<string> */
	private const ALLOWED_ANCHOR_ATTRS = [
		'class',
		'id',
		'rel',
		'target',
		'title',
		'lang',
		'dir',
		'role',
		'aria-label',
		'aria-labelledby',
		'aria-describedby',
	];

	private static bool $processing = false;

	/** @var array<string, string> */
	private array $protected_blocks = [];

	private int $placeholder_index = 0;

	public function obfuscate( string $content, string $context = 'content' ): string {
		if ( self::$processing ) {
			return $content;
		}

		if ( $content === '' || ! str_contains( $content, '@' ) ) {
			return $content;
		}

		self::$processing = true;

		try {
			$this->protected_blocks   = [];
			$this->placeholder_index = 0;

			$working = $this->protectSkipRegions( $content );
			$working = $this->obfuscateMailtoLinks( $working, $context );
			$working = $this->protectAnchorTags( $working );
			$working = $this->protectHtmlTags( $working );
			$working = $this->obfuscatePlainEmails( $working, $context );

			return $this->restoreProtectedBlocks( $working );
		} finally {
			self::$processing = false;
		}
	}

	/**
	 * Obfuscate a mailto href value (for nav menus and other attribute-only contexts).
	 */
	public function obfuscateMailtoHrefValue( string $href, string $context = 'nav_menu' ): string {
		if ( $href === '' || ! str_contains( strtolower( $href ), 'mailto:' ) ) {
			return $href;
		}

		if ( str_contains( $href, 'data-wpps-email-obfuscated' ) || str_contains( $href, '&#' ) ) {
			return $href;
		}

		$decoded = html_entity_decode( $href, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$parsed  = $this->parseMailtoHref( $decoded );
		if ( $parsed === null || ! $this->shouldObfuscate( $parsed['email'], $context ) ) {
			return $href;
		}

		return $this->buildObfuscatedMailtoHref( $parsed['email'], $parsed['query'] );
	}

	private function protectSkipRegions( string $content ): string {
		$content = (string) preg_replace_callback(
			self::SKIP_TAG_PATTERN,
			function ( array $match ): string {
				return $this->storeProtectedBlock( $match[0] );
			},
			$content
		);

		return (string) preg_replace_callback(
			self::VOID_TAG_PATTERN,
			function ( array $match ): string {
				return $this->storeProtectedBlock( $match[0] );
			},
			$content
		);
	}

	private function protectAnchorTags( string $content ): string {
		return (string) preg_replace_callback(
			'/<a\b[^>]*>.*?<\/a>/is',
			function ( array $match ): string {
				return $this->storeProtectedBlock( $match[0] );
			},
			$content
		);
	}

	private function protectHtmlTags( string $content ): string {
		return (string) preg_replace_callback(
			self::HTML_TAG_PATTERN,
			function ( array $match ): string {
				return $this->storeProtectedBlock( $match[0] );
			},
			$content
		);
	}

	private function obfuscateMailtoLinks( string $content, string $context ): string {
		return (string) preg_replace_callback(
			self::MAILTO_LINK_PATTERN,
			function ( array $match ) use ( $context ): string {
				return $this->replaceMailtoLink( $match, $context );
			},
			$content
		);
	}

	private function obfuscatePlainEmails( string $content, string $context ): string {
		return (string) preg_replace_callback(
			self::EMAIL_PATTERN,
			function ( array $match ) use ( $context ): string {
				$email = $this->normalizeDetectedEmail( $match[0] );
				if ( $email === null || ! $this->shouldObfuscate( $email, $context ) ) {
					return $match[0];
				}

				$href  = $this->buildObfuscatedMailtoHref( $email, '' );
				$label = $this->obfuscateEmailEntities( $email );

				return sprintf(
					'<a href="%s" data-wpps-email-obfuscated="1">%s</a>',
					$href,
					$label
				);
			},
			$content
		);
	}

	/**
	 * @param array<int, string> $match
	 */
	private function replaceMailtoLink( array $match, string $context ): string {
		$before_attrs = $match[1];
		$href         = html_entity_decode( $match[3], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$after_attrs  = $match[4];
		$link_text    = $match[5];

		if ( str_contains( $before_attrs . $after_attrs, 'data-wpps-email-obfuscated' ) ) {
			return $match[0];
		}

		$parsed = $this->parseMailtoHref( $href );
		if ( $parsed === null || ! $this->shouldObfuscate( $parsed['email'], $context ) ) {
			return $match[0];
		}

		$safe_href = $this->buildObfuscatedMailtoHref( $parsed['email'], $parsed['query'] );
		$new_text  = $this->obfuscateMailtoLinkText( $link_text, $parsed['email'] );
		$attrs     = $this->sanitizeAnchorAttributes( $before_attrs, $after_attrs );

		return sprintf(
			'<a%s href="%s" data-wpps-email-obfuscated="1">%s</a>',
			$attrs,
			$safe_href,
			$new_text
		);
	}

	private function obfuscateMailtoLinkText( string $link_text, string $email ): string {
		$decoded_text = html_entity_decode( wp_strip_all_tags( $link_text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$decoded_text = trim( $decoded_text );

		if ( $decoded_text === '' || ! str_contains( $decoded_text, $email ) ) {
			return $link_text;
		}

		if ( $decoded_text === $email ) {
			return $this->obfuscateEmailEntities( $email );
		}

		$obfuscated = $this->obfuscateEmailEntities( $email );
		$replaced   = preg_replace(
			'/' . preg_quote( $email, '/' ) . '/',
			$obfuscated,
			$link_text,
			1
		);

		return is_string( $replaced ) ? $replaced : $link_text;
	}

	private function sanitizeAnchorAttributes( string $before_attrs, string $after_attrs ): string {
		$combined = trim( $before_attrs . ' ' . $after_attrs );
		if ( $combined === '' || ! function_exists( 'wp_kses_hair' ) ) {
			return '';
		}

		$parsed = wp_kses_hair( '<a ' . $combined . '>', wp_allowed_protocols() );
		if ( ! is_array( $parsed ) || $parsed === [] ) {
			return '';
		}

		$parts = [];
		foreach ( $parsed as $attr ) {
			if ( ! is_array( $attr ) || empty( $attr['name'] ) || ( $attr['name'] ?? '' ) === 'href' ) {
				continue;
			}

			$name = strtolower( (string) $attr['name'] );
			if ( ! $this->isAllowedAnchorAttribute( $name ) ) {
				continue;
			}

			$value = html_entity_decode( (string) ( $attr['value'] ?? '' ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
			$parts[] = sprintf( '%s="%s"', $attr['name'], esc_attr( $value ) );
		}

		return $parts === [] ? '' : ' ' . implode( ' ', $parts );
	}

	private function isAllowedAnchorAttribute( string $name ): bool {
		if ( in_array( $name, self::ALLOWED_ANCHOR_ATTRS, true ) ) {
			return true;
		}

		return str_starts_with( $name, 'aria-' );
	}

	/**
	 * @return array{email: string, query: string}|null
	 */
	private function parseMailtoHref( string $href ): ?array {
		$href = trim( $href );
		if ( ! preg_match( '/^mailto:/i', $href ) ) {
			return null;
		}

		$remainder = substr( $href, 7 );
		$query     = '';
		if ( str_contains( $remainder, '?' ) ) {
			[ $address_part, $query_part ] = explode( '?', $remainder, 2 );
			$query = $this->sanitizeMailtoQuery( $query_part );
		} else {
			$address_part = $remainder;
		}

		$address_part = rawurldecode( $address_part );
		$candidate    = explode( ',', $address_part )[0] ?? '';
		$candidate    = trim( $candidate );

		if ( ! is_email( $candidate ) ) {
			return null;
		}

		return [
			'email' => $candidate,
			'query' => $query,
		];
	}

	private function sanitizeMailtoQuery( string $query ): string {
		$query = ltrim( $query, '?' );
		if ( $query === '' ) {
			return '';
		}

		parse_str( $query, $params );
		if ( ! is_array( $params ) || $params === [] ) {
			return '';
		}

		$allowed = [ 'subject', 'body', 'cc', 'bcc' ];
		$safe    = [];

		foreach ( $allowed as $key ) {
			if ( ! array_key_exists( $key, $params ) ) {
				continue;
			}

			$value = $params[ $key ];
			if ( is_array( $value ) ) {
				continue;
			}

			$safe[ $key ] = sanitize_text_field( wp_unslash( (string) $value ) );
		}

		if ( $safe === [] ) {
			return '';
		}

		return '?' . http_build_query( $safe, '', '&', PHP_QUERY_RFC3986 );
	}

	private function buildObfuscatedMailtoHref( string $email, string $query ): string {
		return $this->obfuscateEmailEntities( 'mailto:' . $email . $query );
	}

	private function obfuscateEmailEntities( string $value ): string {
		if ( function_exists( 'antispambot' ) ) {
			return antispambot( $value );
		}

		$encoded = '';
		$length  = strlen( $value );
		for ( $i = 0; $i < $length; $i++ ) {
			$encoded .= '&#' . ord( $value[ $i ] ) . ';';
		}

		return $encoded;
	}

	private function normalizeDetectedEmail( string $raw ): ?string {
		if ( str_contains( $raw, '&#' ) || str_contains( $raw, 'data-wpps-email-obfuscated' ) ) {
			return null;
		}

		$email = trim( $raw, " \t\n\r\0\x0B<>\"'" );
		$email = rtrim( $email, '.,;:!?)' );

		if ( ! is_email( $email ) ) {
			return null;
		}

		return $email;
	}

	private function shouldObfuscate( string $email, string $context ): bool {
		return (bool) apply_filters(
			'wp_powersuite_email_obfuscator_should_obfuscate',
			true,
			$email,
			$context
		);
	}

	private function storeProtectedBlock( string $block ): string {
		$key = '%%WPPS_EMAIL_OBFUSCATOR_' . $this->placeholder_index . '%%';
		++$this->placeholder_index;

		$this->protected_blocks[ $key ] = $block;

		return $key;
	}

	private function restoreProtectedBlocks( string $content ): string {
		if ( $this->protected_blocks === [] ) {
			return $content;
		}

		return strtr( $content, $this->protected_blocks );
	}
}
