#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
import Foundation

#if DEBUG
private enum SpawnedProcessGroupTestingOverrides {
    @TaskLocal static var outputHolderDiscoveryDelay: TimeInterval?
    @TaskLocal static var outputHolderPreKillSnapshotHook: (@Sendable () -> Void)?
    @TaskLocal static var outputHolderPreKillDelay: TimeInterval?
    @TaskLocal static var outputHolderCleanupMaxLifetime: TimeInterval?
    @TaskLocal static var forcePTYPrimaryDescriptorReservationFailure = false
}
#endif

package final class SpawnedProcessGroup: @unchecked Sendable {
    package enum LaunchError: LocalizedError {
        case setupFailed(String)
        case spawnFailed(String)

        package var errorDescription: String? {
            switch self {
            case let .setupFailed(details):
                "Failed to prepare process: \(details)"
            case let .spawnFailed(details):
                "Failed to launch process: \(details)"
            }
        }
    }

    private final class TerminationState: @unchecked Sendable {
        private let condition = NSCondition()
        private var exitObserved = false
        private var exitObservedAt: Date?
        private var reapRequested = false
        private var status: Int32?

        var hasObservedExit: Bool {
            self.condition.withLock { self.exitObserved }
        }

        var value: Int32? {
            self.condition.withLock { self.status }
        }

        var observationDate: Date? {
            self.condition.withLock { self.exitObservedAt }
        }

        func observeExit() {
            self.condition.withLock {
                self.exitObserved = true
                self.exitObservedAt = Date()
                self.condition.broadcast()
            }
        }

        func requestReap() {
            self.condition.withLock {
                self.reapRequested = true
                self.condition.broadcast()
            }
        }

        func waitForReapRequest(timeout: TimeInterval) {
            let deadline = Date().addingTimeInterval(timeout)
            self.condition.lock()
            while !self.reapRequested, self.condition.wait(until: deadline) {}
            self.condition.unlock()
        }

        func resolve(_ status: Int32) {
            self.condition.withLock {
                guard self.status == nil else { return }
                self.status = status
                self.condition.broadcast()
            }
        }
    }

    private final class ProcessIdentityState: @unchecked Sendable {
        private let lock = NSLock()
        private var identities: Set<TTYProcessTreeTerminator.ProcessIdentity> = []

        var snapshot: Set<TTYProcessTreeTerminator.ProcessIdentity> {
            self.lock.withLock { self.identities }
        }

        func formUnion(_ identities: Set<TTYProcessTreeTerminator.ProcessIdentity>) {
            self.lock.withLock {
                self.identities.formUnion(identities)
            }
        }
    }

    private struct OutputPipeIdentity: Hashable, Sendable {
        #if canImport(Darwin)
        let firstHandle: UInt64
        let secondHandle: UInt64
        #else
        let inode: UInt64
        #endif

        static func resolve(fileDescriptor: Int32) -> OutputPipeIdentity? {
            #if canImport(Darwin)
            var info = pipe_fdinfo()
            let byteCount = proc_pidfdinfo(
                getpid(),
                fileDescriptor,
                PROC_PIDFDPIPEINFO,
                &info,
                Int32(MemoryLayout<pipe_fdinfo>.size))
            guard byteCount == MemoryLayout<pipe_fdinfo>.size else { return nil }
            let handles = [info.pipeinfo.pipe_handle, info.pipeinfo.pipe_peerhandle].sorted()
            guard handles[0] != 0, handles[1] != 0 else { return nil }
            return OutputPipeIdentity(firstHandle: handles[0], secondHandle: handles[1])
            #else
            var info = stat()
            guard fstat(fileDescriptor, &info) == 0 else { return nil }
            return OutputPipeIdentity(inode: UInt64(info.st_ino))
            #endif
        }

        static func holderPIDs(for pipes: Set<OutputPipeIdentity>) -> Set<pid_t> {
            guard !pipes.isEmpty else { return [] }
            #if canImport(Darwin)
            return Set(SpawnedProcessGroup.allProcessIDs().filter { self.process(pid: $0, holdsAny: pipes) })
            #else
            let targets = Set(pipes.map { "pipe:[\($0.inode)]" })
            return Set(SpawnedProcessGroup.allProcessIDs().filter { pid in
                let directory = "/proc/\(pid)/fd"
                guard let descriptors = try? FileManager.default.contentsOfDirectory(atPath: directory) else {
                    return false
                }
                return descriptors.contains { descriptor in
                    let path = "\(directory)/\(descriptor)"
                    guard let target = try? FileManager.default.destinationOfSymbolicLink(atPath: path) else {
                        return false
                    }
                    return targets.contains(target)
                }
            })
            #endif
        }

        #if canImport(Darwin)
        private static func process(pid: pid_t, holdsAny pipes: Set<OutputPipeIdentity>) -> Bool {
            let requiredBytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
            guard requiredBytes > 0 else { return false }
            let stride = MemoryLayout<proc_fdinfo>.stride
            var descriptors = [proc_fdinfo](
                repeating: proc_fdinfo(),
                count: Int(requiredBytes) / stride + 8)
            let actualBytes = descriptors.withUnsafeMutableBytes { buffer in
                proc_pidinfo(
                    pid,
                    PROC_PIDLISTFDS,
                    0,
                    buffer.baseAddress,
                    Int32(buffer.count))
            }
            guard actualBytes > 0 else { return false }

            for descriptor in descriptors.prefix(Int(actualBytes) / stride)
                where descriptor.proc_fdtype == PROX_FDTYPE_PIPE
            {
                var info = pipe_fdinfo()
                let byteCount = proc_pidfdinfo(
                    pid,
                    descriptor.proc_fd,
                    PROC_PIDFDPIPEINFO,
                    &info,
                    Int32(MemoryLayout<pipe_fdinfo>.size))
                guard byteCount == MemoryLayout<pipe_fdinfo>.size else { continue }
                let handles = [info.pipeinfo.pipe_handle, info.pipeinfo.pipe_peerhandle].sorted()
                let identity = OutputPipeIdentity(firstHandle: handles[0], secondHandle: handles[1])
                if pipes.contains(identity) {
                    return true
                }
            }
            return false
        }
        #endif
    }

    private struct OutputTTYIdentity: Hashable, Sendable {
        let device: UInt64
        let inode: UInt64
        let rawDevice: UInt64

        static func resolve(fileDescriptor: Int32) -> OutputTTYIdentity? {
            var info = stat()
            guard fstat(fileDescriptor, &info) == 0 else { return nil }
            #if canImport(Darwin)
            let device = SpawnedProcessGroup.darwinDeviceIdentifier(info.st_dev)
            let rawDevice = SpawnedProcessGroup.darwinDeviceIdentifier(info.st_rdev)
            #else
            let device = UInt64(info.st_dev)
            let rawDevice = UInt64(info.st_rdev)
            #endif
            return OutputTTYIdentity(
                device: device,
                inode: UInt64(info.st_ino),
                rawDevice: rawDevice)
        }

        static func holderPIDs(for terminals: Set<OutputTTYIdentity>) -> Set<pid_t> {
            guard !terminals.isEmpty else { return [] }
            #if canImport(Darwin)
            return Set(SpawnedProcessGroup.allProcessIDs().filter { self.process(pid: $0, holdsAny: terminals) })
            #else
            return Set(SpawnedProcessGroup.allProcessIDs().filter { pid in
                let directory = "/proc/\(pid)/fd"
                guard let descriptors = try? FileManager.default.contentsOfDirectory(atPath: directory) else {
                    return false
                }
                return descriptors.contains { descriptor in
                    var info = stat()
                    let path = "\(directory)/\(descriptor)"
                    guard path.withCString({ fstatat(AT_FDCWD, $0, &info, 0) }) == 0 else { return false }
                    let identity = OutputTTYIdentity(
                        device: UInt64(info.st_dev),
                        inode: UInt64(info.st_ino),
                        rawDevice: UInt64(info.st_rdev))
                    return terminals.contains(identity)
                }
            })
            #endif
        }

        #if canImport(Darwin)
        private static func process(pid: pid_t, holdsAny terminals: Set<OutputTTYIdentity>) -> Bool {
            let requiredBytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
            guard requiredBytes > 0 else { return false }
            let stride = MemoryLayout<proc_fdinfo>.stride
            var descriptors = [proc_fdinfo](
                repeating: proc_fdinfo(),
                count: Int(requiredBytes) / stride + 8)
            let actualBytes = descriptors.withUnsafeMutableBytes { buffer in
                proc_pidinfo(
                    pid,
                    PROC_PIDLISTFDS,
                    0,
                    buffer.baseAddress,
                    Int32(buffer.count))
            }
            guard actualBytes > 0 else { return false }

            for descriptor in descriptors.prefix(Int(actualBytes) / stride)
                where descriptor.proc_fdtype == PROX_FDTYPE_VNODE
            {
                var info = vnode_fdinfo()
                let byteCount = proc_pidfdinfo(
                    pid,
                    descriptor.proc_fd,
                    PROC_PIDFDVNODEINFO,
                    &info,
                    Int32(MemoryLayout<vnode_fdinfo>.size))
                guard byteCount == MemoryLayout<vnode_fdinfo>.size else { continue }
                let stats = info.pvi.vi_stat
                let identity = OutputTTYIdentity(
                    device: UInt64(stats.vst_dev),
                    inode: stats.vst_ino,
                    rawDevice: UInt64(stats.vst_rdev))
                if terminals.contains(identity) {
                    return true
                }
            }
            return false
        }
        #endif
    }

    private static func allProcessIDs() -> [pid_t] {
        #if canImport(Darwin)
        return self.processIDs(type: UInt32(PROC_ALL_PIDS), typeInfo: 0)
        #else
        guard let entries = try? FileManager.default.contentsOfDirectory(atPath: "/proc") else { return [] }
        return entries.compactMap(pid_t.init)
        #endif
    }

    #if canImport(Darwin)
    /// Darwin exposes `stat` device IDs as signed values while vnode inspection uses the same bits unsigned.
    package static func darwinDeviceIdentifier(_ value: Int32) -> UInt64 {
        UInt64(UInt32(bitPattern: value))
    }
    #endif

    private static func processIDs(inProcessGroup processGroup: pid_t) -> [pid_t] {
        #if canImport(Darwin)
        return self.processIDs(type: UInt32(PROC_PGRP_ONLY), typeInfo: UInt32(bitPattern: processGroup))
        #else
        return self.allProcessIDs().filter { getpgid($0) == processGroup }
        #endif
    }

    #if canImport(Darwin)
    private static func processIDs(type: UInt32, typeInfo: UInt32) -> [pid_t] {
        let requiredBytes = proc_listpids(type, typeInfo, nil, 0)
        guard requiredBytes > 0 else { return [] }
        let stride = MemoryLayout<pid_t>.stride
        var pids = [pid_t](repeating: 0, count: Int(requiredBytes) / stride + 32)
        let actualBytes = pids.withUnsafeMutableBytes { buffer in
            proc_listpids(
                type,
                typeInfo,
                buffer.baseAddress,
                Int32(buffer.count))
        }
        guard actualBytes > 0 else { return [] }
        return Array(pids.prefix(Int(actualBytes) / stride)).filter { $0 > 0 }
    }
    #endif

    package let pid: pid_t
    package let processGroup: pid_t
    private let termination = TerminationState()
    private let observedProcessGroupMembers = ProcessIdentityState()
    private let outputPipes: Set<OutputPipeIdentity>
    private let outputTTYs: Set<OutputTTYIdentity>
    private let rootIdentity: TTYProcessTreeTerminator.ProcessIdentity?
    private let reservedPTYPrimaryDescriptor: OwnedFileDescriptorState?

    private init(
        pid: pid_t,
        outputPipes: Set<OutputPipeIdentity>,
        outputTTYs: Set<OutputTTYIdentity> = [],
        reservedPTYPrimaryDescriptor: OwnedFileDescriptorState? = nil)
    {
        self.pid = pid
        self.processGroup = pid
        self.outputPipes = outputPipes
        self.outputTTYs = outputTTYs
        self.reservedPTYPrimaryDescriptor = reservedPTYPrimaryDescriptor
        self.rootIdentity = TTYProcessTreeTerminator.processIdentity(for: pid)
        self.startWaiter()
    }

    package static func adopt(
        pid: pid_t,
        outputFileDescriptors: [Int32]) -> SpawnedProcessGroup
    {
        let outputPipes = Set(outputFileDescriptors.compactMap(OutputPipeIdentity.resolve(fileDescriptor:)))
        return SpawnedProcessGroup(pid: pid, outputPipes: outputPipes)
    }

    package static func launch(
        binary: String,
        arguments: [String],
        environment: [String: String],
        stdoutPipe: Pipe,
        stderrPipe: Pipe) throws -> SpawnedProcessGroup
    {
        #if canImport(Darwin)
        var fileActions: posix_spawn_file_actions_t?
        #else
        var fileActions = posix_spawn_file_actions_t()
        #endif
        guard posix_spawn_file_actions_init(&fileActions) == 0 else {
            throw LaunchError.setupFailed("posix_spawn_file_actions_init")
        }
        defer { posix_spawn_file_actions_destroy(&fileActions) }

        let stdoutRead = stdoutPipe.fileHandleForReading.fileDescriptor
        let stdoutWrite = stdoutPipe.fileHandleForWriting.fileDescriptor
        let stderrRead = stderrPipe.fileHandleForReading.fileDescriptor
        let stderrWrite = stderrPipe.fileHandleForWriting.fileDescriptor
        let outputPipes = Set(
            [stdoutRead, stderrRead].compactMap(OutputPipeIdentity.resolve(fileDescriptor:)))
        var fileActionResults = [
            posix_spawn_file_actions_addopen(&fileActions, STDIN_FILENO, "/dev/null", O_RDONLY, 0),
            posix_spawn_file_actions_adddup2(&fileActions, stdoutWrite, STDOUT_FILENO),
            posix_spawn_file_actions_adddup2(&fileActions, stderrWrite, STDERR_FILENO),
        ]
        for descriptor in Self.pipeDescriptorsToClose([stdoutRead, stdoutWrite, stderrRead, stderrWrite]) {
            fileActionResults.append(posix_spawn_file_actions_addclose(&fileActions, descriptor))
        }
        #if canImport(Glibc) || canImport(Musl)
        do {
            try PosixSpawnFileActionsCloseFrom.addCloseFrom(
                &fileActions,
                startingAt: STDERR_FILENO + 1)
        } catch {
            throw LaunchError.setupFailed(error.localizedDescription)
        }
        #endif
        guard fileActionResults.allSatisfy({ $0 == 0 }) else {
            throw LaunchError.setupFailed("posix_spawn file actions")
        }

        #if canImport(Darwin)
        var attributes: posix_spawnattr_t?
        #else
        var attributes = posix_spawnattr_t()
        #endif
        guard posix_spawnattr_init(&attributes) == 0 else {
            throw LaunchError.setupFailed("posix_spawnattr_init")
        }
        defer { posix_spawnattr_destroy(&attributes) }

        var emptySignalMask = sigset_t()
        guard sigemptyset(&emptySignalMask) == 0,
              posix_spawnattr_setsigmask(&attributes, &emptySignalMask) == 0
        else {
            throw LaunchError.setupFailed("posix_spawn signal mask")
        }
        #if canImport(Darwin)
        let flags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_CLOEXEC_DEFAULT
        #else
        let flags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK
        #endif
        guard posix_spawnattr_setflags(&attributes, Int16(flags)) == 0,
              posix_spawnattr_setpgroup(&attributes, 0) == 0
        else {
            throw LaunchError.setupFailed("posix_spawn process group")
        }

        var cArguments: [UnsafeMutablePointer<CChar>?] = ([binary] + arguments).map { strdup($0) }
        cArguments.append(nil)
        defer {
            for argument in cArguments {
                free(argument)
            }
        }

        var cEnvironment: [UnsafeMutablePointer<CChar>?] = environment.map { key, value in
            strdup("\(key)=\(value)")
        }
        cEnvironment.append(nil)
        defer {
            for entry in cEnvironment {
                free(entry)
            }
        }

        var pid: pid_t = 0
        let spawnResult = binary.withCString { path in
            posix_spawn(&pid, path, &fileActions, &attributes, cArguments, cEnvironment)
        }
        stdoutPipe.fileHandleForWriting.closeFile()
        stderrPipe.fileHandleForWriting.closeFile()
        guard spawnResult == 0 else {
            throw LaunchError.spawnFailed(String(cString: strerror(spawnResult)))
        }
        return SpawnedProcessGroup(pid: pid, outputPipes: outputPipes)
    }

    package static func launchPTY(
        binary: String,
        arguments: [String],
        environment: [String: String],
        workingDirectory: URL?,
        fileDescriptors: (primary: Int32, secondary: Int32)) throws -> SpawnedProcessGroup
    {
        let primaryFD = fileDescriptors.primary
        let secondaryFD = fileDescriptors.secondary
        guard let outputTTY = OutputTTYIdentity.resolve(fileDescriptor: secondaryFD) else {
            throw LaunchError.setupFailed("resolve PTY identity")
        }
        guard let reservedPTYPrimaryDescriptor = Self.reservePTYPrimaryDescriptor(primaryFD) else {
            throw LaunchError.setupFailed("reserve PTY primary descriptor")
        }
        #if canImport(Darwin)
        var fileActions: posix_spawn_file_actions_t?
        #else
        var fileActions = posix_spawn_file_actions_t()
        #endif
        guard posix_spawn_file_actions_init(&fileActions) == 0 else {
            throw LaunchError.setupFailed("posix_spawn_file_actions_init")
        }
        defer { posix_spawn_file_actions_destroy(&fileActions) }

        var fileActionResults = [
            posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, STDIN_FILENO),
            posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, STDOUT_FILENO),
            posix_spawn_file_actions_adddup2(&fileActions, secondaryFD, STDERR_FILENO),
        ]
        for descriptor in Self.pipeDescriptorsToClose([primaryFD, secondaryFD]) {
            fileActionResults.append(posix_spawn_file_actions_addclose(&fileActions, descriptor))
        }
        if let workingDirectory {
            fileActionResults.append(workingDirectory.path.withCString { path in
                PosixSpawnFileActionsCompatibility.addChangeDirectory(&fileActions, path: path)
            })
        }
        #if canImport(Glibc) || canImport(Musl)
        do {
            try PosixSpawnFileActionsCloseFrom.addCloseFrom(
                &fileActions,
                startingAt: STDERR_FILENO + 1)
        } catch {
            throw LaunchError.setupFailed(error.localizedDescription)
        }
        #endif
        guard fileActionResults.allSatisfy({ $0 == 0 }) else {
            throw LaunchError.setupFailed("posix_spawn PTY file actions")
        }

        #if canImport(Darwin)
        var attributes: posix_spawnattr_t?
        #else
        var attributes = posix_spawnattr_t()
        #endif
        guard posix_spawnattr_init(&attributes) == 0 else {
            throw LaunchError.setupFailed("posix_spawnattr_init")
        }
        defer { posix_spawnattr_destroy(&attributes) }

        var emptySignalMask = sigset_t()
        guard sigemptyset(&emptySignalMask) == 0,
              posix_spawnattr_setsigmask(&attributes, &emptySignalMask) == 0
        else {
            throw LaunchError.setupFailed("posix_spawn PTY signal mask")
        }
        #if canImport(Darwin)
        let flags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_CLOEXEC_DEFAULT
        #else
        let flags = POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_SETSIGMASK
        #endif
        guard posix_spawnattr_setflags(&attributes, Int16(flags)) == 0,
              posix_spawnattr_setpgroup(&attributes, 0) == 0
        else {
            throw LaunchError.setupFailed("posix_spawn PTY process group")
        }

        var cArguments: [UnsafeMutablePointer<CChar>?] = ([binary] + arguments).map { strdup($0) }
        cArguments.append(nil)
        defer {
            for argument in cArguments {
                free(argument)
            }
        }

        var cEnvironment: [UnsafeMutablePointer<CChar>?] = environment.map { key, value in
            strdup("\(key)=\(value)")
        }
        cEnvironment.append(nil)
        defer {
            for entry in cEnvironment {
                free(entry)
            }
        }

        var pid: pid_t = 0
        let spawnResult = binary.withCString { path in
            posix_spawn(&pid, path, &fileActions, &attributes, cArguments, cEnvironment)
        }
        guard spawnResult == 0 else {
            throw LaunchError.spawnFailed(String(cString: strerror(spawnResult)))
        }
        return SpawnedProcessGroup(
            pid: pid,
            outputPipes: [],
            outputTTYs: [outputTTY],
            reservedPTYPrimaryDescriptor: reservedPTYPrimaryDescriptor)
    }

    package var isRunning: Bool {
        !self.termination.hasObservedExit
    }

    package var terminationStatus: Int32? {
        self.termination.value
    }

    package var exitObservationDate: Date? {
        self.termination.observationDate
    }

    package var hasResidualProcessGroup: Bool {
        Self.processGroupExists(self.processGroup)
    }

    @discardableResult
    package func terminateSynchronously(grace: TimeInterval = 0.4) -> Int32? {
        let deadline = Date().addingTimeInterval(max(0, grace))
        var processIdentities = self.currentResidualProcessIdentities(includeDescendants: true)
        processIdentities.formUnion(self.currentProcessGroupMemberIdentities())
        if self.isRunning, let rootIdentity = self.rootIdentity {
            processIdentities.insert(rootIdentity)
        }
        Self.signal(processIdentities: processIdentities, signal: SIGTERM)

        while processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)),
              Date() < deadline
        {
            usleep(20000)
        }

        processIdentities.formUnion(self.currentResidualProcessIdentities(includeDescendants: self.isRunning))
        processIdentities.formUnion(self.currentProcessGroupMemberIdentities())
        if self.isRunning, let rootIdentity = self.rootIdentity {
            processIdentities.insert(rootIdentity)
        } else if let rootIdentity = self.rootIdentity {
            processIdentities.remove(rootIdentity)
        }
        Self.signal(processIdentities: processIdentities, signal: SIGKILL)

        let killDeadline = Date().addingTimeInterval(max(0, grace))
        while processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)),
              Date() < killDeadline
        {
            usleep(20000)
        }
        return self.finishSynchronously()
    }

    /// Abort a process whose output channel is no longer safe to inspect.
    ///
    /// Unlike normal cleanup, this deliberately avoids the system-wide output-holder scan. The caller closes
    /// the output descriptor first, then this method targets only the launch process tree and dedicated process
    /// group so TERM-to-KILL escalation remains bounded even when process enumeration is slow under load.
    @discardableResult
    package func abortSynchronously(grace: TimeInterval = 0.4) -> Int32? {
        let grace = max(0, grace)
        let termDeadline = Date().addingTimeInterval(grace)
        var processIdentities = self.currentAbortProcessIdentities()

        self.signalOwnedProcessGroup(SIGTERM)
        Self.signal(processIdentities: processIdentities, signal: SIGTERM)

        while self.abortTargetsRemain(processIdentities), Date() < termDeadline {
            usleep(20000)
        }

        processIdentities.formUnion(self.currentAbortProcessIdentities())
        self.signalOwnedProcessGroup(SIGKILL)
        Self.signal(processIdentities: processIdentities, signal: SIGKILL)

        let killDeadline = Date().addingTimeInterval(grace)
        while self.abortTargetsRemain(processIdentities), Date() < killDeadline {
            usleep(20000)
        }
        return self.finishSynchronously()
    }

    @discardableResult
    package func finishSynchronously(timeout: TimeInterval = 1) -> Int32? {
        self.termination.requestReap()
        let deadline = Date().addingTimeInterval(max(0, timeout))
        while self.terminationStatus == nil, Date() < deadline {
            usleep(10000)
        }
        return self.terminationStatus
    }

    @discardableResult
    package func terminate(grace: TimeInterval = 0.4) async -> Int32? {
        if self.isRunning {
            let killDeadline = Date().addingTimeInterval(max(0, grace))
            var processIdentities = self.currentResidualProcessIdentities(includeDescendants: true)
            processIdentities.formUnion(self.currentProcessGroupMemberIdentities())
            if let rootIdentity = TTYProcessTreeTerminator.processIdentity(for: self.pid) {
                processIdentities.insert(rootIdentity)
            }
            Self.signal(processIdentities: processIdentities, signal: SIGTERM)
            _ = await self.waitForExit(timeout: max(0, killDeadline.timeIntervalSinceNow))
            while processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)),
                  Date() < killDeadline
            {
                try? await Task.sleep(for: .milliseconds(20))
            }

            processIdentities.formUnion(self.currentResidualProcessIdentities(includeDescendants: false))
            processIdentities.formUnion(self.currentProcessGroupMemberIdentities())
            if self.isRunning {
                processIdentities.formUnion(self.currentResidualProcessIdentities(includeDescendants: true))
                if let rootIdentity = TTYProcessTreeTerminator.processIdentity(for: self.pid) {
                    processIdentities.insert(rootIdentity)
                }
                Self.signal(processIdentities: processIdentities, signal: SIGKILL)
                _ = await self.waitForExit(timeout: grace)
            } else {
                Self.signal(processIdentities: processIdentities, signal: SIGKILL)
            }
            _ = await self.waitForResidualProcessesExit(processIdentities, timeout: grace)
            await self.finish()
            return self.terminationStatus
        }
        await self.terminateResidualProcesses(grace: grace)
        await self.finish()
        return self.terminationStatus
    }

    package func terminateResidualProcesses(grace: TimeInterval = 0.4) async {
        let deadline = Date().addingTimeInterval(max(0, grace))
        var processIdentities = self.currentResidualProcessIdentities(includeDescendants: false)
        processIdentities.formUnion(self.currentProcessGroupMemberIdentities())
        if self.isRunning {
            processIdentities.formUnion(self.currentResidualProcessIdentities(includeDescendants: true))
            if let rootIdentity = TTYProcessTreeTerminator.processIdentity(for: self.pid) {
                processIdentities.insert(rootIdentity)
            }
        }
        Self.signal(processIdentities: processIdentities, signal: SIGTERM)

        while processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)) {
            guard Date() < deadline else { break }
            try? await Task.sleep(for: .milliseconds(20))
        }

        processIdentities.formUnion(self.currentResidualProcessIdentities(includeDescendants: false))
        processIdentities.formUnion(self.currentProcessGroupMemberIdentities())
        if self.isRunning {
            processIdentities.formUnion(self.currentResidualProcessIdentities(includeDescendants: true))
            if let rootIdentity = TTYProcessTreeTerminator.processIdentity(for: self.pid) {
                processIdentities.insert(rootIdentity)
            }
        }
        guard processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)) else {
            return
        }
        Self.signal(processIdentities: processIdentities, signal: SIGKILL)
        _ = await self.waitForResidualProcessesExit(processIdentities, timeout: grace)
    }

    package func finish() async {
        self.termination.requestReap()
        _ = await self.waitForTerminationStatus(timeout: 1)
    }

    private func startWaiter() {
        let pid = self.pid
        let processGroup = self.processGroup
        let observedProcessGroupMembers = self.observedProcessGroupMembers
        let termination = self.termination
        DispatchQueue.global(qos: .userInitiated).async {
            var info = siginfo_t()
            var waitResult: Int32
            repeat {
                waitResult = waitid(P_PID, id_t(pid), &info, WEXITED | WNOWAIT)
            } while waitResult == -1 && errno == EINTR
            guard waitResult == 0 else {
                termination.observeExit()
                termination.resolve(1)
                return
            }

            // The exited root remains unreaped here, so its PID cannot yet be reused as
            // an unrelated process-group ID.
            observedProcessGroupMembers.formUnion(
                Self.processGroupMemberIdentities(processGroup: processGroup, excluding: pid))
            termination.observeExit()
            termination.waitForReapRequest(timeout: 30)

            var rawStatus: Int32 = 0
            var result: pid_t
            repeat {
                result = waitpid(pid, &rawStatus, 0)
            } while result == -1 && errno == EINTR

            let status = result == pid ? Self.exitStatus(from: rawStatus) : 1
            termination.resolve(status)
        }
    }

    private func waitForExit(timeout: TimeInterval) async -> Int32? {
        let deadline = Date().addingTimeInterval(max(0, timeout))
        while self.isRunning, Date() < deadline {
            try? await Task.sleep(for: .milliseconds(20))
        }
        return self.terminationStatus
    }

    private func waitForTerminationStatus(timeout: TimeInterval) async -> Int32? {
        let deadline = Date().addingTimeInterval(max(0, timeout))
        while self.terminationStatus == nil, Date() < deadline {
            try? await Task.sleep(for: .milliseconds(20))
        }
        return self.terminationStatus
    }

    private func waitForResidualProcessesExit(
        _ processIdentities: Set<TTYProcessTreeTerminator.ProcessIdentity>,
        timeout: TimeInterval) async -> Bool
    {
        let deadline = Date().addingTimeInterval(max(0, timeout))
        while Date() < deadline {
            guard processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)) else {
                return true
            }
            try? await Task.sleep(for: .milliseconds(20))
        }
        return !processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:))
    }

    private func currentResidualProcessIdentities(
        includeDescendants: Bool) -> Set<TTYProcessTreeTerminator.ProcessIdentity>
    {
        var identities = self.currentOutputHolderIdentities()
        identities.formUnion(self.observedProcessGroupMembers.snapshot)
        if includeDescendants {
            identities.formUnion(
                TTYProcessTreeTerminator.descendantPIDs(of: self.pid)
                    .compactMap(TTYProcessTreeTerminator.processIdentity(for:)))
        }
        return identities
    }

    private func currentAbortProcessIdentities() -> Set<TTYProcessTreeTerminator.ProcessIdentity> {
        var identities = self.observedProcessGroupMembers.snapshot
        identities.formUnion(self.currentProcessGroupMemberIdentities())
        guard let rootIdentity = self.rootIdentity,
              TTYProcessTreeTerminator.isCurrent(rootIdentity)
        else {
            return identities
        }

        identities.insert(rootIdentity)
        identities.formUnion(
            TTYProcessTreeTerminator.descendantPIDs(of: self.pid)
                .compactMap(TTYProcessTreeTerminator.processIdentity(for:)))
        return identities
    }

    private func signalOwnedProcessGroup(_ signal: Int32) {
        guard let rootIdentity = self.rootIdentity,
              TTYProcessTreeTerminator.isCurrent(rootIdentity)
        else { return }
        _ = kill(-self.processGroup, signal)
    }

    private func abortTargetsRemain(
        _ processIdentities: Set<TTYProcessTreeTerminator.ProcessIdentity>) -> Bool
    {
        processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:)) || self.hasResidualProcessGroup
    }

    private func currentProcessGroupMemberIdentities() -> Set<TTYProcessTreeTerminator.ProcessIdentity> {
        if self.termination.hasObservedExit, self.termination.value == nil {
            let identities = Self.processGroupMemberIdentities(
                processGroup: self.processGroup,
                excluding: self.pid)
            self.observedProcessGroupMembers.formUnion(identities)
            return identities
        }

        guard let rootIdentity = self.rootIdentity
        else {
            return []
        }

        let identities = Self.processGroupMemberIdentities(
            processGroup: self.processGroup,
            rootIdentity: rootIdentity,
            excluding: self.pid)
        self.observedProcessGroupMembers.formUnion(identities)
        return identities
    }

    private static func processGroupMemberIdentities(
        processGroup: pid_t,
        rootIdentity: TTYProcessTreeTerminator.ProcessIdentity,
        excluding excludedPID: pid_t)
        -> Set<TTYProcessTreeTerminator.ProcessIdentity>
    {
        guard TTYProcessTreeTerminator.isCurrent(rootIdentity) else { return [] }

        let identities = Self.processGroupMemberIdentities(
            processGroup: processGroup,
            excluding: excludedPID)
        guard TTYProcessTreeTerminator.isCurrent(rootIdentity) else { return [] }
        return identities
    }

    private static func processGroupMemberIdentities(
        processGroup: pid_t,
        excluding excludedPID: pid_t)
        -> Set<TTYProcessTreeTerminator.ProcessIdentity>
    {
        Set(self.processIDs(inProcessGroup: processGroup)
            .compactMap { pid -> TTYProcessTreeTerminator.ProcessIdentity? in
                guard pid != getpid(),
                      pid != excludedPID,
                      let identity = TTYProcessTreeTerminator.processIdentity(for: pid),
                      getpgid(pid) == processGroup,
                      TTYProcessTreeTerminator.isCurrent(identity)
                else {
                    return nil
                }
                return identity
            })
    }

    private static func processGroupExists(_ processGroup: pid_t) -> Bool {
        errno = 0
        return kill(-processGroup, 0) == 0 || errno == EPERM
    }

    private static func signal(
        processIdentities: Set<TTYProcessTreeTerminator.ProcessIdentity>,
        signal: Int32)
    {
        for identity in processIdentities where TTYProcessTreeTerminator.isCurrent(identity) {
            _ = kill(identity.pid, signal)
        }
    }

    package static func pipeDescriptorsToClose(_ descriptors: [Int32]) -> [Int32] {
        Array(Set(descriptors.filter { $0 > STDERR_FILENO })).sorted()
    }

    private static func exitStatus(from rawStatus: Int32) -> Int32 {
        let signal = rawStatus & 0x7F
        return signal == 0 ? (rawStatus >> 8) & 0xFF : signal
    }
}

extension SpawnedProcessGroup {
    private func currentOutputHolderIdentities() -> Set<TTYProcessTreeTerminator.ProcessIdentity> {
        let excludedPIDs: Set<pid_t> = [getpid(), self.pid]
        return Self.outputHolderIdentities(
            outputPipes: self.outputPipes,
            outputTTYs: self.outputTTYs,
            excludedPIDs: excludedPIDs)
    }

    /// Serializes transfer and closure of a descriptor owned by the process group.
    private final class OwnedFileDescriptorState: @unchecked Sendable {
        private let lock = NSLock()
        private var fileDescriptor: Int32?

        init(fileDescriptor: Int32) {
            self.fileDescriptor = fileDescriptor
        }

        deinit {
            self.discard()
        }

        func take() -> Int32? {
            self.lock.withLock {
                defer { self.fileDescriptor = nil }
                return self.fileDescriptor
            }
        }

        @discardableResult
        func discard() -> Bool {
            guard let fileDescriptor = self.take() else { return false }
            _ = close(fileDescriptor)
            return true
        }
    }

    /// The lock makes the descriptor/completion transition one-shot across worker and expiry queues.
    private final class OutputHolderCleanupLease: @unchecked Sendable {
        let outputPipes: Set<OutputPipeIdentity>
        let outputTTYs: Set<OutputTTYIdentity>
        let excludedPIDs: Set<pid_t>
        let grace: TimeInterval
        let preKillSnapshotHook: (@Sendable () -> Void)?
        let preKillDelay: TimeInterval

        private let deadline: DispatchTime
        private let completion = DispatchGroup()
        private let lock = NSLock()
        private var duplicatedPrimaryFileDescriptor: Int32?
        private var finished = false

        init(
            duplicatedPrimaryFileDescriptor: Int32,
            outputPipes: Set<OutputPipeIdentity>,
            outputTTYs: Set<OutputTTYIdentity>,
            excludedPIDs: Set<pid_t>,
            grace: TimeInterval,
            preKillSnapshotHook: (@Sendable () -> Void)?,
            preKillDelay: TimeInterval,
            maxLifetime: TimeInterval)
        {
            self.duplicatedPrimaryFileDescriptor = duplicatedPrimaryFileDescriptor
            self.outputPipes = outputPipes
            self.outputTTYs = outputTTYs
            self.excludedPIDs = excludedPIDs
            self.grace = max(0, grace)
            self.preKillSnapshotHook = preKillSnapshotHook
            self.preKillDelay = max(0, preKillDelay)
            self.deadline = .now() + max(0, maxLifetime)
            self.completion.enter()
        }

        var isActive: Bool {
            self.withActiveState { true } == true
        }

        func scheduleExpiry() {
            let deadline = self.deadline
            DispatchQueue.global(qos: .utility).asyncAfter(deadline: deadline) { [self] in
                self.finish()
            }
        }

        func withActiveState<T>(_ operation: () -> T) -> T? {
            var descriptorToClose: Int32?
            self.lock.lock()
            if !self.finished, DispatchTime.now().uptimeNanoseconds >= self.deadline.uptimeNanoseconds {
                self.finished = true
                descriptorToClose = self.duplicatedPrimaryFileDescriptor
                self.duplicatedPrimaryFileDescriptor = nil
            }
            guard !self.finished else {
                self.lock.unlock()
                self.closeAndComplete(descriptorToClose)
                return nil
            }
            let result = operation()
            self.lock.unlock()
            return result
        }

        func finish() {
            let descriptorToClose = self.lock.withLock { () -> Int32? in
                guard !self.finished else { return nil }
                self.finished = true
                defer { self.duplicatedPrimaryFileDescriptor = nil }
                return self.duplicatedPrimaryFileDescriptor
            }
            self.closeAndComplete(descriptorToClose)
        }

        func waitForCompletion(timeout: TimeInterval) -> Bool {
            self.completion.wait(timeout: .now() + max(0, timeout)) == .success
        }

        private func closeAndComplete(_ descriptor: Int32?) {
            guard let descriptor else { return }
            _ = close(descriptor)
            self.completion.leave()
        }
    }

    /// Hard-stop a live PTY root without making the caller wait on system-wide holder discovery.
    @discardableResult
    package func hardStopLivePTYRootSynchronously(grace: TimeInterval = 0.4) -> Int32? {
        #if DEBUG
        // Task-local values do not cross a GCD boundary, so capture the test delay before dispatching.
        let discoveryDelay = max(0, SpawnedProcessGroupTestingOverrides.outputHolderDiscoveryDelay ?? 0)
        let preKillSnapshotHook = SpawnedProcessGroupTestingOverrides.outputHolderPreKillSnapshotHook
        let preKillDelay = max(0, SpawnedProcessGroupTestingOverrides.outputHolderPreKillDelay ?? 0)
        let cleanupMaxLifetime = max(0, SpawnedProcessGroupTestingOverrides.outputHolderCleanupMaxLifetime ?? 15)
        #else
        let discoveryDelay: TimeInterval = 0
        let preKillSnapshotHook: (@Sendable () -> Void)? = nil
        let preKillDelay: TimeInterval = 0
        let cleanupMaxLifetime: TimeInterval = 15
        #endif
        guard let reservedPrimaryFileDescriptor = self.reservedPTYPrimaryDescriptor?.take() else {
            return self.abortSynchronously(grace: grace)
        }
        // Production caps the lease at 15 seconds; DEBUG fixtures may add their artificial delay separately.
        let lease = OutputHolderCleanupLease(
            duplicatedPrimaryFileDescriptor: reservedPrimaryFileDescriptor,
            outputPipes: self.outputPipes,
            outputTTYs: self.outputTTYs,
            excludedPIDs: [getpid(), self.pid],
            grace: grace,
            preKillSnapshotHook: preKillSnapshotHook,
            preKillDelay: preKillDelay,
            maxLifetime: cleanupMaxLifetime)
        lease.scheduleExpiry()
        let status = self.abortSynchronously(grace: grace)
        DispatchQueue.global(qos: .utility).async {
            defer { lease.finish() }
            if discoveryDelay > 0 {
                Thread.sleep(forTimeInterval: discoveryDelay)
            }
            SpawnedProcessGroup.terminateOutputHoldersSynchronously(lease: lease)
        }
        _ = lease.waitForCompletion(timeout: 0.2)
        return status
    }

    private static func reservePTYPrimaryDescriptor(_ fileDescriptor: Int32) -> OwnedFileDescriptorState? {
        #if DEBUG
        guard !SpawnedProcessGroupTestingOverrides.forcePTYPrimaryDescriptorReservationFailure else { return nil }
        #endif
        let duplicate = fcntl(fileDescriptor, F_DUPFD_CLOEXEC, STDERR_FILENO + 1)
        guard duplicate >= 0 else { return nil }
        return OwnedFileDescriptorState(fileDescriptor: duplicate)
    }

    package func discardReservedPTYPrimaryDescriptor() {
        self.reservedPTYPrimaryDescriptor?.discard()
    }

    private static func outputHolderIdentities(
        outputPipes: Set<OutputPipeIdentity>,
        outputTTYs: Set<OutputTTYIdentity>,
        excludedPIDs: Set<pid_t>) -> Set<TTYProcessTreeTerminator.ProcessIdentity>
    {
        var holderPIDs = OutputPipeIdentity.holderPIDs(for: outputPipes)
        holderPIDs.formUnion(OutputTTYIdentity.holderPIDs(for: outputTTYs))
        return Set(holderPIDs.subtracting(excludedPIDs).compactMap(TTYProcessTreeTerminator.processIdentity(for:)))
    }

    private static func terminateOutputHoldersSynchronously(lease: OutputHolderCleanupLease) {
        guard lease.isActive else { return }
        let processIdentities = Self.outputHolderIdentities(
            outputPipes: lease.outputPipes,
            outputTTYs: lease.outputTTYs,
            excludedPIDs: lease.excludedPIDs)
        guard lease.isActive,
              Self.signal(processIdentities: processIdentities, signal: SIGTERM, lease: lease)
        else { return }

        Self.waitWhileCurrent(processIdentities, until: Date().addingTimeInterval(lease.grace), lease: lease)

        while lease.isActive {
            let currentIdentities = Self.outputHolderIdentities(
                outputPipes: lease.outputPipes,
                outputTTYs: lease.outputTTYs,
                excludedPIDs: lease.excludedPIDs)
            guard lease.isActive else { return }
            guard !currentIdentities.isEmpty else { return }
            lease.preKillSnapshotHook?()
            if lease.preKillDelay > 0 {
                Thread.sleep(forTimeInterval: lease.preKillDelay)
            }
            guard lease.isActive,
                  Self.signal(processIdentities: currentIdentities, signal: SIGKILL, lease: lease)
            else { return }
            Self.waitWhileCurrent(currentIdentities, until: Date().addingTimeInterval(lease.grace), lease: lease)
        }
    }

    private static func signal(
        processIdentities: Set<TTYProcessTreeTerminator.ProcessIdentity>,
        signal: Int32,
        lease: OutputHolderCleanupLease) -> Bool
    {
        for identity in processIdentities {
            guard lease.withActiveState({
                guard TTYProcessTreeTerminator.isCurrent(identity) else { return }
                _ = kill(identity.pid, signal)
            }) != nil else {
                return false
            }
        }
        return lease.isActive
    }

    private static func waitWhileCurrent(
        _ processIdentities: Set<TTYProcessTreeTerminator.ProcessIdentity>,
        until deadline: Date,
        lease: OutputHolderCleanupLease)
    {
        while Date() < deadline {
            guard let targetsRemain = lease.withActiveState({
                processIdentities.contains(where: TTYProcessTreeTerminator.isCurrent(_:))
            }), targetsRemain else { return }
            usleep(20000)
        }
    }

    #if DEBUG
    package static func withOutputHolderDiscoveryDelayForTesting<T>(
        _ delay: TimeInterval,
        operation: () throws -> T) rethrows -> T
    {
        try SpawnedProcessGroupTestingOverrides.$outputHolderDiscoveryDelay.withValue(delay, operation: operation)
    }

    package static func withOutputHolderPreKillDelayForTesting<T>(
        _ delay: TimeInterval,
        operation: () throws -> T) rethrows -> T
    {
        try SpawnedProcessGroupTestingOverrides.$outputHolderPreKillDelay.withValue(delay, operation: operation)
    }

    package static func withOutputHolderPreKillSnapshotHookForTesting<T>(
        _ hook: @escaping @Sendable () -> Void,
        operation: () throws -> T) rethrows -> T
    {
        try SpawnedProcessGroupTestingOverrides.$outputHolderPreKillSnapshotHook.withValue(
            hook,
            operation: operation)
    }

    package static func withOutputHolderCleanupMaxLifetimeForTesting<T>(
        _ maxLifetime: TimeInterval,
        operation: () throws -> T) rethrows -> T
    {
        try SpawnedProcessGroupTestingOverrides.$outputHolderCleanupMaxLifetime.withValue(
            maxLifetime,
            operation: operation)
    }

    package static func withPTYPrimaryDescriptorReservationFailureForTesting<T>(
        _ operation: () throws -> T) rethrows -> T
    {
        try SpawnedProcessGroupTestingOverrides.$forcePTYPrimaryDescriptorReservationFailure.withValue(
            true,
            operation: operation)
    }

    package static func _test_ownedDescriptorTakeOnce(
        ownedFileDescriptor: Int32) -> (first: Int32?, second: Int32?, discardAfterTake: Bool)
    {
        let state = OwnedFileDescriptorState(fileDescriptor: ownedFileDescriptor)
        let first = state.take()
        return (first, state.take(), state.discard())
    }

    package static func _test_ownedDescriptorDiscardTwice(
        ownedFileDescriptor: Int32) -> (first: Bool, second: Bool)
    {
        let state = OwnedFileDescriptorState(fileDescriptor: ownedFileDescriptor)
        return (state.discard(), state.discard())
    }

    package static func _test_ownedDescriptorDeinit(ownedFileDescriptor: Int32) {
        _ = OwnedFileDescriptorState(fileDescriptor: ownedFileDescriptor)
    }

    package static func _test_outputHolderCleanupLeaseExpiry(
        ownedFileDescriptor: Int32,
        maxLifetime: TimeInterval,
        waitTimeout: TimeInterval) -> (completed: Bool, active: Bool)
    {
        let lease = OutputHolderCleanupLease(
            duplicatedPrimaryFileDescriptor: ownedFileDescriptor,
            outputPipes: [],
            outputTTYs: [],
            excludedPIDs: [],
            grace: 0,
            preKillSnapshotHook: nil,
            preKillDelay: 0,
            maxLifetime: maxLifetime)
        lease.scheduleExpiry()
        let completed = lease.waitForCompletion(timeout: waitTimeout)
        return (completed, lease.isActive)
    }
    #endif
}
