import AppKit  // Added AppKit for NSWorkspace
import ApplicationServices  // For AXUIElement and Accessibility APIs
import CoreAudio  // For audio control
import Foundation

// Represents a node in the accessibility tree. Must be Codable to be sent via RPC.
struct AccessibilityElementNode: Codable {
    // Basic properties - expand as needed
    let role: String?
    let description: String?  // Corresponds to AXDescription
    let title: String?  // Corresponds to AXTitle
    let value: String?  // Corresponds to AXValue (might need to be AnyCodable or specific types)
    let identifier: String?  // Corresponds to AXIdentifier (often not set)
    // let frame: CGRect?    // CGRect is not directly Codable, would need a wrapper or separate fields
    let children: [AccessibilityElementNode]?

    // Example for frame if you want to include it:
    struct CodableRect: Codable {
        let x: Double
        let y: Double
        let width: Double
        let height: Double

        init?(rect: CGRect?) {
            guard let rect = rect else { return nil }
            self.x = Double(rect.origin.x)
            self.y = Double(rect.origin.y)
            self.width = Double(rect.size.width)
            self.height = Double(rect.size.height)
        }
    }
    // let codableFrame: CodableRect?

    // Initializer for convenience (internal use during tree construction)
    init(
        role: String?, description: String?, title: String?, value: String?, identifier: String?,
        children: [AccessibilityElementNode]?
    ) {
        self.role = role
        self.description = description
        self.title = title
        self.value = value
        self.identifier = identifier
        self.children = children
        // self.codableFrame = CodableRect(rect: frame) // If using frame
    }
}

class AccessibilityService {

    private let maxDepth = 10  // To prevent excessively deep recursion and large payloads
    private let dateFormatter: DateFormatter

    // Properties to store original audio states
    private var originalSystemMuteState: Bool?
    private var originalSystemVolume: Float32?

    init() {
        self.dateFormatter = DateFormatter()
        self.dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
    }

    private func logToStderr(_ message: String) {
        let timestamp = dateFormatter.string(from: Date())
        let logMessage = "[\(timestamp)] \(message)\n"
        FileHandle.standardError.write(logMessage.data(using: .utf8)!)
    }

    // Fetches a value for a given accessibility attribute from an element.
    private func getAttributeValue(element: AXUIElement, attribute: String) -> String? {
        var value: AnyObject?
        let error = AXUIElementCopyAttributeValue(element, attribute as CFString, &value)
        if error == .success, let strValue = value as? String {
            return strValue
        }
        // Could also handle other types like AXValue (numbers, bools) if needed
        return nil
    }

    // Fetches children of an accessibility element.
    private func getChildren(element: AXUIElement) -> [AXUIElement]? {
        var value: AnyObject?
        let error = AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value)
        if error == .success, let children = value as? [AXUIElement] {
            return children
        }
        return nil
    }

    // MARK: - Audio Control Helpers
    private func getDefaultOutputDeviceID() -> AudioDeviceID? {
        var deviceID: AudioDeviceID = kAudioObjectUnknown
        var propertyAddress = AudioObjectPropertyAddress(
            mSelector: kAudioHardwarePropertyDefaultOutputDevice,
            mScope: kAudioObjectPropertyScopeGlobal,
            mElement: kAudioObjectPropertyElementMain
        )
        var propertySize = UInt32(MemoryLayout<AudioDeviceID>.size)

        let status = AudioObjectGetPropertyData(
            AudioObjectID(kAudioObjectSystemObject),
            &propertyAddress,
            0,
            nil,
            &propertySize,
            &deviceID
        )

        if status == noErr && deviceID != kAudioObjectUnknown {
            return deviceID
        } else {
            logToStderr("[AccessibilityService] Error getting default output device: \(status).")
            return nil
        }
    }

    private func isDeviceMuted(deviceID: AudioDeviceID) -> Bool? {
        var isMuted: UInt32 = 0
        var propertyAddress = AudioObjectPropertyAddress(
            mSelector: kAudioDevicePropertyMute,
            mScope: kAudioDevicePropertyScopeOutput,
            mElement: kAudioObjectPropertyElementMain  // Master channel
        )
        var propertySize = UInt32(MemoryLayout<UInt32>.size)

        var isSettable: DarwinBoolean = false
        let infoStatus = AudioObjectIsPropertySettable(deviceID, &propertyAddress, &isSettable)
        if infoStatus != noErr || !isSettable.boolValue {
            logToStderr(
                "[AccessibilityService] Mute property not supported or not settable for device \(deviceID)."
            )
            return nil
        }

        let status = AudioObjectGetPropertyData(
            deviceID,
            &propertyAddress,
            0,
            nil,
            &propertySize,
            &isMuted
        )

        if status == noErr {
            return isMuted == 1
        } else {
            logToStderr(
                "[AccessibilityService] Error getting mute state for device \(deviceID): \(status)."
            )
            return nil
        }
    }

    private func setDeviceMute(deviceID: AudioDeviceID, mute: Bool) -> OSStatus {
        var muteVal: UInt32 = mute ? 1 : 0
        var propertyAddress = AudioObjectPropertyAddress(
            mSelector: kAudioDevicePropertyMute,
            mScope: kAudioDevicePropertyScopeOutput,
            mElement: kAudioObjectPropertyElementMain  // Master channel
        )
        let propertySize = UInt32(MemoryLayout<UInt32>.size)

        var isSettable: DarwinBoolean = false
        let infoStatus = AudioObjectIsPropertySettable(deviceID, &propertyAddress, &isSettable)
        if infoStatus != noErr {
            logToStderr(
                "[AccessibilityService] Error checking if mute is settable for device \(deviceID): \(infoStatus)."
            )
            return infoStatus
        }
        if !isSettable.boolValue {
            logToStderr(
                "[AccessibilityService] Mute property is not settable for device \(deviceID).")
            return kAudioHardwareUnsupportedOperationError
        }

        let status = AudioObjectSetPropertyData(
            deviceID,
            &propertyAddress,
            0,
            nil,
            propertySize,
            &muteVal
        )
        if status != noErr {
            logToStderr(
                "[AccessibilityService] Error setting mute state for device \(deviceID) to \(mute): \(status)."
            )
        }
        return status
    }

    private func getDeviceVolume(deviceID: AudioDeviceID) -> Float32? {
        var volume: Float32 = 0.0
        var propertyAddress = AudioObjectPropertyAddress(
            mSelector: kAudioDevicePropertyVolumeScalar,
            mScope: kAudioDevicePropertyScopeOutput,
            mElement: kAudioObjectPropertyElementMain  // Master channel
        )
        var propertySize = UInt32(MemoryLayout<Float32>.size)

        if AudioObjectHasProperty(deviceID, &propertyAddress) == false {
            logToStderr(
                "[AccessibilityService] Volume scalar property not supported for device \(deviceID)."
            )
            return nil
        }

        let status = AudioObjectGetPropertyData(
            deviceID,
            &propertyAddress,
            0,
            nil,
            &propertySize,
            &volume
        )

        if status == noErr {
            return volume
        } else {
            logToStderr(
                "[AccessibilityService] Error getting volume for device \(deviceID): \(status).")
            return nil
        }
    }

    private func setDeviceVolume(deviceID: AudioDeviceID, volume: Float32) -> OSStatus {
        var newVolume = min(max(volume, 0.0), 1.0)  // Clamp volume to 0.0-1.0
        var propertyAddress = AudioObjectPropertyAddress(
            mSelector: kAudioDevicePropertyVolumeScalar,
            mScope: kAudioDevicePropertyScopeOutput,
            mElement: kAudioObjectPropertyElementMain  // Master channel
        )
        let propertySize = UInt32(MemoryLayout<Float32>.size)

        var isSettable: DarwinBoolean = false
        let infoStatus = AudioObjectIsPropertySettable(deviceID, &propertyAddress, &isSettable)
        if infoStatus != noErr {
            logToStderr(
                "[AccessibilityService] Error checking if volume is settable for device \(deviceID): \(infoStatus)."
            )
            return infoStatus
        }
        if !isSettable.boolValue {
            logToStderr(
                "[AccessibilityService] Volume property is not settable for device \(deviceID).")
            return kAudioHardwareUnsupportedOperationError
        }

        let status = AudioObjectSetPropertyData(
            deviceID,
            &propertyAddress,
            0,
            nil,
            propertySize,
            &newVolume
        )
        if status != noErr {
            logToStderr(
                "[AccessibilityService] Error setting volume for device \(deviceID) to \(newVolume): \(status)."
            )
        }
        return status
    }

    // Recursive function to build the tree from a given AXUIElement
    func buildTree(fromElement element: AXUIElement, currentDepth: Int) -> AccessibilityElementNode?
    {
        if currentDepth > maxDepth {
            // Return a placeholder or nil if max depth is exceeded
            return AccessibilityElementNode(
                role: "DepthLimitExceeded", description: "Max recursion depth reached", title: nil,
                value: nil, identifier: nil, children: nil)
        }

        let role = getAttributeValue(element: element, attribute: kAXRoleAttribute)
        let description = getAttributeValue(element: element, attribute: kAXDescriptionAttribute)
        let title = getAttributeValue(element: element, attribute: kAXTitleAttribute)
        let value = getAttributeValue(element: element, attribute: kAXValueAttribute)
        let identifier = getAttributeValue(element: element, attribute: kAXIdentifierAttribute)
        // Add more attributes as needed (e.g., kAXFrameAttribute, kAXEnabledAttribute)

        var childNodes: [AccessibilityElementNode]? = nil
        if let axChildren = getChildren(element: element) {
            childNodes = []  // Initialize if there are children to process
            for childElement in axChildren {
                if let childNode = buildTree(
                    fromElement: childElement, currentDepth: currentDepth + 1)
                {
                    childNodes?.append(childNode)
                }
            }
            if childNodes?.isEmpty ?? true {  // If loop completed but no valid childNodes were added
                childNodes = nil
            }
        }

        // Only create a node if it has some meaningful data or children
        // This helps to avoid empty nodes for elements that might not be relevant
        if role != nil || description != nil || title != nil || value != nil || identifier != nil
            || (childNodes != nil && !childNodes!.isEmpty)
        {
            return AccessibilityElementNode(
                role: role,
                description: description,
                title: title,
                value: value,
                identifier: identifier,
                children: childNodes
            )
        }
        return nil
    }

    // Public method to fetch the entire accessibility tree for the system or a specific app.
    // For `rootId`: if nil, gets system-wide. If "focused", gets the focused application.
    // Otherwise, it could be a bundle identifier (not implemented here yet).
    public func fetchFullAccessibilityTree(rootId: String?) -> AccessibilityElementNode? {
        logToStderr(
            "[AccessibilityService] Starting fetchFullAccessibilityTree. rootId: \(rootId ?? "nil")"
        )

        var rootElement: AXUIElement?

        if let id = rootId, id.lowercased() == "focusedapp" {
            // Get the focused application
            guard let frontmostApp = NSWorkspace.shared.frontmostApplication else {
                logToStderr("[AccessibilityService] Could not get frontmost application.")
                return nil
            }
            rootElement = AXUIElementCreateApplication(frontmostApp.processIdentifier)
            logToStderr(
                "[AccessibilityService] Targeting focused app: \(frontmostApp.localizedName ?? "Unknown App") (PID: \(frontmostApp.processIdentifier))"
            )
        } else if let id = rootId, !id.isEmpty {
            // Basic PID lookup if rootId is a number (representing a PID)
            // More robust app lookup by bundle ID would be better for non-PID rootIds.
            if let pid = Int32(id) {
                rootElement = AXUIElementCreateApplication(pid)
                logToStderr("[AccessibilityService] Targeting PID: \(pid)")
            } else {
                logToStderr(
                    "[AccessibilityService] rootId '\(id)' is not 'focusedapp' or a valid PID. Falling back to system-wide (or implement bundle ID lookup)."
                )
                // Fallback or specific error for unhandled rootId format
                // For now, let's try system-wide if rootId isn't 'focusedapp' or PID.
                rootElement = AXUIElementCreateSystemWide()
                logToStderr(
                    "[AccessibilityService] Defaulting to system-wide due to unhandled rootId.")
            }
        } else {
            // Default to system-wide if rootId is nil or empty
            rootElement = AXUIElementCreateSystemWide()
            logToStderr("[AccessibilityService] Targeting system-wide accessibility tree.")
        }

        guard let element = rootElement else {
            logToStderr("[AccessibilityService] Failed to create root AXUIElement.")
            return nil
        }

        let tree = buildTree(fromElement: element, currentDepth: 0)
        logToStderr(
            "[AccessibilityService] Finished buildTree. Result is \(tree == nil ? "nil" : "not nil")."
        )
        return tree
    }

    // MARK: - System Audio Control

    public func muteSystemAudio() -> Bool {
        logToStderr("[AccessibilityService] Attempting to mute system audio.")
        guard let deviceID = getDefaultOutputDeviceID() else {
            logToStderr("[AccessibilityService] Could not get default output device to mute audio.")
            return false
        }

        // Store original state
        self.originalSystemMuteState = isDeviceMuted(deviceID: deviceID)
        self.originalSystemVolume = getDeviceVolume(deviceID: deviceID)

        logToStderr(
            "[AccessibilityService] Original mute state: \(String(describing: self.originalSystemMuteState)), Original volume: \(String(describing: self.originalSystemVolume))."
        )

        // Attempt to mute
        let muteStatus = setDeviceMute(deviceID: deviceID, mute: true)
        if muteStatus == noErr {
            logToStderr("[AccessibilityService] System audio muted successfully via mute property.")
            return true
        } else {
            logToStderr(
                "[AccessibilityService] Failed to set mute property (status: \(muteStatus)). Attempting to set volume to 0."
            )
            let volumeStatus = setDeviceVolume(deviceID: deviceID, volume: 0.0)
            if volumeStatus == noErr {
                logToStderr("[AccessibilityService] System audio silenced by setting volume to 0.")
            } else {
                logToStderr(
                    "[AccessibilityService] Failed to silence system audio by setting volume to 0 (status: \(volumeStatus))."
                )
            }
            return false
        }
    }

    public func restoreSystemAudio() -> Bool {
        logToStderr("[AccessibilityService] Attempting to restore system audio.")
        guard let deviceID = getDefaultOutputDeviceID() else {
            logToStderr(
                "[AccessibilityService] Could not get default output device to restore audio.")
            return false
        }

        if let originalMute = self.originalSystemMuteState {
            let muteStatus = setDeviceMute(deviceID: deviceID, mute: originalMute)
            if muteStatus == noErr {
                logToStderr("[AccessibilityService] System mute state restored to \(originalMute).")
            } else {
                logToStderr(
                    "[AccessibilityService] Failed to restore original mute state (status: \(muteStatus))."
                )
            }
        }

        let shouldRestoreVolume =
            self.originalSystemVolume != nil
            && (self.originalSystemMuteState == false || self.originalSystemMuteState == nil)

        if shouldRestoreVolume, let originalVolume = self.originalSystemVolume {
            let volumeStatus = setDeviceVolume(deviceID: deviceID, volume: originalVolume)
            if volumeStatus == noErr {
                logToStderr("[AccessibilityService] System volume restored to \(originalVolume).")
            } else {
                logToStderr(
                    "[AccessibilityService] Failed to restore original volume (status: \(volumeStatus))."
                )
            }
        }

        self.originalSystemMuteState = nil
        self.originalSystemVolume = nil
        logToStderr(
            "[AccessibilityService] System audio restoration attempt complete. Stored states cleared."
        )
        return true
    }

    // Pastes the given text into the active application
    public func pasteText(transcript: String) -> Bool {
        logToStderr("[AccessibilityService] Attempting to paste transcript: \(transcript).")

        let pasteboard = NSPasteboard.general
        let originalPasteboardItems =
            pasteboard.pasteboardItems?.compactMap { item -> NSPasteboardItem? in
                let newItem = NSPasteboardItem()
                var hasData = false
                for type in item.types ?? [] {
                    if let data = item.data(forType: type) {
                        newItem.setData(data, forType: type)
                        hasData = true
                    }
                }
                return hasData ? newItem : nil
            } ?? []

        let originalChangeCount = pasteboard.changeCount  // Save change count to detect external modifications

        pasteboard.clearContents()
        let success = pasteboard.setString(transcript, forType: .string)

        if !success {
            logToStderr("[AccessibilityService] Failed to set string on pasteboard.")
            // Restore original content before returning
            restorePasteboard(
                pasteboard: pasteboard, items: originalPasteboardItems,
                originalChangeCount: originalChangeCount)
            return false
        }

        // Simulate Cmd+V
        // Using deprecated kVK_Command might still work but kCGEventFlagMaskCommand is preferred.
        // Virtual key code for 'v' is 9.
        let vKeyCode: CGKeyCode = 9

        let source = CGEventSource(stateID: .hidSystemState)

        let cmdDown = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(55), keyDown: true)  // 55 is kVK_Command
        cmdDown?.flags = .maskCommand

        let vDown = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: true)
        vDown?.flags = .maskCommand  // Keep command flag for the V press as well

        let vUp = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: false)
        vUp?.flags = .maskCommand

        let cmdUp = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(55), keyDown: false)
        // No flags needed for key up typically, or just .maskCommand if it was held

        if cmdDown == nil || vDown == nil || vUp == nil || cmdUp == nil {
            logToStderr("[AccessibilityService] Failed to create CGEvent for paste.")
            restorePasteboard(
                pasteboard: pasteboard, items: originalPasteboardItems,
                originalChangeCount: originalChangeCount)
            return false
        }

        let loc: CGEventTapLocation = .cgSessionEventTap

        cmdDown!.post(tap: loc)
        vDown!.post(tap: loc)
        vUp!.post(tap: loc)
        cmdUp!.post(tap: loc)

        logToStderr("[AccessibilityService] Paste keyboard events posted.")

        // Restore the original pasteboard content after a short delay
        // to allow the paste action to complete.
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {  // 200ms delay
            self.restorePasteboard(
                pasteboard: pasteboard, items: originalPasteboardItems,
                originalChangeCount: originalChangeCount)
        }

        return true
    }

    private func restorePasteboard(
        pasteboard: NSPasteboard, items: [NSPasteboardItem], originalChangeCount: Int
    ) {
        // Only restore if our temporary content is still the active content on the pasteboard.
        // This means the changeCount should be exactly one greater than when we saved it,
        // indicating our setString operation was the last modification.
        if pasteboard.changeCount == originalChangeCount + 1 {
            pasteboard.clearContents()
            if !items.isEmpty {
                pasteboard.writeObjects(items)
            }
            logToStderr("[AccessibilityService] Original pasteboard content restored.")
        } else {
            // If changeCount is different, it means another app or the user has modified the pasteboard
            // after we set our transcript but before this restoration block was executed.
            // In this case, we should not interfere with the new pasteboard content.
            logToStderr(
                "[AccessibilityService] Pasteboard changed by another process or a new copy occurred (expected changeCount: \(originalChangeCount + 1), current: \(pasteboard.changeCount)); not restoring original content to avoid conflict."
            )
        }
    }

    // Define kVK_Function if not available from a system framework directly in this context.
    // 0x3F is the virtual key code for the Fn key on Apple keyboards.
    private let kVK_Function: CGKeyCode = 0x3F

    // Determines whether a keyboard event should be forwarded to the Electron application.
    // This method should be called from the CGEventTap callback in main.swift or RpcHandler.swift.
    public func shouldForwardKeyboardEvent(event: CGEvent) -> Bool {
        let type = event.type
        let keyCode = CGKeyCode(event.getIntegerValueField(.keyboardEventKeycode))

        // Uncomment for verbose logging from Swift helper:
        // logToStderr("[AccessibilityService] shouldForwardKeyboardEvent: type=\(type.rawValue), keyCode=\(keyCode), flags=\(event.flags.rawValue)")

        if type == .flagsChanged {
            // Always forward flagsChanged events. These are crucial for Electron to know
            // the state of modifier keys, including when the Fn key itself is pressed or released,
            // which is used to control recording.
            // logToStderr("[AccessibilityService] Forwarding flagsChanged event.")
            return true
        }

        if type == .keyDown || type == .keyUp {
            // For keyDown and keyUp events, only forward if the event is FOR THE Fn KEY ITSELF.
            if keyCode == kVK_Function {
                // logToStderr("[AccessibilityService] Forwarding \(type == .keyDown ? "keyDown" : "keyUp") event because it IS the Fn key (keyCode: \(keyCode)).")
                return true
            } else {
                // logToStderr("[AccessibilityService] Suppressing \(type == .keyDown ? "keyDown" : "keyUp") event for keyCode \(keyCode) because it is NOT the Fn key.")
                return false
            }
        }

        // For any other event types (e.g., mouse events, system-defined), don't forward by default.
        // logToStderr("[AccessibilityService] Suppressing event of unhandled type: \(type.rawValue).")
        return false
    }
}
