<?php
namespace TeknoTok\AiTranslator\Utils;

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

/**
 * Utility to parse HTML and translate text nodes block-by-block.
 */
class HtmlParser {
	/**
	 * Parse HTML, look up cache, translate remaining nodes, and reassemble.
	 *
	 * @param string $html         Raw HTML content.
	 * @param string $source_lang  Source language code.
	 * @param string $target_lang  Target language code.
	 * @param object $provider     Active AI provider instance.
	 * @param bool   $allow_remote Optional. Whether to allow remote translation requests. Default true.
	 * @param bool   $queue_misses Optional. Whether to queue uncached strings for background translation. Default false.
	 * @param bool   $queue_all_targets Optional. Whether misses should be queued for every configured target. Default true.
	 * @param bool   $shared_chrome_only Optional. Whether to only inspect header/nav/footer chrome. Default false.
	 * @return string Translated HTML.
	 * @throws \Exception
	 */
	public static function translate_html( $html, $source_lang, $target_lang, $provider, $allow_remote = true, $queue_misses = false, $queue_all_targets = true, $shared_chrome_only = false ) {
		if ( empty( $html ) ) {
			return $html;
		}

		// Setup DOM Document
		$dom = new \DOMDocument();
		libxml_use_internal_errors( true );
		// Load with UTF-8 encoding prefix
		$dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
		libxml_clear_errors();

		$xpath = new \DOMXPath( $dom );
		
		// 1. Match text nodes (excluding scripts, styles, code, pre, textareas, admin bar, and anything with class 'notranslate' or translate='no')
		$text_nodes = $xpath->query( '//text()[
			not(ancestor::script) and 
			not(ancestor::style) and 
			not(ancestor::code) and 
			not(ancestor::pre) and 
			not(ancestor::textarea) and 
			not(ancestor::*[@id="wpadminbar"]) and 
			not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " notranslate ")]) and 
			not(ancestor::*[@translate="no"])
		]' );

		// 2. Match image alt attributes (excluding anything with class 'notranslate' or translate='no')
		$img_nodes = $xpath->query( '//img[@alt][
			not(ancestor::*[@id="wpadminbar"]) and 
			not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " notranslate ")]) and 
			not(ancestor::*[@translate="no"])
		]' );

		// 3. Match elements with title attributes (excluding anything with class 'notranslate' or translate='no')
		$title_nodes = $xpath->query( '//*[@title][
			not(ancestor::*[@id="wpadminbar"]) and 
			not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " notranslate ")]) and 
			not(ancestor::*[@translate="no"])
		]' );

		// 4. Match elements with placeholder attributes (excluding anything with class 'notranslate' or translate='no')
		$placeholder_nodes = $xpath->query( '//*[@placeholder][
			not(ancestor::*[@id="wpadminbar"]) and 
			not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " notranslate ")]) and 
			not(ancestor::*[@translate="no"])
		]' );

		// 5. Match elements with aria-label attributes (excluding anything with class 'notranslate' or translate='no')
		$aria_label_nodes = $xpath->query( '//*[@aria-label][
			not(ancestor::*[@id="wpadminbar"]) and 
			not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " notranslate ")]) and 
			not(ancestor::*[@translate="no"])
		]' );

		// 6. Match input elements with type="submit", "button", "reset" and a value attribute
		$input_value_nodes = $xpath->query( '//input[@type="submit" or @type="button" or @type="reset"][@value][
			not(ancestor::*[@id="wpadminbar"]) and 
			not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " notranslate ")]) and 
			not(ancestor::*[@translate="no"])
		]' );

		$nodes_to_translate = array();
		$payload = array();
		$payload_counter = 0;

		$raw_texts_to_lookup = array();

		// 1. Gather text nodes content for batch lookup
		foreach ( $text_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->nodeValue );
			if ( self::should_translate( $text, $target_lang ) ) {
				$raw_texts_to_lookup[] = $text;
			}
		}

		// 2. Gather image alt attributes
		foreach ( $img_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'alt' ) );
			if ( self::should_translate( $text, $target_lang ) ) {
				$raw_texts_to_lookup[] = $text;
			}
		}

		// 3. Gather title attributes
		foreach ( $title_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'title' ) );
			if ( self::should_translate( $text, $target_lang ) ) {
				$raw_texts_to_lookup[] = $text;
			}
		}

		// 4. Gather placeholder attributes
		foreach ( $placeholder_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'placeholder' ) );
			if ( self::should_translate( $text, $target_lang ) ) {
				$raw_texts_to_lookup[] = $text;
			}
		}

		// 5. Gather aria-label attributes
		foreach ( $aria_label_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'aria-label' ) );
			if ( self::should_translate( $text, $target_lang ) ) {
				$raw_texts_to_lookup[] = $text;
			}
		}

		// 6. Gather input values
		foreach ( $input_value_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'value' ) );
			if ( self::should_translate( $text, $target_lang ) ) {
				$raw_texts_to_lookup[] = $text;
			}
		}

		// Batch fetch translations from Cache
		$cached_translations = array();
		if ( ! empty( $raw_texts_to_lookup ) ) {
			$cached_translations = \TeknoTok\AiTranslator\Database\MemoryRepository::get_translations_batch( $raw_texts_to_lookup, $target_lang );
		}

		// Process text nodes
		foreach ( $text_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->nodeValue );
			if ( ! self::should_translate( $text, $target_lang ) ) {
				continue;
			}

			$hash = md5( $text );
			$cached = isset( $cached_translations[ $hash ] ) ? $cached_translations[ $hash ] : false;
			if ( false !== $cached ) {
				$node->nodeValue = htmlspecialchars( $cached, ENT_NOQUOTES, 'UTF-8' );
			} else {
				$nodes_to_translate[ $payload_counter ] = array(
					'type' => 'text',
					'node' => $node
				);
				$payload[ $payload_counter ] = $text;
				$payload_counter++;
			}
		}

		// Process image alt attributes
		foreach ( $img_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'alt' ) );
			if ( ! self::should_translate( $text, $target_lang ) ) {
				continue;
			}

			$hash = md5( $text );
			$cached = isset( $cached_translations[ $hash ] ) ? $cached_translations[ $hash ] : false;
			if ( false !== $cached ) {
				$node->setAttribute( 'alt', $cached );
			} else {
				$nodes_to_translate[ $payload_counter ] = array(
					'type' => 'alt',
					'node' => $node
				);
				$payload[ $payload_counter ] = $text;
				$payload_counter++;
			}
		}

		// Process title attributes
		foreach ( $title_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'title' ) );
			if ( ! self::should_translate( $text, $target_lang ) ) {
				continue;
			}

			$hash = md5( $text );
			$cached = isset( $cached_translations[ $hash ] ) ? $cached_translations[ $hash ] : false;
			if ( false !== $cached ) {
				$node->setAttribute( 'title', $cached );
			} else {
				$nodes_to_translate[ $payload_counter ] = array(
					'type' => 'title',
					'node' => $node
				);
				$payload[ $payload_counter ] = $text;
				$payload_counter++;
			}
		}

		// Process placeholder attributes
		foreach ( $placeholder_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'placeholder' ) );
			if ( ! self::should_translate( $text, $target_lang ) ) {
				continue;
			}

			$hash = md5( $text );
			$cached = isset( $cached_translations[ $hash ] ) ? $cached_translations[ $hash ] : false;
			if ( false !== $cached ) {
				$node->setAttribute( 'placeholder', $cached );
			} else {
				$nodes_to_translate[ $payload_counter ] = array(
					'type' => 'placeholder',
					'node' => $node
				);
				$payload[ $payload_counter ] = $text;
				$payload_counter++;
			}
		}

		// Process aria-label attributes
		foreach ( $aria_label_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'aria-label' ) );
			if ( ! self::should_translate( $text, $target_lang ) ) {
				continue;
			}

			$hash = md5( $text );
			$cached = isset( $cached_translations[ $hash ] ) ? $cached_translations[ $hash ] : false;
			if ( false !== $cached ) {
				$node->setAttribute( 'aria-label', $cached );
			} else {
				$nodes_to_translate[ $payload_counter ] = array(
					'type' => 'aria-label',
					'node' => $node
				);
				$payload[ $payload_counter ] = $text;
				$payload_counter++;
			}
		}

		// Process input value attributes
		foreach ( $input_value_nodes as $node ) {
			if ( $shared_chrome_only && ! self::is_shared_chrome_node( $node ) ) {
				continue;
			}
			$text = trim( $node->getAttribute( 'value' ) );
			if ( ! self::should_translate( $text, $target_lang ) ) {
				continue;
			}

			$hash = md5( $text );
			$cached = isset( $cached_translations[ $hash ] ) ? $cached_translations[ $hash ] : false;
			if ( false !== $cached ) {
				$node->setAttribute( 'value', $cached );
			} else {
				$nodes_to_translate[ $payload_counter ] = array(
					'type' => 'value',
					'node' => $node
				);
				$payload[ $payload_counter ] = $text;
				$payload_counter++;
			}
		}

		if ( ! empty( $raw_texts_to_lookup ) && ! $allow_remote && $queue_misses ) {
			$queue_target_languages = $queue_all_targets ? self::get_queue_target_languages( $target_lang ) : array( sanitize_key( $target_lang ) );
			$unique_source_texts    = array_values( array_unique( $raw_texts_to_lookup ) );
			foreach ( $queue_target_languages as $queue_target_lang ) {
				$queued_cached_translations = \TeknoTok\AiTranslator\Database\MemoryRepository::get_translations_batch( $unique_source_texts, $queue_target_lang );
				foreach ( $unique_source_texts as $source_text ) {
					if ( isset( $queued_cached_translations[ md5( $source_text ) ] ) ) {
						continue;
					}
					\TeknoTok\AiTranslator\Database\JobRepository::add_string_job( $source_text, $queue_target_lang );
				}
			}
		}

		// Translate payload in batches
		if ( ! empty( $payload ) && $allow_remote ) {
			$chunks = array_chunk( $payload, 25, true );
			foreach ( $chunks as $chunk ) {
				try {
					$translations = $provider->translate( $chunk, $source_lang, $target_lang );
					foreach ( $chunk as $index => $original_text ) {
						if ( isset( $translations[ $index ] ) ) {
							$translated_text = $translations[ $index ];
							\TeknoTok\AiTranslator\Database\MemoryRepository::save_translation( $original_text, $translated_text, $source_lang, $target_lang );
							
							$type = $nodes_to_translate[ $index ]['type'];
							$node = $nodes_to_translate[ $index ]['node'];
							
							if ( 'text' === $type ) {
								$node->nodeValue = htmlspecialchars( $translated_text, ENT_NOQUOTES, 'UTF-8' );
							} elseif ( 'alt' === $type ) {
								$node->setAttribute( 'alt', $translated_text );
							} elseif ( 'title' === $type ) {
								$node->setAttribute( 'title', $translated_text );
							} elseif ( 'placeholder' === $type ) {
								$node->setAttribute( 'placeholder', $translated_text );
							} elseif ( 'aria-label' === $type ) {
								$node->setAttribute( 'aria-label', $translated_text );
							} elseif ( 'value' === $type ) {
								$node->setAttribute( 'value', $translated_text );
							}
						}
					}
				} catch ( \Exception $e ) {
					Logger::log( 'HTML parser translation batch failed: ' . $e->getMessage(), 'error' );
					throw $e;
				}
			}
		}

		$output = $dom->saveHTML();
		$output = str_replace( '<?xml encoding="utf-8" ?>', '', $output );

		// Decode HTML entities (e.g. &middot; or Arabic decimal entities) generated by DOMDocument::saveHTML()
		// but preserve HTML special characters (<, >, &, ", ') to avoid breaking markup structure.
		$output = preg_replace_callback(
			'/&(#(?:\d+|x[a-fA-F0-9]+)|[a-zA-Z0-9]+);/',
			function ( $matches ) {
				$entity = $matches[0];
				$char   = html_entity_decode( $entity, ENT_QUOTES, 'UTF-8' );
				if ( in_array( $char, array( '<', '>', '&', '"', "'" ), true ) ) {
					return $entity;
				}
				return $char;
			},
			$output
		);

		return trim( $output );
	}

	/**
	 * Determine whether a DOM node belongs to shared site chrome.
	 *
	 * @param \DOMNode $node Node to inspect.
	 * @return bool True when the node is inside header, navigation, or footer chrome.
	 */
	private static function is_shared_chrome_node( $node ) {
		$current = ( $node instanceof \DOMText ) ? $node->parentNode : $node;
		$markers = array( 'header', 'nav', 'menu', 'navbar', 'navigation', 'topbar', 'footer', 'site-header', 'site-footer' );

		while ( $current instanceof \DOMElement ) {
			$tag = strtolower( $current->tagName );
			if ( in_array( $tag, array( 'header', 'nav', 'footer' ), true ) ) {
				return true;
			}

			$role = strtolower( $current->getAttribute( 'role' ) );
			if ( in_array( $role, array( 'banner', 'navigation', 'contentinfo' ), true ) ) {
				return true;
			}

			$identity = strtolower( trim( $current->getAttribute( 'id' ) . ' ' . $current->getAttribute( 'class' ) ) );
			foreach ( $markers as $marker ) {
				if ( false !== strpos( $identity, $marker ) ) {
					return true;
				}
			}

			$current = $current->parentNode;
		}

		return false;
	}

	/**
	 * Resolve target languages that should receive queued frontend string jobs.
	 *
	 * @param string $target_lang Active target language.
	 * @return array Language codes.
	 */
	private static function get_queue_target_languages( $target_lang ) {
		$target_lang = sanitize_key( $target_lang );
		$settings    = get_option( 'teknotok_ait_settings', array() );

		if ( isset( $settings['auto_translate'] ) && '0' === (string) $settings['auto_translate'] ) {
			return array_filter( array( $target_lang ) );
		}

		$source_lang = isset( $settings['source_language'] ) ? sanitize_key( $settings['source_language'] ) : '';
		$languages   = isset( $settings['target_languages'] ) && is_array( $settings['target_languages'] ) ? $settings['target_languages'] : array();
		$languages[] = $target_lang;

		$languages = array_map( 'sanitize_key', $languages );
		$languages = array_filter(
			array_unique( $languages ),
			function ( $language ) use ( $source_lang ) {
				return ! empty( $language ) && $language !== $source_lang;
			}
		);

		return array_values( $languages );
	}

	/**
	 * Determine if a string should be translated.
	 * Filters out empty/numeric content, shortcode/JSON patterns, HTML strings,
	 * and checks if the text is already written in the target language to prevent double translations.
	 *
	 * @param string $text        The text content.
	 * @param string $target_lang Target language code.
	 * @return bool True if the text should be translated, false otherwise.
	 */
	public static function should_translate( $text, $target_lang ) {
		if ( empty( $text ) || ! preg_match( '/[a-zA-Z\p{L}]/u', $text ) ) {
			return false;
		}
		if ( preg_match( '/^\[[^\]]+\]$/', $text ) ) {
			return false;
		}
		if ( 0 === strpos( $text, '[MOCK]' ) ) {
			return false;
		}
		if ( 0 === strpos( $text, '{' ) && '}' === substr( $text, -1 ) ) {
			return false;
		}
		if ( false !== strpos( $text, '<!--' ) || false !== strpos( $text, '-->' ) || false !== strpos( $text, '<div' ) || false !== strpos( $text, '</' ) ) {
			return false;
		}
		if ( preg_match( '/^[a-z0-9]+(?:[-_][a-z0-9]+)+$/', $text ) ) {
			return false;
		}

		// If text already contains the target language script, skip it to avoid double translation
		switch ( substr( $target_lang, 0, 2 ) ) {
			case 'ar':
				if ( preg_match( '/\p{Arabic}/u', $text ) ) {
					return false;
				}
				break;
			case 'zh':
				if ( preg_match( '/\p{Han}/u', $text ) ) {
					return false;
				}
				break;
			case 'ru':
				if ( preg_match( '/\p{Cyrillic}/u', $text ) ) {
					return false;
				}
				break;
			case 'ja':
				if ( preg_match( '/[\p{Hiragana}\p{Katakana}\p{Han}]/u', $text ) ) {
					return false;
				}
				break;
			case 'ko':
				if ( preg_match( '/\p{Hangul}/u', $text ) ) {
					return false;
				}
				break;
			case 'bn':
				if ( preg_match( '/\p{Bengali}/u', $text ) ) {
					return false;
				}
				break;
			case 'hi':
				if ( preg_match( '/\p{Devanagari}/u', $text ) ) {
					return false;
				}
				break;
		}

		return true;
	}
}
