import SwiftUI
import UserNotifications
import GhosttyKit

protocol GhosttyAppDelegate: AnyObject {
    #if os(macOS)
    /// Called when a callback needs access to a specific surface. This should return nil
    /// when the surface is no longer valid.
    func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView?
    #endif
}

extension Ghostty {
    // IMPORTANT: THIS IS NOT DONE.
    // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS
    class App: ObservableObject {
        enum Readiness: String {
            case loading, error, ready
        }

        /// Optional delegate
        weak var delegate: GhosttyAppDelegate?

        /// The readiness value of the state.
        @Published var readiness: Readiness = .loading

        /// The global app configuration. This defines the app level configuration plus any behavior
        /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
        /// configuration (i.e. font size) from the previously focused window. This would override this.
        @Published private(set) var config: Config

        /// Preferred config file than the default ones
        private var configPath: String?
        /// The ghostty app instance. We only have one of these for the entire app, although I guess
        /// in theory you can have multiple... I don't know why you would...
        @Published var app: ghostty_app_t? {
            didSet {
                guard let old = oldValue else { return }
                ghostty_app_free(old)
            }
        }

        /// True if we need to confirm before quitting.
        var needsConfirmQuit: Bool {
            guard let app = app else { return false }
            return ghostty_app_needs_confirm_quit(app)
        }

        init(configPath: String? = nil) {
            self.configPath = configPath
            // Initialize the global configuration.
            self.config = Config(at: configPath)
            if self.config.config == nil {
                readiness = .error
                return
            }

            // Create our "runtime" config. The "runtime" is the configuration that ghostty
            // uses to interface with the application runtime environment.
            var runtime_cfg = ghostty_runtime_config_s(
                userdata: Unmanaged.passUnretained(self).toOpaque(),
                supports_selection_clipboard: true,
                wakeup_cb: { userdata in App.wakeup(userdata) },
                action_cb: { app, target, action in App.action(app!, target: target, action: action) },
                read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
                confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
                write_clipboard_cb: { userdata, loc, content, len, confirm in
                    App.writeClipboard(userdata, location: loc, content: content, len: len, confirm: confirm) },
                close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }
            )

            // Create the ghostty app.
            guard let app = ghostty_app_new(&runtime_cfg, config.config) else {
                logger.critical("ghostty_app_new failed")
                readiness = .error
                return
            }
            self.app = app

#if os(macOS)
            // Set our initial focus state
            ghostty_app_set_focus(app, NSApp.isActive)

            let center = NotificationCenter.default
            center.addObserver(
                self,
                selector: #selector(keyboardSelectionDidChange(notification:)),
                name: NSTextInputContext.keyboardSelectionDidChangeNotification,
                object: nil)
            center.addObserver(
                self,
                selector: #selector(applicationDidBecomeActive(notification:)),
                name: NSApplication.didBecomeActiveNotification,
                object: nil)
            center.addObserver(
                self,
                selector: #selector(applicationDidResignActive(notification:)),
                name: NSApplication.didResignActiveNotification,
                object: nil)
#endif

            self.readiness = .ready
        }

        deinit {
            // This will force the didSet callbacks to run which free.
            self.app = nil

#if os(macOS)
            NotificationCenter.default.removeObserver(self)
#endif
        }

        // MARK: App Operations

        func appTick() {
            guard let app = self.app else { return }
            ghostty_app_tick(app)
        }

        static func openConfig() {
            let str = Ghostty.AllocatedString(ghostty_config_open_path()).string
            guard !str.isEmpty else { return }
            #if os(macOS)
            let fileURL = URL(fileURLWithPath: str).absoluteString
            var action = ghostty_action_open_url_s()
            action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT
            fileURL.withCString { cStr in
                action.url = cStr
                action.len = UInt(fileURL.count)
                _ = openURL(action)
            }
            #else
            fatalError("Unsupported platform for opening config file")
            #endif
        }

        /// Reload the configuration.
        func reloadConfig(soft: Bool = false) {
            guard let app = self.app else { return }

            // Soft updates just call with our existing config
            if soft {
                ghostty_app_update_config(app, config.config!)
                return
            }

            // Hard or full updates have to reload the full configuration
            let newConfig = Config(at: configPath)
            guard newConfig.loaded else {
                Ghostty.logger.warning("failed to reload configuration")
                return
            }

            ghostty_app_update_config(app, newConfig.config!)
            /// applied config will be updated in ``Self.configChange(_:target:v:)``
        }

        func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) {
            // Soft updates just call with our existing config
            if soft {
                ghostty_surface_update_config(surface, config.config!)
                return
            }

            // Hard or full updates have to reload the full configuration.
            // NOTE: We never set this on self.config because this is a surface-only
            // config. We free it after the call.
            let newConfig = Config(at: configPath)
            guard newConfig.loaded else {
                Ghostty.logger.warning("failed to reload configuration")
                return
            }

            ghostty_surface_update_config(surface, newConfig.config!)
        }

        /// Request that the given surface is closed. This will trigger the full normal surface close event
        /// cycle which will call our close surface callback.
        func requestClose(surface: ghostty_surface_t) {
            ghostty_surface_request_close(surface)
        }

        func newTab(surface: ghostty_surface_t) {
            let action = "new_tab"
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        func newWindow(surface: ghostty_surface_t) {
            let action = "new_window"
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        func split(surface: ghostty_surface_t, direction: ghostty_action_split_direction_e) {
            ghostty_surface_split(surface, direction)
        }

        func splitMoveFocus(surface: ghostty_surface_t, direction: SplitFocusDirection) {
            ghostty_surface_split_focus(surface, direction.toNative())
        }

        func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) {
            ghostty_surface_split_resize(surface, direction.toNative(), amount)
        }

        func splitEqualize(surface: ghostty_surface_t) {
            ghostty_surface_split_equalize(surface)
        }

        func splitToggleZoom(surface: ghostty_surface_t) {
            let action = "toggle_split_zoom"
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        func toggleFullscreen(surface: ghostty_surface_t) {
            let action = "toggle_fullscreen"
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        enum FontSizeModification {
            case increase(Int)
            case decrease(Int)
            case reset
        }

        func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) {
            let action: String
            switch change {
            case .increase(let amount):
                action = "increase_font_size:\(amount)"
            case .decrease(let amount):
                action = "decrease_font_size:\(amount)"
            case .reset:
                action = "reset_font_size"
            }
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        func toggleTerminalInspector(surface: ghostty_surface_t) {
            let action = "inspector:toggle"
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        func resetTerminal(surface: ghostty_surface_t) {
            let action = "reset"
            if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) {
                logger.warning("action failed action=\(action)")
            }
        }

        #if os(iOS)
        // MARK: Ghostty Callbacks (iOS)

        static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
        static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { return false }
        static func readClipboard(
            _ userdata: UnsafeMutableRawPointer?,
            location: ghostty_clipboard_e,
            state: UnsafeMutableRawPointer?
        ) {}

        static func confirmReadClipboard(
            _ userdata: UnsafeMutableRawPointer?,
            string: UnsafePointer<CChar>?,
            state: UnsafeMutableRawPointer?,
            request: ghostty_clipboard_request_e
        ) {}

        static func writeClipboard(
            _ userdata: UnsafeMutableRawPointer?,
            location: ghostty_clipboard_e,
            content: UnsafePointer<ghostty_clipboard_content_s>?,
            len: Int,
            confirm: Bool
        ) {}

        static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {}
        #endif

        #if os(macOS)

        // MARK: Notifications

        // Called when the selected keyboard changes. We have to notify Ghostty so that
        // it can reload the keyboard mapping for input.
        @objc private func keyboardSelectionDidChange(notification: NSNotification) {
            guard let app = self.app else { return }
            ghostty_app_keyboard_changed(app)
        }

        // Called when the app becomes active.
        @objc private func applicationDidBecomeActive(notification: NSNotification) {
            guard let app = self.app else { return }
            ghostty_app_set_focus(app, true)
        }

        // Called when the app becomes inactive.
        @objc private func applicationDidResignActive(notification: NSNotification) {
            guard let app = self.app else { return }
            ghostty_app_set_focus(app, false)
        }

        // MARK: Ghostty Callbacks (macOS)

        static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {
            let surface = self.surfaceUserdata(from: userdata)
            NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [
                "process_alive": processAlive,
            ])
        }

        static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) {
            // If we don't even have a surface, something went terrible wrong so we have
            // to leak "state".
            let surfaceView = self.surfaceUserdata(from: userdata)
            guard let surface = surfaceView.surface else { return }

            // Get our pasteboard
            guard let pasteboard = NSPasteboard.ghostty(location) else {
                return completeClipboardRequest(surface, data: "", state: state)
            }

            // Get our string
            let str = pasteboard.getOpinionatedStringContents() ?? ""
            completeClipboardRequest(surface, data: str, state: state)
        }

        static func confirmReadClipboard(
            _ userdata: UnsafeMutableRawPointer?,
            string: UnsafePointer<CChar>?,
            state: UnsafeMutableRawPointer?,
            request: ghostty_clipboard_request_e
        ) {
            let surface = self.surfaceUserdata(from: userdata)
            guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
            guard let request = Ghostty.ClipboardRequest.from(request: request) else { return }
            NotificationCenter.default.post(
                name: Notification.confirmClipboard,
                object: surface,
                userInfo: [
                    Notification.ConfirmClipboardStrKey: valueStr,
                    Notification.ConfirmClipboardStateKey: state as Any,
                    Notification.ConfirmClipboardRequestKey: request,
                ]
            )
        }

        static func completeClipboardRequest(
            _ surface: ghostty_surface_t,
            data: String,
            state: UnsafeMutableRawPointer?,
            confirmed: Bool = false
        ) {
            data.withCString { ptr in
                ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed)
            }
        }

        static func writeClipboard(
            _ userdata: UnsafeMutableRawPointer?,
            location: ghostty_clipboard_e,
            content: UnsafePointer<ghostty_clipboard_content_s>?,
            len: Int,
            confirm: Bool
        ) {
            let surface = self.surfaceUserdata(from: userdata)
            guard let pasteboard = NSPasteboard.ghostty(location) else { return }
            guard let content = content, len > 0 else { return }

            // Convert the C array to Swift array
            let contentArray = (0..<len).compactMap { i in
                Ghostty.ClipboardContent.from(content: content[i])
            }
            guard !contentArray.isEmpty else { return }

            // Assert there is only one text/plain entry. For security reasons we need
            // to guarantee this for now since our confirmation dialog only shows one.
            assert(contentArray.filter({ $0.mime == "text/plain" }).count <= 1,
                   "clipboard contents should have at most one text/plain entry")

            if !confirm {
                // Declare all types
                let types = contentArray.compactMap { item in
                    NSPasteboard.PasteboardType(mimeType: item.mime)
                }
                pasteboard.declareTypes(types, owner: nil)

                // Set data for each type
                for item in contentArray {
                    guard let type = NSPasteboard.PasteboardType(mimeType: item.mime) else { continue }
                    pasteboard.setString(item.data, forType: type)
                }
                return
            }

            // For confirmation, use the text/plain content if it exists
            guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }) else {
                return
            }

            NotificationCenter.default.post(
                name: Notification.confirmClipboard,
                object: surface,
                userInfo: [
                    Notification.ConfirmClipboardStrKey: textPlainContent.data,
                    Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
                ]
            )
        }

        static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
            let state = Unmanaged<App>.fromOpaque(userdata!).takeUnretainedValue()

            // Wakeup can be called from any thread so we schedule the app tick
            // from the main thread. There is probably some improvements we can make
            // to coalesce multiple ticks but I don't think it matters from a performance
            // standpoint since we don't do this much.
            DispatchQueue.main.async { state.appTick() }
        }

        /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground.
        func shouldPresentNotification(notification: UNNotification) -> Bool {
            let userInfo = notification.request.content.userInfo

            // We always require the notification to be attached to a surface.
            guard let uuidString = userInfo["surface"] as? String,
                  let uuid = UUID(uuidString: uuidString),
                  let surface = delegate?.findSurface(forUUID: uuid),
                  let window = surface.window else { return false }

            // If we don't require focus then we're good!
            let requireFocus = userInfo["requireFocus"] as? Bool ?? true
            if !requireFocus { return true }

            return !window.isKeyWindow || !surface.focused
        }

        /// Returns the GhosttyState from the given userdata value.
        static private func appState(fromView view: SurfaceView) -> App? {
            guard let surface = view.surface else { return nil }
            guard let app = ghostty_surface_app(surface) else { return nil }
            guard let app_ud = ghostty_app_userdata(app) else { return nil }
            return Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
        }

        /// Returns the surface view from the userdata.
        static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
            return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
        }

        static private func surfaceView(from surface: ghostty_surface_t) -> SurfaceView? {
            guard let surface_ud = ghostty_surface_userdata(surface) else { return nil }
            return Unmanaged<SurfaceView>.fromOpaque(surface_ud).takeUnretainedValue()
        }

        // MARK: Actions (macOS)

        static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool {
            // Make sure it a target we understand so all our action handlers can assert
            switch target.tag {
            case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE:
                break

            default:
                Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)")
                return false
            }

            // Action dispatch
            switch action.tag {
            case GHOSTTY_ACTION_QUIT:
                quit(app)

            case GHOSTTY_ACTION_NEW_WINDOW:
                newWindow(app, target: target)

            case GHOSTTY_ACTION_NEW_TAB:
                newTab(app, target: target)

            case GHOSTTY_ACTION_NEW_SPLIT:
                newSplit(app, target: target, direction: action.action.new_split)

            case GHOSTTY_ACTION_CLOSE_TAB:
                closeTab(app, target: target, mode: action.action.close_tab_mode)

            case GHOSTTY_ACTION_CLOSE_WINDOW:
                closeWindow(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
                toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)

            case GHOSTTY_ACTION_MOVE_TAB:
                return moveTab(app, target: target, move: action.action.move_tab)

            case GHOSTTY_ACTION_GOTO_TAB:
                return gotoTab(app, target: target, tab: action.action.goto_tab)

            case GHOSTTY_ACTION_GOTO_SPLIT:
                return gotoSplit(app, target: target, direction: action.action.goto_split)

            case GHOSTTY_ACTION_GOTO_WINDOW:
                return gotoWindow(app, target: target, direction: action.action.goto_window)

            case GHOSTTY_ACTION_RESIZE_SPLIT:
                return resizeSplit(app, target: target, resize: action.action.resize_split)

            case GHOSTTY_ACTION_EQUALIZE_SPLITS:
                equalizeSplits(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM:
                return toggleSplitZoom(app, target: target)

            case GHOSTTY_ACTION_INSPECTOR:
                controlInspector(app, target: target, mode: action.action.inspector)

            case GHOSTTY_ACTION_RENDER_INSPECTOR:
                renderInspector(app, target: target)

            case GHOSTTY_ACTION_DESKTOP_NOTIFICATION:
                showDesktopNotification(app, target: target, n: action.action.desktop_notification)

            case GHOSTTY_ACTION_SET_TITLE:
                setTitle(app, target: target, v: action.action.set_title)

            case GHOSTTY_ACTION_PROMPT_TITLE:
                return promptTitle(app, target: target, v: action.action.prompt_title)

            case GHOSTTY_ACTION_PWD:
                pwdChanged(app, target: target, v: action.action.pwd)

            case GHOSTTY_ACTION_OPEN_CONFIG:
                openConfig()

            case GHOSTTY_ACTION_FLOAT_WINDOW:
                toggleFloatWindow(app, target: target, mode: action.action.float_window)

            case GHOSTTY_ACTION_SECURE_INPUT:
                toggleSecureInput(app, target: target, mode: action.action.secure_input)

            case GHOSTTY_ACTION_MOUSE_SHAPE:
                setMouseShape(app, target: target, shape: action.action.mouse_shape)

            case GHOSTTY_ACTION_MOUSE_VISIBILITY:
                setMouseVisibility(app, target: target, v: action.action.mouse_visibility)

            case GHOSTTY_ACTION_MOUSE_OVER_LINK:
                setMouseOverLink(app, target: target, v: action.action.mouse_over_link)

            case GHOSTTY_ACTION_INITIAL_SIZE:
                setInitialSize(app, target: target, v: action.action.initial_size)

            case GHOSTTY_ACTION_RESET_WINDOW_SIZE:
                resetWindowSize(app, target: target)

            case GHOSTTY_ACTION_CELL_SIZE:
                setCellSize(app, target: target, v: action.action.cell_size)

            case GHOSTTY_ACTION_RENDERER_HEALTH:
                rendererHealth(app, target: target, v: action.action.renderer_health)

            case GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE:
                toggleCommandPalette(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_MAXIMIZE:
                toggleMaximize(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL:
                toggleQuickTerminal(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_VISIBILITY:
                toggleVisibility(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY:
                toggleBackgroundOpacity(app, target: target)

            case GHOSTTY_ACTION_KEY_SEQUENCE:
                keySequence(app, target: target, v: action.action.key_sequence)

            case GHOSTTY_ACTION_KEY_TABLE:
                keyTable(app, target: target, v: action.action.key_table)

            case GHOSTTY_ACTION_PROGRESS_REPORT:
                progressReport(app, target: target, v: action.action.progress_report)

            case GHOSTTY_ACTION_CONFIG_CHANGE:
                configChange(app, target: target, v: action.action.config_change)

            case GHOSTTY_ACTION_RELOAD_CONFIG:
                configReload(app, target: target, v: action.action.reload_config)

            case GHOSTTY_ACTION_COLOR_CHANGE:
                colorChange(app, target: target, change: action.action.color_change)

            case GHOSTTY_ACTION_RING_BELL:
                ringBell(app, target: target)

            case GHOSTTY_ACTION_READONLY:
                setReadonly(app, target: target, v: action.action.readonly)

            case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
                checkForUpdates(app)

            case GHOSTTY_ACTION_OPEN_URL:
                return openURL(action.action.open_url)

            case GHOSTTY_ACTION_UNDO:
                return undo(app, target: target)

            case GHOSTTY_ACTION_REDO:
                return redo(app, target: target)

            case GHOSTTY_ACTION_SCROLLBAR:
                scrollbar(app, target: target, v: action.action.scrollbar)

            case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
                closeAllWindows(app, target: target)

            case GHOSTTY_ACTION_START_SEARCH:
                startSearch(app, target: target, v: action.action.start_search)

            case GHOSTTY_ACTION_END_SEARCH:
                endSearch(app, target: target)

            case GHOSTTY_ACTION_SEARCH_TOTAL:
                searchTotal(app, target: target, v: action.action.search_total)

            case GHOSTTY_ACTION_SEARCH_SELECTED:
                searchSelected(app, target: target, v: action.action.search_selected)

            case GHOSTTY_ACTION_COMMAND_FINISHED:
                commandFinished(app, target: target, v: action.action.command_finished)

            case GHOSTTY_ACTION_PRESENT_TERMINAL:
                return presentTerminal(app, target: target)

            case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
                fallthrough
            case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS:
                fallthrough
            case GHOSTTY_ACTION_SIZE_LIMIT:
                fallthrough
            case GHOSTTY_ACTION_QUIT_TIMER:
                fallthrough
            case GHOSTTY_ACTION_SHOW_CHILD_EXITED:
                Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
                return false
            case GHOSTTY_ACTION_COPY_TITLE_TO_CLIPBOARD:
                return copyTitleToClipboard(app, target: target)
            default:
                Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)")
                return false
            }

            // If we reached here then we assume performed since all unknown actions
            // are captured in the switch and return false.
            return true
        }

        private static func quit(_ app: ghostty_app_t) {
            // On iOS, applications do not terminate programmatically like they do
            // on macOS. On iOS, applications are only terminated when a user physically
            // closes the application (i.e. going to the home screen). If we request
            // exit on iOS we ignore it.
            #if os(iOS)
            logger.info("quit request received, ignoring on iOS")
            #endif

            #if os(macOS)
            // We want to quit, start that process
            NSApplication.shared.terminate(nil)
            #endif
        }

        private static func checkForUpdates(
            _ app: ghostty_app_t
        ) {
            if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
                appDelegate.checkForUpdates(nil)
            }
        }

        private static func openURL(
            _ v: ghostty_action_open_url_s
        ) -> Bool {
            let action = Ghostty.Action.OpenURL(c: v)

            // If the URL doesn't have a valid scheme we assume its a file path. The URL
            // initializer will gladly take invalid URLs (e.g. plain file paths) and turn
            // them into schema-less URLs, but these won't open properly in text editors.
            // See: https://github.com/ghostty-org/ghostty/issues/8763
            let url: URL
            if let candidate = URL(string: action.url), candidate.scheme != nil {
                url = candidate
            } else {
                // Expand ~ to the user's home directory so that file paths
                // like ~/Documents/file.txt resolve correctly.
                let expandedPath = NSString(string: action.url).standardizingPath
                url = URL(filePath: expandedPath)
            }

            switch action.kind {
            case .text:
                // Open with the default editor for `*.ghostty` file or just system text editor
                let editor = NSWorkspace.shared.defaultApplicationURL(forExtension: url.pathExtension) ?? NSWorkspace.shared.defaultTextEditor
                if let textEditor = editor {
                    NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
                    return true
                }

            case .html:
                // The extension will be HTML and we do the right thing automatically.
                break

            case .unknown:
                break
            }

            // Open with the default application for the URL
            NSWorkspace.shared.open(url)
            return true
        }

        private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
            let undoManager: UndoManager?
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                undoManager = (NSApp.delegate as? AppDelegate)?.undoManager

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return false }
                guard let surfaceView = self.surfaceView(from: surface) else { return false }
                undoManager = surfaceView.undoManager

            default:
                assertionFailure()
                return false
            }

            guard let undoManager, undoManager.canUndo else { return false }
            undoManager.undo()
            return true
        }

        private static func redo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
            let undoManager: UndoManager?
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                undoManager = (NSApp.delegate as? AppDelegate)?.undoManager

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return false }
                guard let surfaceView = self.surfaceView(from: surface) else { return false }
                undoManager = surfaceView.undoManager

            default:
                assertionFailure()
                return false
            }

            guard let undoManager, undoManager.canRedo else { return false }
            undoManager.redo()
            return true
        }

        private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                NotificationCenter.default.post(
                    name: Notification.ghosttyNewWindow,
                    object: nil,
                    userInfo: [:]
                )

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: Notification.ghosttyNewWindow,
                    object: surfaceView,
                    userInfo: [
                        Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_WINDOW)),
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                NotificationCenter.default.post(
                    name: Notification.ghosttyNewTab,
                    object: nil,
                    userInfo: [:]
                )

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let appState = self.appState(fromView: surfaceView) else { return }
                guard appState.config.windowDecorations else {
                    let alert = NSAlert()
                    alert.messageText = "Tabs are disabled"
                    alert.informativeText = "Enable window decorations to use tabs"
                    alert.addButton(withTitle: "OK")
                    alert.alertStyle = .warning
                    _ = alert.runModal()
                    return
                }

                NotificationCenter.default.post(
                    name: Notification.ghosttyNewTab,
                    object: surfaceView,
                    userInfo: [
                        Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_TAB)),
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func newSplit(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            direction: ghostty_action_split_direction_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                // New split does nothing with an app target
                Ghostty.logger.warning("new split does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                NotificationCenter.default.post(
                    name: Notification.ghosttyNewSplit,
                    object: surfaceView,
                    userInfo: [
                        "direction": direction,
                        Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_SPLIT)),
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func presentTerminal(
            _ app: ghostty_app_t,
            target: ghostty_target_s
        ) -> Bool {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                return false

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return false }
                guard let surfaceView = self.surfaceView(from: surface) else { return false }

                NotificationCenter.default.post(
                    name: Notification.ghosttyPresentTerminal,
                    object: surfaceView
                )
                return true

            default:
                assertionFailure()
                return false
            }
        }

        private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("close tabs does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                switch mode {
                case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS:
                    NotificationCenter.default.post(
                        name: .ghosttyCloseTab,
                        object: surfaceView
                    )
                    return

                case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER:
                    NotificationCenter.default.post(
                        name: .ghosttyCloseOtherTabs,
                        object: surfaceView
                    )
                    return

                case GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT:
                    NotificationCenter.default.post(
                        name: .ghosttyCloseTabsOnTheRight,
                        object: surfaceView
                    )
                    return

                default:
                    assertionFailure()
                }

            default:
                assertionFailure()
            }
        }

        private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("close window does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                NotificationCenter.default.post(
                    name: .ghosttyCloseWindow,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func closeAllWindows(_ app: ghostty_app_t, target: ghostty_target_s) {
            guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
            appDelegate.closeAllWindows(nil)
        }

        private static func toggleFullscreen(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            mode raw: ghostty_action_fullscreen_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle fullscreen does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let mode = FullscreenMode.from(ghostty: raw) else {
                    Ghostty.logger.warning("unknown fullscreen mode raw=\(raw.rawValue)")
                    return
                }
                NotificationCenter.default.post(
                    name: Notification.ghosttyToggleFullscreen,
                    object: surfaceView,
                    userInfo: [
                        Notification.FullscreenModeKey: mode,
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func toggleCommandPalette(
            _ app: ghostty_app_t,
            target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle command palette does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: .ghosttyCommandPaletteDidToggle,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func toggleMaximize(
            _ app: ghostty_app_t,
            target: ghostty_target_s
        ) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle maximize does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: .ghosttyMaximizeDidToggle,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func toggleVisibility(
            _ app: ghostty_app_t,
            target: ghostty_target_s
        ) {
            guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
            appDelegate.toggleVisibility(self)
        }

        private static func ringBell(
            _ app: ghostty_app_t,
            target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                // Technically we could still request app attention here but there
                // are no known cases where the bell is rang with an app target so
                // I think its better to warn.
                Ghostty.logger.warning("ring bell does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: .ghosttyBellDidRing,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func setReadonly(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_readonly_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("set readonly does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: .ghosttyDidChangeReadonly,
                    object: surfaceView,
                    userInfo: [
                        SwiftUI.Notification.Name.ReadonlyKey: v == GHOSTTY_READONLY_ON,
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func moveTab(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            move: ghostty_action_move_tab_s) -> Bool {
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    Ghostty.logger.warning("move tab does nothing with an app target")
                    return false

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return false }
                    guard let surfaceView = self.surfaceView(from: surface) else { return false }

                    // See gotoTab for notes on this check.
                    guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }

                    NotificationCenter.default.post(
                        name: .ghosttyMoveTab,
                        object: surfaceView,
                        userInfo: [
                            SwiftUI.Notification.Name.GhosttyMoveTabKey: Action.MoveTab(c: move),
                        ]
                    )

                default:
                    assertionFailure()
                }

                return true
        }

        private static func gotoTab(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            tab: ghostty_action_goto_tab_e) -> Bool {
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    Ghostty.logger.warning("goto tab does nothing with an app target")
                    return false

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return false }
                    guard let surfaceView = self.surfaceView(from: surface) else { return false }

                    // Similar to goto_split (see comment there) about our performability,
                    // we should make this more accurate later.
                    guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }

                    NotificationCenter.default.post(
                        name: Notification.ghosttyGotoTab,
                        object: surfaceView,
                        userInfo: [
                            Notification.GotoTabKey: tab,
                        ]
                    )

                default:
                    assertionFailure()
                }

                return true
        }

        private static func gotoSplit(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            direction: ghostty_action_goto_split_e) -> Bool {
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    Ghostty.logger.warning("goto split does nothing with an app target")
                    return false

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return false }
                    guard let surfaceView = self.surfaceView(from: surface) else { return false }
                    guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }

                    // If the window has no splits, the action is not performable
                    guard controller.surfaceTree.isSplit else { return false }

                    // Convert the C API direction to our Swift type
                    guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return false }

                    // Find the current node in the tree
                    guard let targetNode = controller.surfaceTree.root?.node(view: surfaceView) else { return false }

                    // Check if a split actually exists in the target direction before
                    // returning true. This ensures performable keybinds only consume
                    // the key event when we actually perform navigation.
                    let focusDirection: SplitTree<Ghostty.SurfaceView>.FocusDirection = splitDirection.toSplitTreeFocusDirection()
                    guard controller.surfaceTree.focusTarget(for: focusDirection, from: targetNode) != nil else {
                        return false
                    }

                    // We have a valid target, post the notification to perform the navigation
                    NotificationCenter.default.post(
                        name: Notification.ghosttyFocusSplit,
                        object: surfaceView,
                        userInfo: [
                            Notification.SplitDirectionKey: splitDirection as Any,
                        ]
                    )

                    return true

                default:
                    assertionFailure()
                    return false
                }
        }

        private static func gotoWindow(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            direction: ghostty_action_goto_window_e
        ) -> Bool {
            // Collect candidate windows: visible terminal windows that are either
            // standalone or the currently selected tab in their tab group. This
            // treats each native tab group as a single "window" for navigation
            // purposes, since goto_tab handles per-tab navigation.
            let candidates: [NSWindow] = NSApplication.shared.windows.filter { window in
                guard window.windowController is BaseTerminalController else { return false }
                guard window.isVisible, !window.isMiniaturized else { return false }
                // For native tabs, only include the selected tab in each group
                if let group = window.tabGroup, group.selectedWindow !== window {
                    return false
                }
                return true
            }

            // Need at least two windows to navigate between
            guard candidates.count > 1 else { return false }

            // Find starting index from the current key/main window
            let startIndex = candidates.firstIndex(where: { $0.isKeyWindow })
                ?? candidates.firstIndex(where: { $0.isMainWindow })
                ?? 0

            let step: Int
            switch direction {
            case GHOSTTY_GOTO_WINDOW_NEXT:
                step = 1
            case GHOSTTY_GOTO_WINDOW_PREVIOUS:
                step = -1
            default:
                return false
            }

            // Iterate with wrap-around until we find a valid window or return to start
            let count = candidates.count
            var index = (startIndex + step + count) % count

            while index != startIndex {
                let candidate = candidates[index]
                if candidate.isVisible, !candidate.isMiniaturized {
                    candidate.makeKeyAndOrderFront(nil)
                    // Also focus the terminal surface within the window
                    if let controller = candidate.windowController as? BaseTerminalController,
                       let surface = controller.focusedSurface {
                        Ghostty.moveFocus(to: surface)
                    }
                    return true
                }
                index = (index + step + count) % count
            }

            return false
        }

        private static func resizeSplit(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            resize: ghostty_action_resize_split_s) -> Bool {
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    Ghostty.logger.warning("resize split does nothing with an app target")
                    return false

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return false }
                    guard let surfaceView = self.surfaceView(from: surface) else { return false }
                    guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }

                    // If the window has no splits, the action is not performable
                    guard controller.surfaceTree.isSplit else { return false }

                    guard let resizeDirection = SplitResizeDirection.from(direction: resize.direction) else { return false }
                    NotificationCenter.default.post(
                        name: Notification.didResizeSplit,
                        object: surfaceView,
                        userInfo: [
                            Notification.ResizeSplitDirectionKey: resizeDirection,
                            Notification.ResizeSplitAmountKey: resize.amount,
                        ]
                    )
                    return true

                default:
                    assertionFailure()
                    return false
                }
        }

        private static func equalizeSplits(
            _ app: ghostty_app_t,
            target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("equalize splits does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: Notification.didEqualizeSplits,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func toggleSplitZoom(
            _ app: ghostty_app_t,
            target: ghostty_target_s) -> Bool {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle split zoom does nothing with an app target")
                return false

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return false }
                guard let surfaceView = self.surfaceView(from: surface) else { return false }
                guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }

                // If the window has no splits, the action is not performable
                guard controller.surfaceTree.isSplit else { return false }

                NotificationCenter.default.post(
                    name: Notification.didToggleSplitZoom,
                    object: surfaceView
                )
                return true

            default:
                assertionFailure()
                return false
            }
        }

        private static func controlInspector(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            mode: ghostty_action_inspector_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle inspector does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: Notification.didControlInspector,
                    object: surfaceView,
                    userInfo: ["mode": mode]
                )

            default:
                assertionFailure()
            }
        }

        private static func showDesktopNotification(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            n: ghostty_action_desktop_notification_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("desktop notification does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let title = String(cString: n.title!, encoding: .utf8) else { return }
                guard let body = String(cString: n.body!, encoding: .utf8) else { return }
                showDesktopNotification(surfaceView, title: title, body: body)

            default:
                assertionFailure()
            }
        }

        private static func showDesktopNotification(
            _ surfaceView: SurfaceView,
            title: String,
            body: String,
            requireFocus: Bool = true) {
            let center = UNUserNotificationCenter.current()
            center.requestAuthorization(options: [.alert, .sound]) { _, error in
                if let error = error {
                    Ghostty.logger.error("Error while requesting notification authorization: \(error)")
                }
            }

            center.getNotificationSettings { settings in
                guard settings.authorizationStatus == .authorized else { return }
                surfaceView.showUserNotification(
                    title: title,
                    body: body,
                    requireFocus: requireFocus
                )
            }
        }

        private static func commandFinished(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_command_finished_s
        ) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("command finished does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                // Determine if we even care about command finish notifications
                guard let config = (NSApplication.shared.delegate as? AppDelegate)?.ghostty.config else { return }
                switch config.notifyOnCommandFinish {
                case .never:
                    return

                case .unfocused:
                    if surfaceView.focused { return }

                case .always:
                    break
                }

                // Determine if the command was slow enough
                let duration = Duration.nanoseconds(v.duration)
                guard Duration.nanoseconds(v.duration) >= config.notifyOnCommandFinishAfter else { return }

                let actions = config.notifyOnCommandFinishAction

                if actions.contains(.bell) {
                    NotificationCenter.default.post(
                        name: .ghosttyBellDidRing,
                        object: surfaceView
                    )
                }

                if actions.contains(.notify) {
                    let title: String
                    if v.exit_code < 0 {
                        title = "Command Finished"
                    } else if v.exit_code == 0 {
                        title = "Command Succeeded"
                    } else {
                        title = "Command Failed"
                    }

                    let body: String
                    let formattedDuration = duration.formatted(
                        .units(
                            allowed: [.hours, .minutes, .seconds, .milliseconds],
                            width: .abbreviated,
                            fractionalPart: .hide
                        )
                    )
                    if v.exit_code < 0 {
                        body = "Command took \(formattedDuration)."
                    } else {
                        body = "Command took \(formattedDuration) and exited with code \(v.exit_code)."
                    }

                    showDesktopNotification(
                        surfaceView,
                        title: title,
                        body: body,
                        requireFocus: false
                    )
                }

            default:
                assertionFailure()
            }
        }

        private static func toggleFloatWindow(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            mode mode_raw: ghostty_action_float_window_e
        ) {
            guard let mode = SetFloatWIndow.from(mode_raw) else { return }

            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle float window does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let window = surfaceView.window as? TerminalWindow else { return }

                switch mode {
                case .on:
                    window.level = .floating

                case .off:
                    window.level = .normal

                case .toggle:
                    window.level = window.level == .floating ? .normal : .floating
                }

                if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
                    appDelegate.syncFloatOnTopMenu(window)
                }

            default:
                assertionFailure()
            }
        }

        private static func toggleBackgroundOpacity(
            _ app: ghostty_app_t,
            target: ghostty_target_s
        ) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("toggle background opacity does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface,
                    let surfaceView = self.surfaceView(from: surface),
                    let controller = surfaceView.window?.windowController as? BaseTerminalController else { return }

                controller.toggleBackgroundOpacity()

            default:
                assertionFailure()
            }
        }

        private static func toggleSecureInput(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            mode mode_raw: ghostty_action_secure_input_e
        ) {
            guard let mode = SetSecureInput.from(mode_raw) else { return }

            switch target.tag {
            case GHOSTTY_TARGET_APP:
                guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
                appDelegate.setSecureInput(mode)

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let appState = self.appState(fromView: surfaceView) else { return }
                guard appState.config.autoSecureInput else { return }

                switch mode {
                case .on:
                    surfaceView.passwordInput = true

                case .off:
                    surfaceView.passwordInput = false

                case .toggle:
                    surfaceView.passwordInput = !surfaceView.passwordInput
                }

            default:
                assertionFailure()
            }
        }

        private static func toggleQuickTerminal(
            _ app: ghostty_app_t,
            target: ghostty_target_s
        ) {
            guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
            appDelegate.toggleQuickTerminal(self)
        }

        private static func setTitle(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_set_title_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("set title does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let title = String(cString: v.title!, encoding: .utf8) else { return }
                surfaceView.setTitle(title)

            default:
                assertionFailure()
            }
        }

        private static func copyTitleToClipboard(
            _ app: ghostty_app_t,
            target: ghostty_target_s) -> Bool {
            switch target.tag {
            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return false }
                guard let surfaceView = self.surfaceView(from: surface) else { return false }
                let title = surfaceView.title
                if title.isEmpty { return false }
                let pasteboard = NSPasteboard.general
                pasteboard.clearContents()
                pasteboard.setString(title, forType: .string)
                return true

            default:
                return false
            }
        }

        private static func promptTitle(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_prompt_title_e) -> Bool {
            let promptTitle = Action.PromptTitle(v)
            switch promptTitle {
            case .surface:
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    Ghostty.logger.warning("set title prompt does nothing with an app target")
                    return false

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return false }
                    guard let surfaceView = self.surfaceView(from: surface) else { return false }
                    surfaceView.promptTitle()
                    return true

                default:
                    assertionFailure()
                    return false
                }

            case .tab:
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    guard let window = NSApp.mainWindow ?? NSApp.keyWindow,
                          let controller = window.windowController as? BaseTerminalController
                    else { return false }
                    controller.promptTabTitle()
                    return true

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return false }
                    guard let surfaceView = self.surfaceView(from: surface) else { return false }
                    guard let window = surfaceView.window,
                          let controller = window.windowController as? BaseTerminalController
                    else { return false }
                    controller.promptTabTitle()
                    return true

                default:
                    assertionFailure()
                    return false
                }
            }
        }

        private static func pwdChanged(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_pwd_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("pwd change does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let pwd = String(cString: v.pwd!, encoding: .utf8) else { return }
                surfaceView.pwd = pwd

            default:
                assertionFailure()
            }
        }

        private static func setMouseShape(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            shape: ghostty_action_mouse_shape_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("set mouse shapes nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                surfaceView.setCursorShape(shape)

            default:
                assertionFailure()
            }
        }

        private static func setMouseVisibility(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_mouse_visibility_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("set mouse shapes nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                switch v {
                case GHOSTTY_MOUSE_VISIBLE:
                    surfaceView.setCursorVisibility(true)

                case GHOSTTY_MOUSE_HIDDEN:
                    surfaceView.setCursorVisibility(false)

                default:
                    return
                }

            default:
                assertionFailure()
            }
        }

        private static func setMouseOverLink(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_mouse_over_link_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("mouse over link does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard v.len > 0 else {
                    surfaceView.hoverUrl = nil
                    return
                }

                let buffer = Data(bytes: v.url!, count: v.len)
                surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)

            default:
                assertionFailure()
            }
        }

        private static func setInitialSize(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_initial_size_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("initial size does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                surfaceView.initialSize = NSSize(width: Double(v.width), height: Double(v.height))

            default:
                assertionFailure()
            }
        }

        private static func resetWindowSize(
            _ app: ghostty_app_t,
            target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("reset window size does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: .ghosttyResetWindowSize,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func setCellSize(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_cell_size_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("mouse over link does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                let backingSize = NSSize(width: Double(v.width), height: Double(v.height))
                DispatchQueue.main.async { [weak surfaceView] in
                    guard let surfaceView else { return }
                    surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
                }

            default:
                assertionFailure()
            }
        }

        private static func renderInspector(
            _ app: ghostty_app_t,
            target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("mouse over link does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: Notification.inspectorNeedsDisplay,
                    object: surfaceView
                )

            default:
                assertionFailure()
            }
        }

        private static func rendererHealth(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_renderer_health_e) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("mouse over link does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                NotificationCenter.default.post(
                    name: Notification.didUpdateRendererHealth,
                    object: surfaceView,
                    userInfo: [
                        "health": v,
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func keySequence(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_key_sequence_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("key sequence does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                if v.active {
                    NotificationCenter.default.post(
                        name: Notification.didContinueKeySequence,
                        object: surfaceView,
                        userInfo: [
                            Notification.KeySequenceKey: keyboardShortcut(for: v.trigger) as Any
                        ]
                    )
                } else {
                    NotificationCenter.default.post(
                        name: Notification.didEndKeySequence,
                        object: surfaceView
                    )
                }

            default:
                assertionFailure()
            }
        }

        private static func keyTable(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_key_table_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("key table does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }
                guard let action = Ghostty.Action.KeyTable(c: v) else { return }

                NotificationCenter.default.post(
                    name: Notification.didChangeKeyTable,
                    object: surfaceView,
                    userInfo: [Notification.KeyTableKey: action]
                )

            default:
                assertionFailure()
            }
        }

        private static func progressReport(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_progress_report_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("progress report does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                let progressReport = Ghostty.Action.ProgressReport(c: v)
                DispatchQueue.main.async {
                    if progressReport.state == .remove {
                        surfaceView.progressReport = nil
                    } else {
                        surfaceView.progressReport = progressReport
                    }
                }

            default:
                assertionFailure()
            }
        }

        private static func scrollbar(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_scrollbar_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("scrollbar does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                let scrollbar = Ghostty.Action.Scrollbar(c: v)
                NotificationCenter.default.post(
                    name: .ghosttyDidUpdateScrollbar,
                    object: surfaceView,
                    userInfo: [
                        SwiftUI.Notification.Name.ScrollbarKey: scrollbar
                    ]
                )

            default:
                assertionFailure()
            }
        }

        private static func startSearch(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_start_search_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("start_search does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                let startSearch = Ghostty.Action.StartSearch(c: v)
                DispatchQueue.main.async {
                    if let searchState = surfaceView.searchState {
                        if let needle = startSearch.needle, !needle.isEmpty {
                            searchState.needle = needle
                        }
                    } else {
                        surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch)
                    }

                    NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView)
                }

            default:
                assertionFailure()
            }
        }

        private static func endSearch(
            _ app: ghostty_app_t,
            target: ghostty_target_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("end_search does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                DispatchQueue.main.async {
                    surfaceView.searchState = nil
                }

            default:
                assertionFailure()
            }
        }

        private static func searchTotal(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_search_total_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("search_total does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                let total: UInt? = v.total >= 0 ? UInt(v.total) : nil
                DispatchQueue.main.async {
                    surfaceView.searchState?.total = total
                }

            default:
                assertionFailure()
            }
        }

        private static func searchSelected(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_search_selected_s) {
            switch target.tag {
            case GHOSTTY_TARGET_APP:
                Ghostty.logger.warning("search_selected does nothing with an app target")
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                guard let surfaceView = self.surfaceView(from: surface) else { return }

                let selected: UInt? = v.selected >= 0 ? UInt(v.selected) : nil
                DispatchQueue.main.async {
                    surfaceView.searchState?.selected = selected
                }

            default:
                assertionFailure()
            }
        }

        private static func configReload(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_reload_config_s) {
            logger.info("config reload notification")

            guard let app_ud = ghostty_app_userdata(app) else { return }
            let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()

            switch target.tag {
            case GHOSTTY_TARGET_APP:
                ghostty.reloadConfig(soft: v.soft)
                return

            case GHOSTTY_TARGET_SURFACE:
                guard let surface = target.target.surface else { return }
                ghostty.reloadConfig(surface: surface, soft: v.soft)

            default:
                assertionFailure()
            }
        }

        private static func configChange(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            v: ghostty_action_config_change_s) {
                logger.info("config change notification")

                // Clone the config so we own the memory. It'd be nicer to not have to do
                // this but since we async send the config out below we have to own the lifetime.
                // A future improvement might be to add reference counting to config or
                // something so apprt's do not have to do this.
                let config = Config(clone: v.config)

                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    // Notify the world that the app config changed
                    NotificationCenter.default.post(
                        name: .ghosttyConfigDidChange,
                        object: nil,
                        userInfo: [
                            SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
                        ]
                    )

                    // We also REPLACE our app-level config when this happens. This lets
                    // all the various things that depend on this but are still theme specific
                    // such as split border color work.
                    guard let app_ud = ghostty_app_userdata(app) else { return }
                    let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
                    ghostty.config = config

                    return

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return }
                    guard let surfaceView = self.surfaceView(from: surface) else { return }
                    NotificationCenter.default.post(
                        name: .ghosttyConfigDidChange,
                        object: surfaceView,
                        userInfo: [
                            SwiftUI.Notification.Name.GhosttyConfigChangeKey: config,
                        ]
                    )

                default:
                    assertionFailure()
                }
            }

        private static func colorChange(
            _ app: ghostty_app_t,
            target: ghostty_target_s,
            change: ghostty_action_color_change_s) {
                switch target.tag {
                case GHOSTTY_TARGET_APP:
                    Ghostty.logger.warning("color change does nothing with an app target")
                    return

                case GHOSTTY_TARGET_SURFACE:
                    guard let surface = target.target.surface else { return }
                    guard let surfaceView = self.surfaceView(from: surface) else { return }
                    NotificationCenter.default.post(
                        name: .ghosttyColorDidChange,
                        object: surfaceView,
                        userInfo: [
                            SwiftUI.Notification.Name.GhosttyColorChangeKey: Action.ColorChange(c: change)
                        ]
                    )

                default:
                    assertionFailure()
                }
        }

        // MARK: User Notifications

        /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user
        func handleUserNotification(response: UNNotificationResponse) {
            let userInfo = response.notification.request.content.userInfo
            guard let uuidString = userInfo["surface"] as? String,
                  let uuid = UUID(uuidString: uuidString),
                  let surface = delegate?.findSurface(forUUID: uuid) else { return }

            switch response.actionIdentifier {
            case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow:
                // The user clicked on a notification
                surface.handleUserNotification(notification: response.notification, focus: true)
            case UNNotificationDismissActionIdentifier:
                // The user dismissed the notification
                surface.handleUserNotification(notification: response.notification, focus: false)
            default:
                break
            }
        }

        #endif
    }
}
