/*
 * Copyright 2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hbasesoft.framework.ai.agent.tool.browser;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.LoadState;

/**
 * A class that manages a collection of interactive elements on a page, providing global index access.
 */
public class InteractiveElementRegistry {

    private static final Logger log = LoggerFactory.getLogger(InteractiveElementRegistry.class);

    /**
     * JavaScript code for selecting interactive elements
     */
    private static final String EXTRACT_INTERACTIVE_ELEMENTS_JS = """
        ((index) => {

        const TMP = []
        const ID = {"count": index}
        const COMPUTED_STYLES = new WeakMap();
        const XPATH_CACHE = new WeakMap();
        const INTERACTIVE_ELEMENT_CACHE = new WeakMap();
        const CURRENT_TIMESTAMP = Date.now();
        const INTERACTIVE_ELEMENT_TAG = new Set([
        	"a",          // Links
        	"button",     // Buttons
        	"input",      // All input types (text, checkbox, radio, etc.)
        	"select",     // Dropdown menus
        	"textarea",   // Text areas
        	"details",    // Expandable details
        	"summary",    // Summary element (clickable part of details)
        	"label",      // Form labels (often clickable)
        	"option",     // Select options
        	"optgroup",   // Option groups
        	"fieldset",   // Form fieldsets (can be interactive with legend)
        	"legend",     // Fieldset legends
        ]);

        extract(document.body)
        return parseElement()

        function parseElement() {
        	const RES = []
        	for (const element of TMP) {
        	// Filter sub elements can be clicked
        	let skip = false
        	for (const child of element.childNodes) {
        		if (INTERACTIVE_ELEMENT_CACHE.has(child) && INTERACTIVE_ELEMENT_CACHE.get(child)) {
        		skip = true
        		break
        		}
        	}
        	const tagName = element.tagName.toLowerCase()
        	// if tagName is in INTERACTIVE_ELEMENT_TAG need to be processed
        	if (skip && !INTERACTIVE_ELEMENT_TAG.has(tagName)) {
        		continue
        	}
        	const index = ID.count++
        	let jManusId
        	if (element.setAttribute) {
        		jManusId = CURRENT_TIMESTAMP + "-" + index;
        		element.setAttribute("jmanus-id", jManusId)
        	}
        	const text = element.innerText
        	const outerHtml = element.outerHTML
        	const xpath = getXPathTree(element)
        	RES.push({tagName, text, outerHtml, index, xpath, jManusId})
        	}
        	return RES
        }

        function extract(element) {
        	for (const child of element.childNodes) {
        	extract(child)
        	}

        	// Process children, with special handling for iframes and rich text editors
        	if (element.tagName) {
        	const tagName = element.tagName.toLowerCase();
        	// iframe ignore
        	if (tagName === "iframe") {
        		return;
        	} else {
        		// Handle shadow DOM
        		if (element.shadowRoot) {
        		for (const child of element.shadowRoot.childNodes) {
        			extract(child);
        		}
        		}
        	}
        	}

        	let isInteractive = isInteractiveElement(element);
        	if (!isInteractive) {
        	return;
        	}
        	if (!isElementVisible(element)) {
        	return;
        	}
        	INTERACTIVE_ELEMENT_CACHE.set(element, isInteractive);
        	TMP.push(element)
        }

        /**
         * Returns an XPath tree string for an element.
         */
        function getXPathTree(element, stopAtBoundary = true) {
        	if (XPATH_CACHE.has(element)) {
        	return XPATH_CACHE.get(element);
        	}
        	const segments = [];
        	let currentElement = element;

        	while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
        	// Stop if we hit a shadow root or iframe
        	if (stopAtBoundary &&
        		(currentElement.parentNode instanceof ShadowRoot ||
        		currentElement.parentNode instanceof HTMLIFrameElement)
        	) {
        		break;
        	}

        	const position = getElementPosition(currentElement);
        	const tagName = currentElement.nodeName.toLowerCase();
        	const xpathIndex = position > 0 ? `[${position}]` : "";
        	segments.unshift(`${tagName}${xpathIndex}`);

        	currentElement = currentElement.parentNode;
        	}

        	const result = segments.join("/");
        	XPATH_CACHE.set(element, result);
        	return result;
        }

        function getElementPosition(currentElement) {
        	if (!currentElement.parentElement) {
        	return 0; // No parent means no siblings
        	}

        	const tagName = currentElement.nodeName.toLowerCase();

        	const siblings = Array.from(currentElement.parentElement.children)
        	.filter((sib) => sib.nodeName.toLowerCase() === tagName);

        	if (siblings.length === 1) {
        	return 0; // Only element of its type
        	}

        	const index = siblings.indexOf(currentElement) + 1; // 1-based index
        	return index;
        }

        function getCachedComputedStyle(element) {
        	if (!element) return null;

        	if (COMPUTED_STYLES.has(element)) {
        	return COMPUTED_STYLES.get(element);
        	}
        	let style = window.getComputedStyle(element);
        	if (style) {
        	COMPUTED_STYLES.set(element, style);
        	}
        	return style;
        }

        function isElementVisible(element) {
        	const style = getCachedComputedStyle(element);
        	return (
        	element.offsetWidth > 0 &&
        	element.offsetHeight > 0 &&
        	style.visibility !== "hidden" &&
        	style.display !== "none"
        	);
        }

        function isInteractiveElement(element) {
        	if (!element || element.nodeType !== Node.ELEMENT_NODE) {
        	return false;
        	}

        	// Cache the tagName and style lookups
        	const tagName = element.tagName.toLowerCase();
        	const style = getCachedComputedStyle(element);

        	// Define interactive cursors
        	const interactiveCursors = new Set([
        	'pointer',    // Link/clickable elements
        	'move',       // Movable elements
        	'text',       // Text selection
        	'grab',       // Grabbable elements
        	'grabbing',   // Currently grabbing
        	'cell',       // Table cell selection
        	'copy',       // Copy operation
        	'alias',      // Alias creation
        	'all-scroll', // Scrollable content
        	'col-resize', // Column resize
        	'context-menu', // Context menu available
        	'crosshair',  // Precise selection
        	'e-resize',   // East resize
        	'ew-resize',  // East-west resize
        	'help',       // Help available
        	'n-resize',   // North resize
        	'ne-resize',  // Northeast resize
        	'nesw-resize', // Northeast-southwest resize
        	'ns-resize',  // North-south resize
        	'nw-resize',  // Northwest resize
        	'nwse-resize', // Northwest-southeast resize
        	'row-resize', // Row resize
        	's-resize',   // South resize
        	'se-resize',  // Southeast resize
        	'sw-resize',  // Southwest resize
        	'vertical-text', // Vertical text selection
        	'w-resize',   // West resize
        	'zoom-in',    // Zoom in
        	'zoom-out'    // Zoom out
        	]);

        	// Define non-interactive cursors
        	const nonInteractiveCursors = new Set([
        	'not-allowed', // Action not allowed
        	'no-drop',     // Drop not allowed
        	'wait',        // Processing
        	'progress',    // In progress
        	'initial',     // Initial value
        	'inherit'      // Inherited value
        	//? Let's just include all potentially clickable elements that are not specifically blocked
        	// 'none',        // No cursor
        	// 'default',     // Default cursor
        	// 'auto',        // Browser default
        	]);

        	function doesElementHaveInteractivePointer(element) {
        	if (element.tagName.toLowerCase() === "html") return false;

        	if (interactiveCursors.has(style.cursor)) return true;

        	return false;
        	}

        	let isInteractiveCursor = doesElementHaveInteractivePointer(element);

        	// Genius fix for almost all interactive elements
        	if (isInteractiveCursor) {
        	return true;
        	}

        	// Define explicit disable attributes and properties
        	const explicitDisableTags = new Set([
        	'disabled',           // Standard disabled attribute
        	// 'aria-disabled',      // ARIA disabled state
        	'readonly',          // Read-only state
        	// 'aria-readonly',     // ARIA read-only state
        	// 'aria-hidden',       // Hidden from accessibility
        	// 'hidden',            // Hidden attribute
        	// 'inert',             // Inert attribute
        	// 'aria-inert',        // ARIA inert state
        	// 'tabindex="-1"',     // Removed from tab order
        	// 'aria-hidden="true"' // Hidden from screen readers
        	]);

        	// handle inputs, select, checkbox, radio, textarea, button and make sure they are not cursor style disabled/not-allowed
        	if (INTERACTIVE_ELEMENT_TAG.has(tagName)) {
        	// Check for non-interactive cursor
        	if (nonInteractiveCursors.has(style.cursor)) {
        		return false;
        	}

        	// Check for explicit disable attributes
        	for (const disableTag of explicitDisableTags) {
        		if (element.hasAttribute(disableTag) ||
        		element.getAttribute(disableTag) === 'true' ||
        		element.getAttribute(disableTag) === '') {
        		return false;
        		}
        	}

        	// Check for disabled property on form elements
        	if (element.disabled) {
        		return false;
        	}

        	// Check for readonly property on form elements
        	if (element.readOnly) {
        		return false;
        	}

        	// Check for inert property
        	if (element.inert) {
        		return false;
        	}

        	return true;
        	}

        	const role = element.getAttribute("role");
        	const ariaRole = element.getAttribute("aria-role");

        	// Check for contenteditable attribute
        	if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
        	return true;
        	}

        	// Added enhancement to capture dropdown interactive elements
        	if (element.classList && (
        	element.classList.contains("button") ||
        	element.classList.contains('dropdown-toggle') ||
        	element.getAttribute('data-index') ||
        	element.getAttribute('data-toggle') === 'dropdown' ||
        	element.getAttribute('aria-haspopup') === 'true'
        	)) {
        	return true;
        	}

        	const interactiveRoles = new Set([
        	'button',           // Directly clickable element
        	// 'link',            // Clickable link
        	// 'menuitem',        // Clickable menu item
        	'menuitemradio',   // Radio-style menu item (selectable)
        	'menuitemcheckbox', // Checkbox-style menu item (toggleable)
        	'radio',           // Radio button (selectable)
        	'checkbox',        // Checkbox (toggleable)
        	'tab',             // Tab (clickable to switch content)
        	'switch',          // Toggle switch (clickable to change state)
        	'slider',          // Slider control (draggable)
        	'spinbutton',      // Number input with up/down controls
        	'combobox',        // Dropdown with text input
        	'searchbox',       // Search input field
        	'textbox',         // Text input field
        	// 'listbox',         // Selectable list
        	'option',          // Selectable option in a list
        	'scrollbar'        // Scrollable control
        	]);

        	// Basic role/attribute checks
        	const hasInteractiveRole =
        	INTERACTIVE_ELEMENT_TAG.has(tagName) ||
        	interactiveRoles.has(role) ||
        	interactiveRoles.has(ariaRole);

        	if (hasInteractiveRole) return true;

        	// check whether element has event listeners by window.getEventListeners
        	try {
        	if (typeof getEventListeners === 'function') {
        		const listeners = getEventListeners(element);
        		const mouseEvents = ['click', 'mousedown', 'mouseup', 'dblclick'];
        		for (const eventType of mouseEvents) {
        		if (listeners[eventType] && listeners[eventType].length > 0) {
        			return true; // Found a mouse interaction listener
        		}
        		}
        	}

        	const getEventListenersForNode = window.getEventListenersForNode;
        	if (typeof getEventListenersForNode === 'function') {
        		const listeners = getEventListenersForNode(element);
        		const interactionEvents = ['click', 'mousedown', 'mouseup', 'keydown', 'keyup', 'submit', 'change', 'input', 'focus', 'blur'];
        		for (const eventType of interactionEvents) {
        		for (const listener of listeners) {
        			if (listener.type === eventType) {
        			return true; // Found a common interaction listener
        			}
        		}
        		}
        	}
        	// Fallback: Check common event attributes if getEventListeners is not available (getEventListeners doesn't work in page.evaluate context)
        	const commonMouseAttrs = ['onclick', 'onmousedown', 'onmouseup', 'ondblclick'];
        	for (const attr of commonMouseAttrs) {
        		if (element.hasAttribute(attr) || typeof element[attr] === 'function') {
        		return true;
        		}
        	}
        	} catch (e) {
        	// console.warn(`Could not check event listeners for ${element.tagName}:`, e);
        	// If checking listeners fails, rely on other checks
        	}

        	return false
        }
        })""";

    // Removed the static initialization block, directly using string constants

    /**
     * A list of all interactive elements, sorted by global index
     */
    private final List<InteractiveElement> interactiveElements = new CopyOnWriteArrayList<>();

    /**
     * A quick lookup from index to element
     */
    private final Map<Integer, InteractiveElement> indexToElementMap = new ConcurrentHashMap<>();

    /**
     * Refresh all interactive elements on the specified page
     * 
     * @param page The page to process
     */
    public void refresh(Page page) {
        clearCache();
        waitForPageLoad(page);
        processPageElements(page);
        log.info("Loaded {} interactive elements", interactiveElements.size());
    }

    /**
     * Clear the current cache
     */
    private void clearCache() {
        interactiveElements.clear();
        indexToElementMap.clear();
    }

    /**
     * Wait for the page to fully load
     * 
     * @param page Page instance
     */
    private void waitForPageLoad(Page page) {
        try {
            page.waitForLoadState(LoadState.DOMCONTENTLOADED);
            log.info("Page loaded");
        }
        catch (Exception e) {
            log.warn("Error waiting for page load: {}", e.getMessage());
        }
    }

    /**
     * Process interactive elements in a single iframe
     * 
     * @param page current browser page
     */
    @SuppressWarnings("unchecked")
    private void processPageElements(Page page) {
        try {
            int index = 0;
            for (Frame frame : page.frames()) {
                List<Map<String, Object>> elementMapList = (List<Map<String, Object>>) frame
                    .evaluate(EXTRACT_INTERACTIVE_ELEMENTS_JS, index);
                for (Map<String, Object> elementMap : elementMapList) {
                    Integer globalIndex = (Integer) elementMap.get("index");
                    InteractiveElement element = new InteractiveElement(globalIndex, frame, elementMap);
                    interactiveElements.add(element);
                    indexToElementMap.put(globalIndex, element);
                }
                index = interactiveElements.size();
            }

        }
        catch (Exception e) {
            log.warn("Error processing page elements: {}", e.getMessage());
        }
    }

    /**
     * Get all interactive elements list
     * 
     * @return Interactive elements list
     */
    public List<InteractiveElement> getAllElements(Page page) {
        refresh(page);
        return new CopyOnWriteArrayList<>(interactiveElements);
    }

    /**
     * Get interactive element by global index
     * 
     * @param index Global index
     * @return The corresponding interactive element, or empty if not found
     */
    public Optional<InteractiveElement> getElementById(int index) {
        return Optional.ofNullable(indexToElementMap.get(index));
    }

    /**
     * Get the number of currently registered elements
     * 
     * @return Number of elements
     */
    public int size() {
        return interactiveElements.size();
    }

    /**
     * Generate detailed information text for all elements
     * 
     * @return Formatted element information string
     */
    public String generateElementsInfoText(Page page) {
        StringBuilder result = new StringBuilder();
        for (InteractiveElement element : getAllElements(page)) {
            result.append(element.toString().replace("\n", "")).append("\n");
        }
        return result.toString();
    }

    /**
     * Perform an action on a specific index element
     * 
     * @param index The global index of the element
     * @param action The action to perform, such as click, fill, etc.
     * @return Whether the action was successful
     */
    public boolean performAction(int index, ElementAction action) {
        Optional<InteractiveElement> elementOpt = getElementById(index);
        if (elementOpt.isPresent()) {
            InteractiveElement element = elementOpt.get();
            try {
                // Execute the specified action
                action.execute(element);
                return true;
            }
            catch (Exception e) {
                log.error("Error performing element action: {}", e.getMessage());
                return false;
            }
        }
        log.warn("Element with index {} not found", index);
        return false;
    }

    /**
     * Element action interface
     */
    public interface ElementAction {

        /**
         * Execute an action on an element
         * 
         * @param element The element to operate on
         */
        void execute(InteractiveElement element);

    }

}
