import AppKit
import ApplicationServices
import Foundation

struct MatchDescriptor {
    let identifier: String
    let rect: CGRect?
    let hasParentWindow: Bool
}

enum AXReadFailure: Error {
    case nonBenign(AXError)
}

func isBenignAbsence(_ error: AXError) -> Bool {
    error == .noValue || error == .attributeUnsupported
}

func completeMatches(_ result: Result<[MatchDescriptor], AXReadFailure>) throws -> [MatchDescriptor] {
    try result.get()
}

func validRect(_ rect: CGRect?) -> Bool {
    guard let rect else { return false }
    return [rect.origin.x, rect.origin.y, rect.width, rect.height].allSatisfy(\.isFinite)
        && rect.width > 0 && rect.height > 0
}

func postMutationReadbackExit(
    _ readback: Result<Bool, AXReadFailure>,
    validationFailure: Int32,
    cleanupFailure: Int32,
    rollback: () -> Bool
) -> Int32? {
    if case .success(true) = readback { return nil }
    return rollback() ? validationFailure : cleanupFailure
}

func uniqueMatch(_ matches: [MatchDescriptor], expected: String) -> MatchDescriptor? {
    let exact = matches.filter { $0.identifier == expected }
    guard exact.count == 1,
          exact[0].hasParentWindow,
          validRect(exact[0].rect) else { return nil }
    return exact[0]
}

func selfTest() -> Int32 {
    let good = MatchDescriptor(identifier: "fixture.excalidraw", rect: CGRect(x: 1, y: 2, width: 3, height: 4), hasParentWindow: true)
    let traversalErrorRejected = (try? completeMatches(.failure(.nonBenign(.cannotComplete)))) == nil
    let invalidParentRects: [CGRect] = [
        CGRect(x: 0, y: 0, width: 0, height: 1),
        CGRect(x: 0, y: 0, width: 1, height: 0),
        CGRect(x: CGFloat.nan, y: 0, width: 1, height: 1),
        CGRect(x: 0, y: CGFloat.infinity, width: 1, height: 1),
    ]
    var rollbackCalled = 0
    let readErrorCleanupSuccess = postMutationReadbackExit(
        .failure(.nonBenign(.cannotComplete)), validationFailure: 69, cleanupFailure: 71
    ) { rollbackCalled += 1; return true }
    let readErrorCleanupFailure = postMutationReadbackExit(
        .failure(.nonBenign(.cannotComplete)), validationFailure: 69, cleanupFailure: 71
    ) { rollbackCalled += 1; return false }
    guard uniqueMatch([good], expected: "fixture.excalidraw") != nil,
          uniqueMatch([good], expected: "fixture") == nil,
          uniqueMatch([good, good], expected: "fixture.excalidraw") == nil,
          uniqueMatch([MatchDescriptor(identifier: good.identifier, rect: .zero, hasParentWindow: true)], expected: good.identifier) == nil,
          uniqueMatch([MatchDescriptor(identifier: good.identifier, rect: good.rect, hasParentWindow: false)], expected: good.identifier) == nil,
          traversalErrorRejected,
          isBenignAbsence(.noValue), isBenignAbsence(.attributeUnsupported),
          !isBenignAbsence(.cannotComplete), !isBenignAbsence(.invalidUIElement), !isBenignAbsence(.apiDisabled),
          (invalidParentRects.allSatisfy { !validRect($0) }),
          rollbackCalled == 2, readErrorCleanupSuccess == 69, readErrorCleanupFailure == 71,
          postMutationReadbackExit(.success(true), validationFailure: 69, cleanupFailure: 71, rollback: { false }) == nil else { return 1 }
    print("self_test=PASS explicit_exact_unique_source=true traversal_read_error_rejected=true parent_geometry_rejected=true post_mutation_read_error_rollback=true cleanup_status_distinct=true")
    return 0
}

if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] { exit(selfTest()) }
guard CommandLine.arguments.count == 2, !CommandLine.arguments[1].isEmpty else {
    fputs("usage: PlaceFinderSource exactSourceIdentifier\n", stderr)
    exit(64)
}
let expected = CommandLine.arguments[1]

func attribute(_ element: AXUIElement, _ name: String) throws -> CFTypeRef? {
    var value: CFTypeRef?
    let error = AXUIElementCopyAttributeValue(element, name as CFString, &value)
    if error == .success { return value }
    if isBenignAbsence(error) { return nil }
    throw AXReadFailure.nonBenign(error)
}
func stringValue(_ element: AXUIElement, _ name: String) throws -> String? {
    guard let value = try attribute(element, name), CFGetTypeID(value) == CFStringGetTypeID() else { return nil }
    return value as? String
}
func rect(_ element: AXUIElement) throws -> CGRect? {
    guard let pv = try attribute(element, kAXPositionAttribute), CFGetTypeID(pv) == AXValueGetTypeID(),
          let sv = try attribute(element, kAXSizeAttribute), CFGetTypeID(sv) == AXValueGetTypeID() else { return nil }
    var point = CGPoint.zero
    var size = CGSize.zero
    guard AXValueGetValue(pv as! AXValue, .cgPoint, &point), AXValueGetValue(sv as! AXValue, .cgSize, &size) else { return nil }
    return CGRect(origin: point, size: size)
}
func parentWindow(_ element: AXUIElement) throws -> AXUIElement? {
    var current = element
    for _ in 0..<16 {
        if try stringValue(current, kAXRoleAttribute) == (kAXWindowRole as String) { return current }
        guard let parent = try attribute(current, kAXParentAttribute), CFGetTypeID(parent) == AXUIElementGetTypeID() else { return nil }
        current = parent as! AXUIElement
    }
    return nil
}
func collect(_ element: AXUIElement, depth: Int = 0, into result: inout [AXUIElement]) throws {
    if try stringValue(element, kAXRoleAttribute) == (kAXTextFieldRole as String),
       try stringValue(element, kAXValueAttribute) == expected { result.append(element) }
    guard depth < 14, let children = try attribute(element, kAXChildrenAttribute) as? [AXUIElement] else { return }
    for child in children { try collect(child, depth: depth + 1, into: &result) }
}
func setPoint(_ element: AXUIElement, _ value: CGPoint) -> AXError {
    var value = value
    return AXUIElementSetAttributeValue(element, kAXPositionAttribute as CFString, AXValueCreate(.cgPoint, &value)!)
}
func setSize(_ element: AXUIElement, _ value: CGSize) -> AXError {
    var value = value
    return AXUIElementSetAttributeValue(element, kAXSizeAttribute as CFString, AXValueCreate(.cgSize, &value)!)
}

guard let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else { exit(66) }
let app = AXUIElementCreateApplication(finder.processIdentifier)
var matches: [AXUIElement] = []
try collect(app, into: &matches)
let descriptors = try completeMatches(.success(try matches.map { element in
    MatchDescriptor(
        identifier: try stringValue(element, kAXValueAttribute) ?? "",
        rect: try rect(element),
        hasParentWindow: try parentWindow(element) != nil
    )
}))
guard matches.count == 1,
      uniqueMatch(descriptors, expected: expected) != nil,
      let window = try parentWindow(matches[0]),
      let originalWindowRect = try rect(window), validRect(originalWindowRect),
      let sourceRectBefore = try rect(matches[0]), validRect(sourceRectBefore) else {
    fputs("exact unique source binding failed\n", stderr)
    exit(67)
}

let desiredPosition = CGPoint(x: 10, y: 70)
let desiredSize = CGSize(width: 500, height: 320)
let positionError = setPoint(window, desiredPosition)
guard positionError == .success else { exit(70) }
let sizeError = setSize(window, desiredSize)
guard sizeError == .success else {
    let rollback = setPoint(window, originalWindowRect.origin)
    exit(rollback == .success ? 70 : 71)
}
let raiseError = AXUIElementPerformAction(window, kAXRaiseAction as CFString)
guard raiseError == .success else {
    let sizeRollback = setSize(window, originalWindowRect.size)
    let pointRollback = setPoint(window, originalWindowRect.origin)
    exit(sizeRollback == .success && pointRollback == .success ? 70 : 71)
}
Thread.sleep(forTimeInterval: 0.6)
let readbackResult: Result<Bool, AXReadFailure>
var verifiedSourceRect: CGRect?
var verifiedWindowRect: CGRect?
do {
    let sourceRect = try rect(matches[0])
    let windowRect = try rect(window)
    readbackResult = .success(validRect(sourceRect) && validRect(windowRect))
    if validRect(sourceRect) && validRect(windowRect) {
        verifiedSourceRect = sourceRect
        verifiedWindowRect = windowRect
    }
} catch let error as AXReadFailure {
    readbackResult = .failure(error)
}
if let exitCode = postMutationReadbackExit(readbackResult, validationFailure: 69, cleanupFailure: 71, rollback: {
    let sizeRollback = setSize(window, originalWindowRect.size)
    let pointRollback = setPoint(window, originalWindowRect.origin)
    return sizeRollback == .success && pointRollback == .success
}) {
    exit(exitCode)
}
let sourceRect = verifiedSourceRect!
let windowRect = verifiedWindowRect!
print("source=<QA_SOURCE> parent_window=<QA_WINDOW> exact_unique=true")
print("window_rect=\(windowRect.origin.x),\(windowRect.origin.y),\(windowRect.width),\(windowRect.height)")
print("source_rect=\(sourceRect.origin.x),\(sourceRect.origin.y),\(sourceRect.width),\(sourceRect.height)")
print("source_center=\(sourceRect.midX),\(sourceRect.midY)")
