import CryptoKit
import Foundation
#if canImport(XCTest) && os(iOS)
    import UIKit
    import XCTest
#endif

/// Locates elements using XCUITest APIs and returns Android-compatible format
/// Applies filtering similar to Android's ViewHierarchyExtractor to reduce hierarchy size
public class ElementLocator: ElementLocating {
    // MARK: - Filtering Constants

    /// Maximum depth to traverse (prevent infinite recursion)
    private static let maxDepth = 30

    /// Generic class names that are typically structural wrappers
    private static let structuralClassNames: Set<String> = [
        "UIView",
        "UIImageView",
        "UIWindow",
    ]

    #if canImport(XCTest) && os(iOS)
        /// Element types whose internal UIKit subviews produce same-type nested children
        /// in the XCUITest accessibility tree. These are collapsed during hierarchy building
        /// to avoid exposing non-interactive internal subviews (e.g. _UITextFieldRoundedRectBackgroundViewNeue).
        private static let textInputElementTypes: Set<XCUIElement.ElementType> = [
            .textField,        // UITextField internal subviews
            .secureTextField,  // Same internals as UITextField with isSecureTextEntry
            .textView,         // TextKit 2 internal views (_UITextLayoutCanvasView)
            .searchField,      // UISearchTextField inside UISearchBar (iOS 16+)
        ]
    #endif
    public enum LocatorError: LocalizedError {
        case noApplication
        case elementNotFound(String)

        public var errorDescription: String? {
            switch self {
            case .noApplication:
                return "No application available for element lookup"
            case let .elementNotFound(id):
                return "Element not found: \(id)"
            }
        }
    }

    #if canImport(XCTest) && os(iOS)
        /// The foreground app we're currently observing (not springboard)
        /// At most one instance besides springboard should exist
        private var foregroundApp: XCUIApplication?
        public private(set) var foregroundBundleId: String?

        /// Springboard app for detecting foreground app - always kept
        private lazy var springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

        /// Cache of resource IDs to XCUIElements
        private var elementCache: [String: XCUIElement] = [:]

        /// Whether the last foreground detection fell back to SpringBoard
        private var didFallbackToSpringboard: Bool = false

        /// Timestamp of the last explicit switchForegroundApp call (monotonic clock, nanoseconds)
        private var lastExplicitSwitchTime: UInt64 = 0

        /// Performance tracking provider
        private let perfProvider: PerfProvider

        public init(
            application: XCUIApplication? = nil,
            perfProvider: PerfProvider = PerfProvider.instance
        ) {
            foregroundApp = application
            self.perfProvider = perfProvider
        }

        public func setApplication(_ app: XCUIApplication) {
            // Release old app reference before setting new one
            foregroundApp = nil
            foregroundBundleId = nil

            foregroundApp = app
            elementCache.removeAll()
        }

        public func trackObservedBundleId(_ bundleId: String) {
            guard bundleId != "com.apple.springboard" else { return }
            observedBundleIds.insert(bundleId)
        }

        /// Set the application to observe with its bundle ID
        public func setApplication(_ app: XCUIApplication, bundleId: String) {
            // Release old app reference before setting new one
            foregroundApp = nil
            foregroundBundleId = nil

            foregroundApp = app
            foregroundBundleId = bundleId
            elementCache.removeAll()

            // Track this bundle ID for future detection
            if bundleId != "com.apple.springboard" {
                observedBundleIds.insert(bundleId)
            }
        }

        /// Explicitly switch the tracked foreground app to the given bundle ID, clearing caches.
        /// Called by CommandHandler after state-changing operations (launch, terminate, home).
        public func switchForegroundApp(bundleId: String) {
            foregroundApp = nil
            foregroundBundleId = nil
            elementCache.removeAll()
            didFallbackToSpringboard = false

            if bundleId == "com.apple.springboard" {
                foregroundApp = springboard
            } else {
                foregroundApp = XCUIApplication(bundleIdentifier: bundleId)
                observedBundleIds.insert(bundleId)
            }
            foregroundBundleId = bundleId
            lastExplicitSwitchTime = DispatchTime.now().uptimeNanoseconds
        }

        public func getAppState(bundleId: String) -> ObservedAppState {
            let stateRaw: UInt = runOnMainThreadNonThrowing({
                XCUIApplication(bundleIdentifier: bundleId).state.rawValue
            }, fallback: 0)
            switch stateRaw {
            case 0: return .unknown
            case 1: return .notRunning
            case 2: return .runningBackgroundSuspended
            case 3: return .runningBackground
            default: return .runningForeground  // rawValue >= 4
            }
        }

        public func awaitAppState(bundleId: String, expectedState: AppStateExpectation) -> Bool {
            for _ in 0..<10 {
                let stateRaw: UInt = runOnMainThreadNonThrowing({
                    XCUIApplication(bundleIdentifier: bundleId).state.rawValue
                }, fallback: 0)
                let matched: Bool
                switch expectedState {
                case .foreground:
                    matched = stateRaw >= 4
                case .notRunning:
                    matched = stateRaw <= 1
                case .background:
                    matched = stateRaw == 3
                }
                if matched { return true }
                Thread.sleep(forTimeInterval: 0.05)
            }
            return false
        }

        /// Safety-net detection: re-detect the foreground app if the current tracking seems stale.
        /// With explicit transitions via switchForegroundApp, this should rarely fire.
        private func ensureForegroundApp() {
            // If we recently did an explicit switch, trust it — the caller already set the right app
            let nsSinceSwitch = DispatchTime.now().uptimeNanoseconds - lastExplicitSwitchTime
            let msSinceSwitch = nsSinceSwitch / 1_000_000
            if msSinceSwitch < 200 {
                return
            }

            // IMPORTANT: Create fresh XCUIApplication instances to check state, because
            // cached instances may return stale state values
            let stateInfo: (springboardState: UInt, currentAppState: UInt?, currentBundleId: String?) =
                perfProvider.track("checkState") {
                    runOnMainThreadNonThrowing({
                        let sbState = self.springboard.state.rawValue
                        let freshAppState: UInt? = self.foregroundBundleId.map { bundleId in
                            XCUIApplication(bundleIdentifier: bundleId).state.rawValue
                        }
                        return (sbState, freshAppState, self.foregroundBundleId)
                    }, fallback: (0, nil, self.foregroundBundleId))
                }

            print("[ElementLocator] ensureForegroundApp: current=\(stateInfo.currentBundleId ?? "nil") state=\(stateInfo.currentAppState.map { String($0) } ?? "nil") springboard=\(stateInfo.springboardState) observed=\(observedBundleIds)")

            let isCurrentAppInForeground = (stateInfo.currentAppState ?? 0) >=
                4 // .runningForeground only (3 = .runningBackground)
            let isCurrentAppSpringboard = stateInfo.currentBundleId == "com.apple.springboard"

            // Springboard reports as foreground even when another app is on top,
            // so we always re-detect unless a non-springboard app is confirmed foreground
            if isCurrentAppInForeground && !isCurrentAppSpringboard {
                didFallbackToSpringboard = false
                return
            }

            if let detectedBundleId = perfProvider.track("detectForeground", block: { detectForegroundAppBundleId() }) {
                if detectedBundleId != foregroundBundleId {
                    print("[ElementLocator] Switching to foreground app: \(detectedBundleId)")
                    switchForegroundApp(bundleId: detectedBundleId)
                }
                didFallbackToSpringboard = false
            } else if !isCurrentAppInForeground {
                if foregroundBundleId != "com.apple.springboard" {
                    print("[ElementLocator] WARNING: No foreground app detected, falling back to springboard")
                    switchForegroundApp(bundleId: "com.apple.springboard")
                    didFallbackToSpringboard = true
                }
            }
        }

        /// Get the current application to observe
        /// Returns foreground app if available and in foreground, otherwise springboard
        private var currentApplication: XCUIApplication {
            // Check state on main thread using fresh instance (cached instances return stale state)
            let stateInfo: (state: UInt?, bundleId: String?) = runOnMainThreadNonThrowing({
                let freshState: UInt? = self.foregroundBundleId.map { bundleId in
                    XCUIApplication(bundleIdentifier: bundleId).state.rawValue
                }
                return (freshState, self.foregroundBundleId)
            }, fallback: (nil, self.foregroundBundleId))
            let foregroundAppInForeground = (stateInfo.state ?? 0) >= 4 // .runningForeground only
            if let app = foregroundApp, foregroundAppInForeground {
                return app
            }
            return springboard
        }

        /// Common system apps to check when detecting foreground app
        /// These are apps that might be launched by the user during testing
        private static let commonSystemApps: [String] = [
            "com.apple.Preferences", // Settings
            "com.apple.mobilesafari", // Safari
            "com.apple.MobileAddressBook", // Contacts
            "com.apple.mobilephone", // Phone
            "com.apple.MobileSMS", // Messages
            "com.apple.mobileslideshow", // Photos
            "com.apple.camera", // Camera
            "com.apple.AppStore", // App Store
            "com.apple.Maps", // Maps
            "com.apple.Health", // Health
            "com.apple.Fitness", // Fitness
            "com.apple.weather", // Weather
            "com.apple.mobilenotes", // Notes
            "com.apple.reminders", // Reminders
            "com.apple.mobilecal", // Calendar
            "com.apple.mobilemail", // Mail
            "com.apple.Music", // Music
            "com.apple.Podcasts", // Podcasts
            "com.apple.TV", // TV
            "com.apple.news", // News
            "com.apple.stocks", // Stocks
            "com.apple.tips", // Tips
            "com.apple.iBooks", // Books
            "com.apple.DocumentsApp", // Files
            "com.apple.calculator", // Calculator
            "com.apple.VoiceMemos", // Voice Memos
            "com.apple.compass", // Compass
            "com.apple.measure", // Measure
            "com.apple.facetime", // FaceTime
            "com.apple.Home", // Home
            "com.apple.shortcuts", // Shortcuts
            "com.apple.Translate", // Translate
            "com.apple.Magnifier", // Magnifier
            "com.apple.clock", // Clock
            "com.apple.findmy", // Find My
            "com.apple.Passbook", // Wallet
            "dev.jasonpearson.automobile.Playground", // AutoMobile Playground app
        ]

        /// Bundle IDs that have been observed during this session
        /// Used to check for foreground app without requiring hardcoded list
        private var observedBundleIds: Set<String> = []

        /// Detect the bundle ID of the foreground app
        /// Returns nil if detection fails or springboard is in front
        private func detectForegroundAppBundleId() -> String? {
            // First, try to find bundle IDs from springboard's element tree
            // This can work when apps embed their bundle ID in element identifiers
            let snapshot: XCUIElementSnapshot? = perfProvider.track("springboardSnapshot") {
                runOnMainThreadNonThrowing({
                    try? self.springboard.snapshot()
                }, fallback: nil)
            }

            if let snapshot = snapshot {
                let result: String? = perfProvider.track("checkCandidates") {
                    var candidateBundleIds: [String] = []
                    collectBundleIdsFromElement(snapshot, into: &candidateBundleIds)

                    print("[ElementLocator] Springboard candidates: \(candidateBundleIds)")

                    for bundleId in candidateBundleIds {
                        if bundleId == "com.apple.springboard" {
                            continue
                        }

                        let stateRawValue: UInt = runOnMainThreadNonThrowing({
                            let testApp = XCUIApplication(bundleIdentifier: bundleId)
                            return testApp.state.rawValue
                        }, fallback: 0)
                        print("[ElementLocator] Candidate \(bundleId) state=\(stateRawValue)")
                        if stateRawValue >= 4 { // .runningForeground only
                            print("[ElementLocator] Found foreground app from springboard: \(bundleId)")
                            return bundleId
                        }
                    }
                    return nil
                }
                if let foundBundleId = result {
                    return foundBundleId
                }
            } else {
                print("[ElementLocator] WARNING: Failed to snapshot springboard")
            }

            // Fallback: Check observed bundle IDs first (apps we've seen before)
            let observedResult: String? = perfProvider.track("checkObserved") {
                print("[ElementLocator] Checking observed bundle IDs: \(observedBundleIds)")
                for bundleId in observedBundleIds {
                    // Skip current app (we already know it's not in foreground)
                    if bundleId == foregroundBundleId {
                        continue
                    }

                    let stateRawValue: UInt = runOnMainThreadNonThrowing({
                        let testApp = XCUIApplication(bundleIdentifier: bundleId)
                        return testApp.state.rawValue
                    }, fallback: 0)
                    print("[ElementLocator] Observed \(bundleId) state=\(stateRawValue)")
                    if stateRawValue >= 4 { // .runningForeground only
                        return bundleId
                    }
                }
                return nil
            }
            if let found = observedResult {
                return found
            }

            // Fallback: Check common system apps directly
            // This is necessary because when another app is in foreground,
            // springboard's element tree may not contain that app's bundle ID
            return perfProvider.track("checkSystemApps") {
                for bundleId in Self.commonSystemApps {
                    // Skip current app (we already know it's not in foreground)
                    if bundleId == foregroundBundleId {
                        continue
                    }
                    // Skip already checked in observedBundleIds
                    if observedBundleIds.contains(bundleId) {
                        continue
                    }

                    let stateRawValue: UInt = runOnMainThreadNonThrowing({
                        let testApp = XCUIApplication(bundleIdentifier: bundleId)
                        return testApp.state.rawValue
                    }, fallback: 0)
                    if stateRawValue >= 4 { // .runningForeground only
                        print("[ElementLocator] Found foreground app from system apps: \(bundleId)")
                        return bundleId
                    }
                }
                print("[ElementLocator] No foreground app found from any source")
                return nil
            }
        }

        /// Collect all potential bundle IDs from springboard element tree
        private func collectBundleIdsFromElement(
            _ element: XCUIElementSnapshot,
            into bundleIds: inout [String],
            depth: Int = 0
        ) {
            let identifier = element.identifier

            // Springboard elements use "card:<bundleId>:sceneID:<sceneId>" format
            // e.g., "card:com.tinyspeck.chatlyio:sceneID:com.tinyspeck.chatlyio-default"
            // Also seen: "@card:<bundleId>:sceneID:..." and suffixes like "-window", "-sceneID"
            if !identifier.isEmpty {
                var cleanId = identifier

                // Handle @card:*:sceneID:* and card:*:sceneID:* formats
                if identifier.hasPrefix("@card:") {
                    cleanId = String(identifier.dropFirst(6)) // Remove "@card:"
                    if let colonIndex = cleanId.firstIndex(of: ":") {
                        cleanId = String(cleanId[..<colonIndex])
                    }
                } else if identifier.hasPrefix("card:") {
                    cleanId = String(identifier.dropFirst(5)) // Remove "card:"
                    if let colonIndex = cleanId.firstIndex(of: ":") {
                        cleanId = String(cleanId[..<colonIndex])
                    }
                }

                // Clean up common suffixes
                cleanId = cleanId.replacingOccurrences(of: "-window", with: "")
                    .replacingOccurrences(of: "-sceneID", with: "")
                    .replacingOccurrences(of: "-SceneWindow", with: "")

                // Check if it looks like a bundle ID
                if cleanId.contains(".") && !cleanId.contains(" ") {
                    if cleanId.hasPrefix("com.") || cleanId.hasPrefix("io.") || cleanId.hasPrefix("org.") ||
                        cleanId.hasPrefix("net.") || cleanId.hasPrefix("me.") || cleanId.hasPrefix("dev.")
                    {
                        if !bundleIds.contains(cleanId) {
                            bundleIds.append(cleanId)
                        }
                    }
                }
            }

            // Recursively check children
            for child in element.children {
                collectBundleIdsFromElement(child, into: &bundleIds, depth: depth + 1)
            }
        }

        // MARK: - View Hierarchy

        public func getViewHierarchy(disableAllFiltering: Bool = false) throws -> ViewHierarchy {
            perfProvider.serial("getViewHierarchy")
            defer { perfProvider.end() }

            // First, ensure we're observing the foreground app
            perfProvider.track("ensureForegroundApp") {
                ensureForegroundApp()
            }

            elementCache.removeAll()

            // Use the observed app's bundle identifier for packageName
            let bundleId = foregroundBundleId ?? "com.apple.springboard"
            print("[ElementLocator] getViewHierarchy: using bundleId=\(bundleId) fallback=\(didFallbackToSpringboard)")

            // Use snapshot() for fast hierarchy extraction - single IPC call captures everything
            // snapshot() captures all element data in ONE IPC call (fast!)
            // vs accessing properties individually which is extremely slow
            // IMPORTANT: Create a FRESH XCUIApplication instance for each snapshot to avoid
            // stale accessibility cache. Cached instances may not reflect system-presented
            // alerts like permission dialogs.
            let (snapshot, keyboardFocusFrame) = try perfProvider.track("snapshot") {
                try runOnMainThread {
                    let freshApp = XCUIApplication(bundleIdentifier: bundleId)
                    let snap = try freshApp.snapshot()

                    // Query keyboard focus via predicate — snapshot.hasFocus reflects
                    // UIKit focus (tvOS/iPad), not keyboard input focus on iPhone.
                    let focused = freshApp.descendants(matching: .any)
                        .matching(NSPredicate(format: "hasKeyboardFocus == true"))
                        .firstMatch
                    let focusFrame: CGRect? = focused.exists ? focused.frame : nil

                    return (snap, focusFrame)
                }
            }

            // Get screen bounds for offscreen filtering
            let screenBounds = snapshot.frame

            // Build hierarchy from snapshot (no more IPC calls - all data is local)
            let rawElement = perfProvider.track("buildHierarchy") {
                buildElementInfoFromSnapshot(
                    snapshot,
                    depth: 0,
                    screenBounds: screenBounds,
                    keyboardFocusFrame: keyboardFocusFrame,
                    disableAllFiltering: disableAllFiltering
                )
            }

            // Apply optimization - flatten structural wrappers and filter empty nodes
            // Skip optimization when disableAllFiltering is true (for raw hierarchy debugging)
            let rootElement: UIElementInfo
            if disableAllFiltering {
                rootElement = rawElement
            } else {
                rootElement = perfProvider.track("optimize") {
                    let optimizedElements = optimizeHierarchy(rawElement, isRoot: true)
                    return optimizedElements.first ?? rawElement
                }
            }

            // Get window info from snapshot
            let frame = snapshot.frame
            let windowInfo = WindowInfo(
                id: 0,
                type: 1, // Application window
                isActive: true,
                isFocused: true,
                bounds: ElementBounds(
                    left: Int(frame.origin.x),
                    top: Int(frame.origin.y),
                    right: Int(frame.origin.x + frame.width),
                    bottom: Int(frame.origin.y + frame.height)
                )
            )

            // Check for system alerts from multiple sources:
            // 1. Alerts in the app's own snapshot tree (permission dialogs presented within the app)
            // 2. Alerts in SpringBoard's tree (system dialogs managed by SpringBoard)
            // System permission dialogs may appear in either location depending on iOS version.
            let systemAlerts = try perfProvider.track("systemAlerts") {
                try getSystemAlerts(appSnapshot: snapshot, keyboardFocusFrame: keyboardFocusFrame)
            }

            // If there are system alerts, include them in the hierarchy
            let finalHierarchy: UIElementInfo
            if !systemAlerts.isEmpty {
                // Create a wrapper that contains both the app hierarchy and alerts
                var children = rootElement.node ?? []
                children.append(contentsOf: systemAlerts)
                finalHierarchy = UIElementInfo(
                    text: rootElement.text,
                    resourceId: rootElement.resourceId,
                    className: rootElement.className,
                    bounds: rootElement.bounds,
                    clickable: rootElement.clickable,
                    focused: rootElement.focused,
                    scrollable: rootElement.scrollable,
                    selected: rootElement.selected,
                    role: rootElement.role,
                    node: children
                )
            } else {
                finalHierarchy = rootElement
            }

            // Get screen scale and dimensions for coordinate conversion
            // iOS reports bounds in points, but screenshots are in pixels
            // screenScale converts: pixels = points * screenScale
            let (screenScale, screenWidth, screenHeight): (Float, Int, Int) = try runOnMainThread {
                let scale = Float(UIScreen.main.scale)
                let bounds = UIScreen.main.bounds
                return (scale, Int(bounds.width), Int(bounds.height))
            }

            return ViewHierarchy(
                packageName: bundleId,
                hierarchy: finalHierarchy,
                windowInfo: windowInfo,
                windows: [windowInfo],
                screenScale: screenScale,
                screenWidth: screenWidth,
                screenHeight: screenHeight,
                fallbackToSpringboard: didFallbackToSpringboard ? true : nil
            )
        }

        /// Get system alerts from the app snapshot and springboard.
        /// Checks two sources because system permission dialogs may appear in either:
        /// 1. The foreground app's accessibility tree (common on modern iOS)
        /// 2. SpringBoard's accessibility tree (for some system-level dialogs)
        /// Alert elements are extracted separately from the main hierarchy tree to ensure
        /// they are always visible as top-level children and never lost to optimization.
        /// Deduplicates by alert label text to avoid showing the same alert twice.
        private func getSystemAlerts(appSnapshot: XCUIElementSnapshot, keyboardFocusFrame: CGRect? = nil) throws -> [UIElementInfo] {
            // Check for alerts in the app's own snapshot tree
            let appAlerts = collectAlertElements(from: appSnapshot).map { snapshot in
                buildElementInfoFromSnapshot(snapshot, depth: 0, screenBounds: snapshot.frame, keyboardFocusFrame: keyboardFocusFrame)
            }

            // Also check SpringBoard for alerts not in the app's tree
            let springboardAlerts = try getAlertsFromSpringboard(keyboardFocusFrame: keyboardFocusFrame)

            // Deduplicate by alert label text
            var seenLabels: Set<String> = []
            var combined: [UIElementInfo] = []

            for alert in appAlerts {
                let label = alert.text ?? ""
                if !seenLabels.contains(label) {
                    seenLabels.insert(label)
                    combined.append(alert)
                }
            }

            for alert in springboardAlerts {
                let label = alert.text ?? ""
                if !seenLabels.contains(label) {
                    seenLabels.insert(label)
                    combined.append(alert)
                }
            }

            if !combined.isEmpty {
                print(
                    "[ElementLocator] Found \(combined.count) system alert(s): appAlerts=\(appAlerts.count), springboardAlerts=\(springboardAlerts.count)"
                )
            }

            return combined
        }

        /// Get alerts from a fresh springboard snapshot.
        /// Uses single snapshot() + tree traversal instead of .alerts query which can hang
        /// indefinitely on system permission dialogs, blocking the main thread.
        /// IMPORTANT: Creates a new XCUIApplication each call to avoid stale cached state.
        private func getAlertsFromSpringboard(keyboardFocusFrame: CGRect? = nil) throws -> [UIElementInfo] {
            let alertSnapshots: [XCUIElementSnapshot] = try runOnMainThread {
                let freshSpringboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
                guard let snapshot = try? freshSpringboard.snapshot() else { return [] }
                return self.collectAlertElements(from: snapshot)
            }

            return alertSnapshots.map { snapshot in
                buildElementInfoFromSnapshot(
                    snapshot,
                    depth: 0,
                    screenBounds: snapshot.frame,
                    keyboardFocusFrame: keyboardFocusFrame
                )
            }
        }

        /// Recursively collect alert-type element snapshots from a snapshot tree.
        /// Used instead of .alerts query which can hang on system permission dialogs.
        private func collectAlertElements(from snapshot: XCUIElementSnapshot) -> [XCUIElementSnapshot] {
            if snapshot.elementType == .alert {
                // Found an alert - return it without recursing into children
                // (buildElementInfoFromSnapshot will handle the alert's children)
                return [snapshot]
            }
            var alerts: [XCUIElementSnapshot] = []
            for child in snapshot.children {
                alerts.append(contentsOf: collectAlertElements(from: child))
            }
            return alerts
        }

        /// Build element info from XCUIElementSnapshot - all data is already captured, no IPC calls
        /// Applies early filtering: offscreen elements, zero-area elements
        /// Only sets boolean fields when true (nil = false) to reduce JSON size
        private func buildElementInfoFromSnapshot(
            _ snapshot: XCUIElementSnapshot,
            depth: Int,
            screenBounds: CGRect,
            parentPath: String = "",
            childIndex: Int = 0,
            keyboardFocusFrame: CGRect? = nil,
            disableAllFiltering: Bool = false
        )
            -> UIElementInfo
        {
            let frame = snapshot.frame

            // Skip zero-area elements
            let hasZeroArea = frame.width <= 0 || frame.height <= 0

            let bounds = ElementBounds(
                left: Int(frame.origin.x),
                top: Int(frame.origin.y),
                right: Int(frame.origin.x + frame.width),
                bottom: Int(frame.origin.y + frame.height)
            )

            // Get identifier
            let identifier = snapshot.identifier

            // Build deterministic path for viewId generation
            let resId = identifier.isEmpty ? nil : identifier
            let segment: String
            if let rid = resId {
                segment = "\(childIndex):\(rid)"
            } else {
                segment = "\(childIndex)"
            }
            let currentPath = parentPath.isEmpty ? segment : "\(parentPath)/\(segment)"
            let viewId = resId ?? generateDeterministicUuid(from: currentPath)

            // Get children from snapshot (already captured - fast!)
            // Filter out offscreen and zero-area children
            // Alert-type elements are SKIPPED here because they are extracted separately
            // by collectAlertElements() and added as top-level system alerts. This ensures
            // permission dialogs are always visible and never lost to hierarchy optimization.
            let parentClassName = mapElementType(snapshot.elementType)
            var childNodes: [UIElementInfo]?
            if depth < ElementLocator.maxDepth {
                let children = snapshot.children
                if !children.isEmpty {
                    var filteredChildren = children.enumerated().compactMap { (idx, child) -> UIElementInfo? in
                        // Skip alert elements - they are extracted separately as system alerts
                        // to ensure they're always visible as top-level children
                        if child.elementType == .alert {
                            return nil
                        }

                        let childFrame = child.frame

                        // Skip zero-area children
                        if childFrame.width <= 0 || childFrame.height <= 0 {
                            return nil
                        }

                        // Skip completely offscreen children (with margin)
                        let margin: CGFloat = 50
                        let expandedScreen = screenBounds.insetBy(dx: -margin, dy: -margin)
                        if !expandedScreen.intersects(childFrame) {
                            return nil
                        }

                        return buildElementInfoFromSnapshot(
                            child,
                            depth: depth + 1,
                            screenBounds: screenBounds,
                            parentPath: currentPath,
                            childIndex: idx,
                            keyboardFocusFrame: keyboardFocusFrame,
                            disableAllFiltering: disableAllFiltering
                        )
                    }

                    if !disableAllFiltering {
                        // Collapse same-type text-input children (e.g. UITextField inside UITextField)
                        // that are internal UIKit subviews with no unique identifying properties.
                        filteredChildren = ElementLocator.collapseSameTypeTextInputChildren(
                            parentClassName: parentClassName,
                            children: filteredChildren
                        )

                        // Deduplicate siblings with identical type + bounds + no unique properties.
                        filteredChildren = ElementLocator.deduplicateSiblings(filteredChildren)
                    }

                    childNodes = filteredChildren.isEmpty ? nil : filteredChildren
                }
            }

            // Determine boolean properties - only set to "true", leave nil for false
            // This significantly reduces JSON size
            let isEnabled = snapshot.isEnabled

            // Only mark specific element types as clickable (not generic UIViews)
            let isClickableType = isActuallyClickableType(snapshot.elementType)
            let isClickable = isEnabled && isClickableType

            let isScrollable = isScrollableType(snapshot.elementType)
            let isCheckable = isCheckableType(snapshot.elementType)
            let isSelected = snapshot.isSelected
            // UISwitch reports toggle state via value ("0"/"1"), not isSelected
            let isChecked: Bool
            if isCheckable, let value = snapshot.value as? String {
                isChecked = value == "1"
            } else {
                isChecked = isCheckable && isSelected
            }
            // snapshot.hasFocus reflects UIKit focus (tvOS/iPad), not keyboard input
            // focus on iPhone. Use the keyboardFocusFrame from the predicate query instead.
            let hasFocus: Bool
            if let focusFrame = keyboardFocusFrame, !frame.isEmpty, !focusFrame.isEmpty {
                let epsilon: CGFloat = 0.5
                let framesMatch = abs(frame.origin.x - focusFrame.origin.x) < epsilon
                    && abs(frame.origin.y - focusFrame.origin.y) < epsilon
                    && abs(frame.width - focusFrame.width) < epsilon
                    && abs(frame.height - focusFrame.height) < epsilon
                let isTextInput = snapshot.elementType == .textField
                    || snapshot.elementType == .textView
                    || snapshot.elementType == .secureTextField
                hasFocus = framesMatch && isTextInput
            } else {
                hasFocus = snapshot.hasFocus
            }
            let isPassword = snapshot.elementType == .secureTextField

            // Only include actions for text input elements (click is implied by clickable)
            var actions: [String]?
            if isEnabled && (snapshot.elementType == .textField || snapshot.elementType == .textView ||
                snapshot.elementType == .secureTextField)
            {
                actions = ["set_text", "clear_text"]
            }

            // Get label - use for text (don't duplicate in content-desc)
            let label = snapshot.label.isEmpty ? nil : snapshot.label

            // For text inputs, surface the entered value separately from the
            // accessibility label (which is typically the placeholder for
            // UISearchBar / UITextField). Mask password content to avoid
            // leaking secrets through the hierarchy.
            let isTextInput = snapshot.elementType == .textField
                || snapshot.elementType == .textView
                || snapshot.elementType == .secureTextField
                || snapshot.elementType == .searchField
            var enteredValue: String?
            if isTextInput, let raw = snapshot.value as? String, !raw.isEmpty {
                enteredValue = isPassword ? String(repeating: "•", count: raw.count) : raw
            }

            return UIElementInfo(
                text: label,
                value: enteredValue,
                textSize: nil,
                contentDesc: nil, // Don't duplicate - label is in text
                resourceId: resId,
                className: parentClassName,
                bounds: hasZeroArea ? nil : bounds, // Don't include bounds for zero-area elements
                // Only include boolean fields when true (nil = false)
                clickable: isClickable ? "true" : nil,
                enabled: nil, // Don't include enabled - it's almost always true and implied by clickable
                focusable: nil, // Don't include - almost all elements are focusable on iOS
                focused: hasFocus ? "true" : nil,
                accessibilityFocused: nil,
                scrollable: isScrollable ? "true" : nil,
                password: isPassword ? "true" : nil,
                checkable: isCheckable ? "true" : nil,
                checked: isChecked ? "true" : nil,
                selected: isSelected ? "true" : nil,
                longClickable: nil, // Don't include - same as clickable on iOS
                testTag: nil, // Don't duplicate - identifier is in resourceId
                role: mapRole(snapshot.elementType),
                stateDescription: nil,
                errorMessage: nil,
                hintText: snapshot.placeholderValue,
                viewId: viewId,
                extras: nil,
                actions: actions,
                node: childNodes
            )
        }

        // MARK: - Deterministic ID Generation

        /// Generate a deterministic UUID from a hierarchy path string using SHA-256.
        /// The path encodes the element's position in the tree, ensuring stable IDs
        /// across snapshots when the tree structure hasn't changed.
        private func generateDeterministicUuid(from path: String) -> String {
            let data = Data(path.utf8)
            let digest = SHA256.hash(data: data)
            let bytes = Array(digest)
            let hex = bytes.prefix(16).map { String(format: "%02x", $0) }.joined()
            return "\(hex.prefix(8))-\(hex.dropFirst(8).prefix(4))-\(hex.dropFirst(12).prefix(4))-\(hex.dropFirst(16).prefix(4))-\(hex.dropFirst(20).prefix(12))"
        }

        // MARK: - Hierarchy Optimization

        /// Check if an element has meaningful content that should be preserved
        private func meetsFilterCriteria(_ element: UIElementInfo) -> Bool {
            // String criteria - element has useful identifying information
            let hasStringCriteria =
                element.text != nil ||
                element.resourceId != nil ||
                element.role != nil ||
                element.hintText != nil

            // Boolean criteria - element is interactive
            let hasBooleanCriteria =
                element.clickable == "true" ||
                element.scrollable == "true" ||
                element.focused == "true" ||
                element.selected == "true" ||
                element.checkable == "true"

            return hasStringCriteria || hasBooleanCriteria
        }

        /// Check if a class name is a structural wrapper (no semantic meaning)
        private func isStructuralWrapper(_ className: String?) -> Bool {
            guard let className = className else { return false }
            return ElementLocator.structuralClassNames.contains(className)
        }

        /// Optimizes the hierarchy by:
        /// 1. Promoting children of bounds-only wrapper nodes (structural nodes with only bounds)
        /// 2. Filtering out empty structural nodes
        /// 3. Preserving interactive elements and their children
        ///
        /// This significantly reduces hierarchy size for complex UIs.
        private func optimizeHierarchy(_ element: UIElementInfo, isRoot: Bool = false) -> [UIElementInfo] {
            // Check if this element is a bounds-only wrapper (has no useful properties)
            let meetsCriteria = meetsFilterCriteria(element)
            let isStructural = isStructuralWrapper(element.className)
            let isBoundsOnlyWrapper = !meetsCriteria && isStructural

            // Never promote children of interactive elements
            let isInteractive = element.clickable == "true" ||
                element.scrollable == "true" ||
                element.selected == "true"

            // First, recursively optimize children
            var optimizedChildren: [UIElementInfo]?
            if let children = element.node {
                let optimized = children.flatMap { child in
                    optimizeHierarchy(child, isRoot: false)
                }
                optimizedChildren = optimized.isEmpty ? nil : optimized
            }

            // Root element is always kept
            if isRoot {
                return [UIElementInfo(
                    text: element.text,
                    value: element.value,
                    textSize: element.textSize,
                    contentDesc: element.contentDesc,
                    resourceId: element.resourceId,
                    className: element.className,
                    bounds: element.bounds,
                    clickable: element.clickable,
                    enabled: element.enabled,
                    focusable: element.focusable,
                    focused: element.focused,
                    accessibilityFocused: element.accessibilityFocused,
                    scrollable: element.scrollable,
                    password: element.password,
                    checkable: element.checkable,
                    checked: element.checked,
                    selected: element.selected,
                    longClickable: element.longClickable,
                    testTag: element.testTag,
                    role: element.role,
                    stateDescription: element.stateDescription,
                    errorMessage: element.errorMessage,
                    hintText: element.hintText,
                    actions: element.actions,
                    node: optimizedChildren
                )]
            }

            // Only promote children (flatten hierarchy) if this is a bounds-only wrapper AND not interactive
            if isBoundsOnlyWrapper && !isInteractive {
                if let children = optimizedChildren {
                    // Promote children - flatten this wrapper node
                    return children
                }
                // No children and no content - filter out completely
                return []
            }

            // Keep this element with optimized children
            return [UIElementInfo(
                text: element.text,
                value: element.value,
                textSize: element.textSize,
                contentDesc: element.contentDesc,
                resourceId: element.resourceId,
                className: element.className,
                bounds: element.bounds,
                clickable: element.clickable,
                enabled: element.enabled,
                focusable: element.focusable,
                focused: element.focused,
                accessibilityFocused: element.accessibilityFocused,
                scrollable: element.scrollable,
                password: element.password,
                checkable: element.checkable,
                checked: element.checked,
                selected: element.selected,
                longClickable: element.longClickable,
                testTag: element.testTag,
                role: element.role,
                stateDescription: element.stateDescription,
                errorMessage: element.errorMessage,
                hintText: element.hintText,
                actions: element.actions,
                node: optimizedChildren
            )]
        }


        private func mapElementType(_ type: XCUIElement.ElementType) -> String {
            switch type {
            case .application: return "XCUIApplication"
            case .window: return "UIWindow"
            case .button: return "UIButton"
            case .staticText: return "UILabel"
            case .textField: return "UITextField"
            case .secureTextField: return "UISecureTextField"
            case .textView: return "UITextView"
            case .image: return "UIImageView"
            case .switch: return "UISwitch"
            case .slider: return "UISlider"
            case .picker: return "UIPickerView"
            case .table: return "UITableView"
            case .cell: return "UITableViewCell"
            case .scrollView: return "UIScrollView"
            case .collectionView: return "UICollectionView"
            case .navigationBar: return "UINavigationBar"
            case .tabBar: return "UITabBar"
            case .toolbar: return "UIToolbar"
            case .searchField: return "UISearchBar"
            case .alert: return "UIAlertController"
            case .sheet: return "UIActionSheet"
            case .progressIndicator: return "UIProgressView"
            case .activityIndicator: return "UIActivityIndicatorView"
            case .segmentedControl: return "UISegmentedControl"
            case .stepper: return "UIStepper"
            case .datePicker: return "UIDatePicker"
            case .webView: return "WKWebView"
            case .link: return "UILink"
            case .keyboard: return "UIKeyboard"
            case .key: return "UIKeyboardKey"
            default: return "UIView"
            }
        }

        private func mapRole(_ type: XCUIElement.ElementType) -> String? {
            switch type {
            case .button: return "button"
            case .link: return "link"
            case .switch: return "switch"
            case .checkBox: return "checkbox"
            case .radioButton: return "radio"
            case .slider: return "slider"
            case .textField, .textView, .secureTextField: return "textfield"
            case .image: return "image"
            case .staticText: return "text"
            case .table, .collectionView: return "list"
            case .cell: return "listitem"
            case .tab: return "tab"
            case .progressIndicator: return "progressbar"
            default: return nil
            }
        }

        private func isScrollableType(_ type: XCUIElement.ElementType) -> Bool {
            switch type {
            case .scrollView, .table, .collectionView, .webView, .textView:
                return true
            default:
                return false
            }
        }

        private func isCheckableType(_ type: XCUIElement.ElementType) -> Bool {
            switch type {
            case .switch, .checkBox, .radioButton:
                return true
            default:
                return false
            }
        }

        /// Check if an element type is actually clickable (not just a generic container)
        /// This prevents marking every UIView as clickable just because it's enabled
        private func isActuallyClickableType(_ type: XCUIElement.ElementType) -> Bool {
            switch type {
            // Interactive controls
            case .button, .link, .switch, .slider, .stepper, .segmentedControl:
                return true
            // Checkable items
            case .checkBox, .radioButton:
                return true
            // Text input
            case .textField, .textView, .secureTextField, .searchField:
                return true
            // List items (cells are tappable)
            case .cell:
                return true
            // Tab and navigation items
            case .tab, .tabBar:
                return true
            // Pickers
            case .picker, .datePicker:
                return true
            // Alert/sheet buttons
            case .alert, .sheet:
                return true
            // Keyboard keys
            case .key:
                return true
            // Images can be tappable
            case .image:
                return true
            // Everything else (UIView, window, staticText, etc.) is not inherently clickable
            default:
                return false
            }
        }

        // MARK: - Element Finding

        public func findElement(byResourceId resourceId: String) -> Any? {
            if let cached = elementCache[resourceId] {
                return cached
            }

            let app = currentApplication
            // Query .any first (1 IPC call). If the match is a text-input type,
            // re-query with the specific type to get the outermost (parent) element
            // instead of an internal UIKit subview that shares the same identifier.
            guard let element: XCUIElement = runOnMainThreadNonThrowing({
                let anyMatch = app.descendants(matching: .any)
                    .matching(identifier: resourceId).firstMatch
                guard anyMatch.exists else { return nil as XCUIElement? }

                let matchedType = anyMatch.elementType
                if Self.textInputElementTypes.contains(matchedType) {
                    let typedMatch = app.descendants(matching: matchedType)
                        .matching(identifier: resourceId).firstMatch
                    if typedMatch.exists {
                        return typedMatch as XCUIElement?
                    }
                }
                return anyMatch as XCUIElement?
            }, fallback: nil) else { return nil }
            elementCache[resourceId] = element
            return element
        }

        public func findElement(byText text: String) -> Any? {
            let app = currentApplication
            return runOnMainThreadNonThrowing({
                let match = app.descendants(matching: .any)
                    .matching(NSPredicate(format: "label == %@", text)).firstMatch
                return match.exists ? match as XCUIElement? : nil
            }, fallback: nil)
        }

        public func getCachedElement(_ resourceId: String) -> XCUIElement? {
            return elementCache[resourceId]
        }

    #else
        /// Non-iOS stub implementation
        public init() {}

        public func getViewHierarchy(disableAllFiltering _: Bool = false) throws -> ViewHierarchy {
            return ViewHierarchy(
                packageName: nil,
                hierarchy: nil,
                windowInfo: nil,
                windows: nil,
                error: "XCUITest only available on iOS"
            )
        }

        public func findElement(byResourceId _: String) -> Any? {
            return nil
        }

        public func findElement(byText _: String) -> Any? {
            return nil
        }

        public func trackObservedBundleId(_: String) {
            // no-op on non-iOS
        }

        public func switchForegroundApp(bundleId _: String) {
            // no-op on non-iOS
        }

        public func getAppState(bundleId _: String) -> ObservedAppState {
            return .unknown
        }

        public func awaitAppState(bundleId _: String, expectedState _: AppStateExpectation) -> Bool {
            return true
        }

        public var foregroundBundleId: String? { nil }
    #endif

    // MARK: - Same-Type Child Collapsing & Sibling Dedup
    // These are platform-independent (operate on UIElementInfo only) so they
    // live outside the #if os(iOS) block and are testable on macOS.

    /// Class name strings for text-input element types whose UIKit internal subviews
    /// produce same-type nested children in the XCUITest accessibility tree.
    /// NOTE: Must stay in sync with `textInputElementTypes` (the XCUIElement.ElementType set
    /// inside #if os(iOS)). Note that .searchField maps to "UISearchBar" (not "UISearchField").
    static let textInputClassNames: Set<String> = [
        "UITextField",
        "UISecureTextField",
        "UITextView",
        "UISearchBar",
    ]

    /// Check whether a UIElementInfo carries any unique identifying information
    /// (text, resourceId, contentDesc, hintText). Elements without these are
    /// considered internal UIKit subviews that should be collapsed/deduped.
    static func hasUniqueIdentifyingProperties(_ element: UIElementInfo) -> Bool {
        return element.text != nil
            || element.resourceId != nil
            || element.contentDesc != nil
            || element.hintText != nil
    }

    /// Collapse same-type text-input children into their parent.
    ///
    /// iOS UIKit exposes internal subviews (e.g. _UITextFieldRoundedRectBackgroundViewNeue)
    /// as accessibility elements with the *same* elementType as the parent text field.
    /// These are non-interactive noise that confuse element targeting.
    ///
    /// For text-input parent types, any child with the same className that carries no
    /// unique identifying properties is collapsed: its children are absorbed into the
    /// parent's child list, and the duplicate wrapper is removed.
    static func collapseSameTypeTextInputChildren(
        parentClassName: String?,
        children: [UIElementInfo]
    ) -> [UIElementInfo] {
        guard let parentClass = parentClassName,
              textInputClassNames.contains(parentClass)
        else {
            return children
        }

        var result: [UIElementInfo] = []
        for child in children {
            if child.className == parentClass && !hasUniqueIdentifyingProperties(child) && !hasStateFlags(child) {
                // Collapse: absorb this child's children into the parent level
                if let grandchildren = child.node {
                    result.append(contentsOf: grandchildren)
                }
                // else: empty same-type wrapper — discard entirely
            } else {
                result.append(child)
            }
        }
        return result
    }

    /// Whether the element carries any state flags (focused, selected, checked,
    /// password, clickable, scrollable) that make it semantically distinct.
    private static func hasStateFlags(_ element: UIElementInfo) -> Bool {
        return element.focused != nil || element.selected != nil
            || element.checked != nil || element.password != nil
            || element.clickable != nil || element.scrollable != nil
    }

    /// Deduplicate sibling elements that share the same elementType, identical bounds,
    /// and carry no unique identifying properties (no id, text, contentDesc, hintText).
    /// Only deduplicates leaf elements (no children) — elements with distinct subtrees
    /// are always preserved to avoid discarding valid controls.
    /// Keeps only the first occurrence of each duplicate.
    static func deduplicateSiblings(_ children: [UIElementInfo]) -> [UIElementInfo] {
        var seen: Set<String> = []
        var result: [UIElementInfo] = []

        for child in children {
            if hasUniqueIdentifyingProperties(child) {
                // Has unique info — always keep
                result.append(child)
                continue
            }

            // Elements with children have distinct subtrees — always keep
            if let node = child.node, !node.isEmpty {
                result.append(child)
                continue
            }

            // Elements with any state flags are considered distinct — don't discard
            // a focused/selected/checked/password element as a duplicate
            if hasStateFlags(child) {
                result.append(child)
                continue
            }

            // Build key from className + bounds
            let key: String
            if let cls = child.className, let b = child.bounds {
                key = "\(cls)|\(b.left),\(b.top),\(b.right),\(b.bottom)"
            } else if let cls = child.className {
                key = "\(cls)|nobounds"
            } else {
                // No className — can't meaningfully dedup, keep it
                result.append(child)
                continue
            }

            if seen.contains(key) {
                continue
            }
            seen.insert(key)
            result.append(child)
        }
        return result
    }
}
