#!/usr/bin/env swift

import Cocoa
import ApplicationServices

// Minimal production selection observer.
// Option + left mouse up triggers selection capture (immediate + two delayed passes).

final class SelectionObserver {
    private var eventTap: CFMachPort?
    private var runLoopSource: CFRunLoopSource?
    private var mouseUpTimer: Timer?
    private var retrievalTimers: [DispatchWorkItem] = []
    private var lastSentJSON: String?
    private var optionDown = false
    private let passes: [TimeInterval] = [0.0, 0.05, 0.15]
    private var sequence: UInt64 = 0
    private var lastHadSelection: Bool = false
    private var selectionAnchor: CGPoint? // position where selection was emitted
    private let cancelDistanceX: CGFloat = 130
    private let cancelDistanceY: CGFloat = 80
    private var selectionTimestamp: TimeInterval = 0
    private let minLifetimeBeforeCancel: TimeInterval = 0.25 // don't cancel immediately
    private var lastMovementCheckPos: CGPoint?
    private var lastMovementCheckTime: TimeInterval = 0
    private let movementCheckInterval: TimeInterval = 0.04 // sample throttling
    private var mouseUpPosition: CGPoint?
    private var currentModifiers: Set<String> = []
    private var selectionModifiers: [String] = []
    private var lastWindowInfo: [String: Any]? // store window info for paste targeting
    private var selectionAppPID: pid_t?
    private let editableRoles: Set<String> = ["AXTextField", "AXTextArea", "AXSearchField", "AXComboBox", "AXTextView", "AXEditableText"]

    init() {
        setupEventTap()
        setupAXObserver()
    listenForProcessedText()
    }

    func run() { CFRunLoopRun() }

    private func setupEventTap() {
        let mask = (1 << CGEventType.leftMouseUp.rawValue) |
                   (1 << CGEventType.flagsChanged.rawValue) |
                   (1 << CGEventType.mouseMoved.rawValue) |
                   (1 << CGEventType.leftMouseDragged.rawValue) |
                   (1 << CGEventType.keyDown.rawValue)
        let ref = Unmanaged.passUnretained(self).toOpaque()
        eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap,
                                     place: .headInsertEventTap,
                                     options: .listenOnly,
                                     eventsOfInterest: CGEventMask(mask),
                                     callback: { _, type, event, refcon in
            let me = Unmanaged<SelectionObserver>.fromOpaque(refcon!).takeUnretainedValue()
            me.handle(eventType: type, event: event)
            return Unmanaged.passRetained(event)
        }, userInfo: ref)
        guard let tap = eventTap else {
            fputs("event tap unavailable; enable Input Monitoring for Textwrench\n", stderr)
            return
        }
        runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
        CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
        CGEvent.tapEnable(tap: tap, enable: true)
    }

    private func handle(eventType: CGEventType, event: CGEvent) {
        if eventType == .flagsChanged {
            optionDown = event.flags.contains(.maskAlternate)
            updateCurrentModifiers(from: event.flags)
        }
        if eventType == .leftMouseUp { mouseReleased(event) }
        if eventType == .mouseMoved || eventType == .leftMouseDragged { trackMovement(event) }
        if eventType == .keyDown { handleKeyDown(event) }
    }

    // MARK: - Keyboard Shortcut Handling
    private func handleKeyDown(_ event: CGEvent) {
        // Virtual key code for 'C' is 8 (kVK_ANSI_C)
        let cKeyCode: Int64 = 8
        let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
        if keyCode != cKeyCode { return }
        // Require Control + Shift (and NOT Command / Option) for explicitness
        let flags = event.flags
        let hasControl = flags.contains(.maskControl)
        let hasShift = flags.contains(.maskShift)
        if !(hasControl && hasShift) { return }
        // Ignore if Command or Option also pressed (avoid clashes with standard shortcuts)
        if flags.contains(.maskCommand) || flags.contains(.maskAlternate) { return }
        // Debounce: ignore autorepeat
        let isRepeat = event.getIntegerValueField(.keyboardEventAutorepeat) != 0
        if isRepeat { return }
        // Snapshot modifiers (derived from last flagsChanged + explicit control/shift)
        updateCurrentModifiers(from: flags)
        selectionModifiers = Array(currentModifiers).sorted()
        // Use current mouse position as anchor reference
        mouseUpPosition = legacyMouseTopLeftPoint()
        selectionAppPID = NSWorkspace.shared.frontmostApplication?.processIdentifier
        // Start the same multi-pass retrieval used for Option+Drag flow
        startRetrieval()
    }

    private func mouseReleased(_ _: CGEvent) {
        guard optionDown else { return }
        selectionAppPID = NSWorkspace.shared.frontmostApplication?.processIdentifier
        // Capture legacy top-left coordinate at mouse up
        mouseUpPosition = legacyMouseTopLeftPoint()
        // Snapshot modifiers at mouse up
        selectionModifiers = Array(currentModifiers).sorted()
        mouseUpTimer?.invalidate()
        mouseUpTimer = Timer.scheduledTimer(withTimeInterval: 0.03, repeats: false) { [weak self] _ in
            self?.startRetrieval()
        }
    }

    private func startRetrieval() {
        sequence &+= 1
        retrievalTimers.forEach { $0.cancel() }
        retrievalTimers.removeAll()
        for (i, delay) in passes.enumerated() {
            let seq = sequence
            let work = DispatchWorkItem { [weak self] in
                guard let self = self, self.sequence == seq else { return }
                self.attempt(passIndex: i)
            }
            retrievalTimers.append(work)
            DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
        }
    }

    private func attempt(passIndex: Int) {
        guard let pid = selectionAppPID ?? NSWorkspace.shared.frontmostApplication?.processIdentifier else { return }
        let appEl = AXUIElementCreateApplication(pid)
        var focusedRef: CFTypeRef?
        guard AXUIElementCopyAttributeValue(appEl, kAXFocusedUIElementAttribute as CFString, &focusedRef) == .success,
              let focused = focusedRef else {
            // Fallback: if we cannot get the focused element on the final pass,
            // mirror the existing clipboard fallback behavior.
            if passIndex == passes.count - 1 {
                if lastHadSelection {
                    emitEmptyIfNeeded()
                } else {
                    fputs("focused element not found, attempting clipboard fallback\n", stderr)
                    attemptClipboardFallback()
                }
            }
            return
        }
        let element = focused as! AXUIElement
        let editable = isEditableElement(element)
        if let text = readSelectionText(element: element) {
            emit(text: text, isEditable: editable)
        } else if passIndex == passes.count - 1 {
            // On the final pass, if we previously had a selection but now none is detected,
            // emit an empty signal to mirror the legacy behavior where a deselection is sent
            // once the selection collapses.
            if lastHadSelection { emitEmptyIfNeeded() }
            if !lastHadSelection {
                // We never had a selection across all passes
                fputs("selected is empty\n", stderr)
                // Clipboard fallback: issue Cmd+C then read pasteboard
                attemptClipboardFallback(isEditable: editable)
            }
        }
    }

    private func readSelectionText(element: AXUIElement) -> String? {
        for candidate in candidateElements(start: element) {
            if let text = selectionText(for: candidate) { return text }
        }
        return nil
    }

    private func candidateElements(start: AXUIElement) -> [AXUIElement] {
        var elements: [AXUIElement] = []
        var current: AXUIElement? = start
        var depth = 0
        while let el = current, depth < 6 {
            elements.append(el)
            current = parentElement(of: el)
            depth += 1
        }
        return elements
    }

    private func selectionText(for element: AXUIElement) -> String? {
        if let text = selectedTextAttributeString(element: element) { return text }
        if let text = selectedTextMarkerRangeString(element: element) { return text }
        if let text = selectedTextRangeValueString(element: element) { return text }
        if let text = selectedTextRangeParameterizedString(element: element) { return text }
        return nil
    }

    private func selectedTextAttributeString(element: AXUIElement) -> String? {
        var selRef: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, kAXSelectedTextAttribute as CFString, &selRef) == .success,
              let value = selRef,
              let text = stringValue(from: value),
              hasContent(text) else { return nil }
        return cleaned(text)
    }

    private func selectedTextMarkerRangeString(element: AXUIElement) -> String? {
        var rangeRef: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, "AXSelectedTextMarkerRange" as CFString, &rangeRef) == .success,
              let value = rangeRef else { return nil }
        return parameterizedString(element: element, attribute: "AXStringForTextMarkerRange" as CFString, value: value)
    }

    private func selectedTextRangeValueString(element: AXUIElement) -> String? {
        guard let rangeRef = selectedTextRangeRef(element: element),
              CFGetTypeID(rangeRef) == AXValueGetTypeID() else { return nil }
        let rangeValue = rangeRef as! AXValue
        guard AXValueGetType(rangeValue) == .cfRange else { return nil }
        var valueRef: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &valueRef) == .success,
              let value = valueRef,
              let full = stringValue(from: value),
              !full.isEmpty else { return nil }
        var r = CFRange(location: 0, length: 0)
        let ns = full as NSString
        if AXValueGetValue(rangeValue, .cfRange, &r), r.length > 0, r.location + r.length <= ns.length {
            let sub = ns.substring(with: NSRange(location: r.location, length: r.length))
            if hasContent(sub) { return cleaned(sub) }
        }
        return nil
    }

    private func selectedTextRangeParameterizedString(element: AXUIElement) -> String? {
        guard let rangeRef = selectedTextRangeRef(element: element) else { return nil }
        if CFGetTypeID(rangeRef) == AXValueGetTypeID() {
            let rangeValue = rangeRef as! AXValue
            if AXValueGetType(rangeValue) == .cfRange {
                if let text = stringForRange(element: element, rangeValue: rangeValue) { return text }
            } else if let text = stringForTextMarkerRange(element: element, rangeRef: rangeRef) {
                return text
            }
        } else if let text = stringForTextMarkerRange(element: element, rangeRef: rangeRef) {
            return text
        }
        return nil
    }

    private func selectedTextRangeRef(element: AXUIElement) -> CFTypeRef? {
        var rangeRef: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, kAXSelectedTextRangeAttribute as CFString, &rangeRef) == .success,
              let value = rangeRef else { return nil }
        return value
    }

    private func stringForRange(element: AXUIElement, rangeValue: AXValue) -> String? {
        var r = CFRange(location: 0, length: 0)
        guard AXValueGetValue(rangeValue, .cfRange, &r), r.length > 0 else { return nil }
        var copy = r
        guard let axVal = AXValueCreate(.cfRange, &copy) else { return nil }
        return parameterizedString(element: element, attribute: "AXStringForRange" as CFString, value: axVal)
    }

    private func stringForTextMarkerRange(element: AXUIElement, rangeRef: CFTypeRef) -> String? {
        return parameterizedString(element: element, attribute: "AXStringForTextMarkerRange" as CFString, value: rangeRef)
    }

    private func parameterizedString(element: AXUIElement, attribute: CFString, value: CFTypeRef) -> String? {
        var out: CFTypeRef?
        guard AXUIElementCopyParameterizedAttributeValue(element, attribute, value, &out) == .success,
              let output = out,
              let text = stringValue(from: output),
              hasContent(text) else { return nil }
        return cleaned(text)
    }

    private func stringValue(from value: CFTypeRef) -> String? {
        if let s = value as? String { return s }
        if CFGetTypeID(value) == CFAttributedStringGetTypeID(), let a = value as? NSAttributedString { return a.string }
        return nil
    }

    private func isEditableElement(_ element: AXUIElement) -> Bool {
        var current: AXUIElement? = element
        var depth = 0
        while let el = current, depth < 4 {
            if let explicit = boolAttribute(el, "AXEditable" as CFString) { return explicit }
            if let explicit = boolAttribute(el, "AXDOMEditable" as CFString) { return explicit }
            if isAttributeSettable(el, kAXSelectedTextRangeAttribute as CFString) { return true }
            if isAttributeSettable(el, kAXValueAttribute as CFString) { return true }
            if let role = stringAttribute(el, kAXRoleAttribute as CFString), editableRoles.contains(role) { return true }
            current = parentElement(of: el)
            depth += 1
        }
        return false
    }

    private func boolAttribute(_ element: AXUIElement, _ attribute: CFString) -> Bool? {
        var ref: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, attribute, &ref) == .success,
              let value = ref else { return nil }
        return boolValue(from: value)
    }

    private func boolValue(from value: CFTypeRef) -> Bool? {
        if let b = value as? Bool { return b }
        if let n = value as? NSNumber { return n.boolValue }
        if let s = value as? String {
            let normalized = s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
            if normalized == "true" || normalized == "yes" || normalized == "1" { return true }
            if normalized == "false" || normalized == "no" || normalized == "0" { return false }
        }
        return nil
    }

    private func stringAttribute(_ element: AXUIElement, _ attribute: CFString) -> String? {
        var ref: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, attribute, &ref) == .success,
              let value = ref else { return nil }
        return value as? String
    }

    private func parentElement(of element: AXUIElement) -> AXUIElement? {
        var ref: CFTypeRef?
        guard AXUIElementCopyAttributeValue(element, kAXParentAttribute as CFString, &ref) == .success,
              let value = ref,
              CFGetTypeID(value) == AXUIElementGetTypeID() else { return nil }
        return (value as! AXUIElement)
    }

    private func isAttributeSettable(_ element: AXUIElement, _ attribute: CFString) -> Bool {
        var settable = DarwinBoolean(false)
        let result = AXUIElementIsAttributeSettable(element, attribute, &settable)
        return result == .success && settable.boolValue
    }

    private func emit(text: String, isEditable: Bool) {
        guard hasContent(text) else {
            // Text provided but after cleaning it's empty
            fputs("selected is empty\n", stderr)
            return
        }
    let pos = mouseUpPosition ?? legacyMouseTopLeftPoint()
        var payload: [String: Any] = [
            "text": text,
            "position": ["x": pos.x, "y": pos.y],
            "isEditable": isEditable
        ]
    if !selectionModifiers.isEmpty { payload["modifiers"] = selectionModifiers }
        if let win = currentWindowInfo() { payload["window"] = win }
        guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]),
              let json = String(data: data, encoding: .utf8), json != lastSentJSON else { return }
        print(json)
        fflush(stdout)
        lastSentJSON = json
        lastHadSelection = true
    selectionAnchor = pos // use legacy top-left based capture point for movement anchoring
        selectionTimestamp = CFAbsoluteTimeGetCurrent()
        lastMovementCheckPos = selectionAnchor
        lastMovementCheckTime = selectionTimestamp
        retrievalTimers.forEach { $0.cancel() }
        retrievalTimers.removeAll()
    if let win = payload["window"] as? [String: Any] { lastWindowInfo = win }
    }

    private func cleaned(_ s: String) -> String { s.trimmingCharacters(in: .whitespacesAndNewlines) }
    private func hasContent(_ s: String) -> Bool {
        let stripped = s
            .replacingOccurrences(of: "\u{00A0}", with: " ")
            .replacingOccurrences(of: "\u{200B}", with: "")
            .replacingOccurrences(of: "\u{200C}", with: "")
            .replacingOccurrences(of: "\u{200D}", with: "")
            .replacingOccurrences(of: "\u{FEFF}", with: "")
        return !stripped.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    private func setupAXObserver() {
        if !AXIsProcessTrusted() {
            fputs("accessibility permission missing; enable Accessibility for Textwrench\n", stderr)
            return
        }
        guard let app = NSWorkspace.shared.frontmostApplication else { return }
        var observer: AXObserver?
        let pid = app.processIdentifier
        if AXObserverCreate(pid, { _,_,_,_ in }, &observer) == .success, let obs = observer {
            CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(obs), .commonModes)
        } else {
            fputs("failed to create AX observer; check Accessibility permissions\n", stderr)
        }
    }

    private func emitEmptyIfNeeded() {
        let pos = mouseUpPosition ?? legacyMouseTopLeftPoint()
        let payload: [String: Any] = [
            "text": "",
            "position": ["x": pos.x, "y": pos.y]
        ]
        if let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]),
           let json = String(data: data, encoding: .utf8), json != lastSentJSON {
            print(json)
            fflush(stdout)
            lastSentJSON = json
        }
        lastHadSelection = false
        selectionAnchor = nil
        selectionTimestamp = 0
        lastMovementCheckPos = nil
    mouseUpPosition = nil
    selectionModifiers = []
    }

    private func trackMovement(_ event: CGEvent) {
        guard lastHadSelection, let anchor = selectionAnchor else { return }
        let now = CFAbsoluteTimeGetCurrent()
        // Throttle checks
        if now - lastMovementCheckTime < movementCheckInterval { return }
        lastMovementCheckTime = now
        let current = legacyTopLeftPoint(for: event.location)
        lastMovementCheckPos = current
        // Enforce minimum lifetime so quick tiny adjustments don't cancel
        if now - selectionTimestamp < minLifetimeBeforeCancel { return }
        let dx = abs(current.x - anchor.x)
        let dy = abs(current.y - anchor.y)
        if dx > cancelDistanceX || dy > cancelDistanceY { emitEmptyIfNeeded() }
    }

    // Legacy top-left coordinate conversion (flips Y within screen bounds)
    private func legacyMouseTopLeftPoint() -> CGPoint {
        return legacyTopLeftPoint(for: NSEvent.mouseLocation)
    }

    private func legacyTopLeftPoint(for location: CGPoint) -> CGPoint {
        let primary = NSScreen.screens.first { $0.frame.origin == .zero } ?? NSScreen.main ?? NSScreen.screens.first
        if let f = primary?.frame {
            let flippedY = f.origin.y + f.size.height - location.y
            return CGPoint(x: location.x, y: flippedY)
        }
        // Fallback using primary screen height
        if let h = NSScreen.main?.frame.height {
            return CGPoint(x: location.x, y: h - location.y)
        }
        return location
    }

    // MARK: - Modifier Handling
    private func updateCurrentModifiers(from flags: CGEventFlags) {
        var mods: Set<String> = []
        if flags.contains(.maskShift) { mods.insert("shift") }
        if flags.contains(.maskControl) { mods.insert("control") }
        if flags.contains(.maskAlternate) { mods.insert("option") }
        if flags.contains(.maskCommand) { mods.insert("command") }
        currentModifiers = mods
    }

    // MARK: - Window Info (minimal)
    private func currentWindowInfo() -> [String: Any]? {
        guard let pid = selectionAppPID ?? NSWorkspace.shared.frontmostApplication?.processIdentifier else { return nil }
        let app = NSRunningApplication(processIdentifier: pid)
        var info: [String: Any] = [
            "appName": app?.localizedName ?? "unknown",
            "appPID": pid,
            "windowTitle": ""
        ]
        let appEl = AXUIElementCreateApplication(pid)
        var mainWindowRef: CFTypeRef?
        if AXUIElementCopyAttributeValue(appEl, kAXMainWindowAttribute as CFString, &mainWindowRef) == .success,
           let win = mainWindowRef {
            let winEl = win as! AXUIElement
            var titleRef: CFTypeRef?
            if AXUIElementCopyAttributeValue(winEl, kAXTitleAttribute as CFString, &titleRef) == .success,
               let t = titleRef as? String { info["windowTitle"] = t }
        }
        return info
    }

    // MARK: - Paste Handling (simplified)
    private func listenForProcessedText() {
        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            while let line = readLine() {
                DispatchQueue.main.async { self?.handleIncomingText(line) }
            }
        }
    }

    private func handleIncomingText(_ text: String) {
        // Accept either plain text or { text:..., appPID:... }
        guard let data = text.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            copyAndPaste(text: text)
            return
        }
        let processedText = (json["text"] as? String) ?? text
        let pid = json["appPID"] as? pid_t
        copyAndPaste(text: processedText, targetAppPID: pid)
    }

    private func copyAndPaste(text: String, targetAppPID: pid_t? = nil) {
        // Preserve user's clipboard
        let snapshot = snapshotClipboard()
        copyToClipboard(text)
        performPaste(targetAppPID: targetAppPID ?? (lastWindowInfo?["appPID"] as? pid_t), restoreClipboard: snapshot)
    }

    private func copyToClipboard(_ text: String) {
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(text, forType: .string)
    }

    private func performPaste(targetAppPID: pid_t?, restoreClipboard snapshot: ClipboardSnapshot?) {
        guard let pid = targetAppPID else {
            // Fallback: just issue paste globally
            issuePasteKeystroke()
            // Restore clipboard shortly after paste completes
            if let snap = snapshot {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
                    self?.restoreClipboard(snap)
                }
            }
            return
        }
        if let tap = eventTap { CGEvent.tapEnable(tap: tap, enable: false) }
        // Clear selection popup state
        emitEmptyIfNeeded()
        // Activate and focus target app
        activateApplication(pid: pid)
        focusMainWindow(pid: pid)
        // Wait briefly for focus to settle
        waitForFrontmostApp(pid: pid)
        issuePasteKeystroke()
        // Re-enable tap after slight delay
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
            if let tap = self?.eventTap { CGEvent.tapEnable(tap: tap, enable: true) }
        }
        // Restore clipboard after paste finishes
        if let snap = snapshot {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
                self?.restoreClipboard(snap)
            }
        }
    }

    private func issuePasteKeystroke() {
        let source = CGEventSource(stateID: .hidSystemState)
        let tapLoc = CGEventTapLocation.cghidEventTap
        let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: true)
        keyDown?.flags = .maskCommand
        let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: false)
        keyUp?.flags = .maskCommand
        keyDown?.post(tap: tapLoc)
        usleep(30_000)
        keyUp?.post(tap: tapLoc)
    }

    private func waitForFrontmostApp(pid: pid_t) {
        let timeout: TimeInterval = 0.6
        let start = CFAbsoluteTimeGetCurrent()
        while CFAbsoluteTimeGetCurrent() - start < timeout {
            if NSWorkspace.shared.frontmostApplication?.processIdentifier == pid {
                return
            }
            usleep(30_000)
        }
        // Fallback to a short grace period if activation is slow
        usleep(60_000)
    }

    private func issueCopyKeystroke() {
        let source = CGEventSource(stateID: .hidSystemState)
        let tapLoc = CGEventTapLocation.cghidEventTap
        // 'C' key virtual code: 8 (kVK_ANSI_C)
        let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 8, keyDown: true)
        keyDown?.flags = .maskCommand
        let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 8, keyDown: false)
        keyUp?.flags = .maskCommand
        keyDown?.post(tap: tapLoc)
        usleep(30_000)
        keyUp?.post(tap: tapLoc)
    }

    private func attemptClipboardFallback(isEditable: Bool? = nil) {
        // Preserve clipboard, then attempt Cmd+C and read
        let snapshot = snapshotClipboard()
        let pb = NSPasteboard.general
        let beforeChangeCount = pb.changeCount
        let beforeString = pb.string(forType: .string)
        if let tap = eventTap { CGEvent.tapEnable(tap: tap, enable: false) }
        issueCopyKeystroke()
        // Allow a short delay for target app to update clipboard
        usleep(90_000)
        let afterChangeCount = pb.changeCount
        let afterString = pb.string(forType: .string)
        if afterChangeCount == beforeChangeCount && afterString == beforeString {
            fputs("clipboard unchanged after copy\n", stderr)
        } else if let clip = afterString, hasContent(clip) {
            // Emit clipboard text as if selected (isEditable may be inferred)
            emit(text: cleaned(clip), isEditable: isEditable ?? false)
        } else {
            fputs("clipboard also empty\n", stderr)
        }
        // Restore user's clipboard
        restoreClipboard(snapshot)
        if let tap = eventTap { CGEvent.tapEnable(tap: tap, enable: true) }
    }

    // MARK: - Clipboard preservation helpers
    private typealias ClipboardSnapshot = [[String: Data]]

    private func snapshotClipboard() -> ClipboardSnapshot {
        let pb = NSPasteboard.general
        guard let items = pb.pasteboardItems, !items.isEmpty else { return [] }
        return items.map { item in
            var dict: [String: Data] = [:]
            for t in item.types {
                if let data = item.data(forType: t) {
                    dict[t.rawValue] = data
                }
            }
            return dict
        }
    }

    private func restoreClipboard(_ snapshot: ClipboardSnapshot) {
        let pb = NSPasteboard.general
        pb.clearContents()
        guard !snapshot.isEmpty else { return }
        let items: [NSPasteboardItem] = snapshot.map { dict in
            let i = NSPasteboardItem()
            for (uti, data) in dict {
                i.setData(data, forType: NSPasteboard.PasteboardType(rawValue: uti))
            }
            return i
        }
        _ = pb.writeObjects(items as [NSPasteboardWriting])
    }

    private func activateApplication(pid: pid_t) {
        guard let app = NSRunningApplication(processIdentifier: pid) else { return }
        if !app.activate(options: [.activateAllWindows]) {
            let script = """
            tell application "System Events"
                set frontmost of process whose unix id is \(pid) to true
            end tell
            """
            let task = Process()
            task.launchPath = "/usr/bin/osascript"
            task.arguments = ["-e", script]
            try? task.run()
            task.waitUntilExit()
        }
    }

    private func focusMainWindow(pid: pid_t) {
        let appRef = AXUIElementCreateApplication(pid)
        var windowRef: CFTypeRef?
        if AXUIElementCopyAttributeValue(appRef, kAXMainWindowAttribute as CFString, &windowRef) == .success,
           let win = windowRef {
            let windowEl = win as! AXUIElement
            AXUIElementSetAttributeValue(windowEl, kAXMainAttribute as CFString, kCFBooleanTrue)
            AXUIElementSetAttributeValue(windowEl, kAXFocusedAttribute as CFString, kCFBooleanTrue)
            usleep(150_000)
        }
    }
}

let observer = SelectionObserver()
observer.run()
