//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

#if os(Linux)

import ContainerizationOS
import Foundation
import Synchronization

struct ProcessSubscription: Sendable {
    fileprivate let id: UUID
}

/// Protocol for running commands and waiting for their exit
protocol CommandRunner: Sendable {
    func start(_ cmd: inout Command) throws -> ProcessSubscription
    func wait(_ cmd: Command, subscription: ProcessSubscription) async throws -> Int32
}

struct DirectCommandRunner: CommandRunner {
    func start(_ cmd: inout Command) throws -> ProcessSubscription {
        try cmd.start()
        return ProcessSubscription(id: UUID())
    }

    func wait(_ cmd: Command, subscription: ProcessSubscription) async throws -> Int32 {
        var rus = rusage()
        var ws = Int32()

        let result = wait4(cmd.pid, &ws, 0, &rus)
        guard result == cmd.pid else {
            throw POSIXError(.init(rawValue: errno)!)
        }
        return Command.toExitStatus(ws)
    }
}

final class ReaperCommandRunner: CommandRunner, Sendable {
    private struct Subscriber {
        let continuation: AsyncStream<(pid: pid_t, status: Int32)>.Continuation
        let stream: AsyncStream<(pid: pid_t, status: Int32)>
    }

    private let subscribers: Mutex<[UUID: Subscriber]> = Mutex([:])

    func start(_ cmd: inout Command) throws -> ProcessSubscription {
        // Subscribe before starting to avoid missing fast exits
        let id = UUID()
        let (stream, continuation) = AsyncStream<(pid: pid_t, status: Int32)>.makeStream()

        subscribers.withLock { subscribers in
            subscribers[id] = Subscriber(continuation: continuation, stream: stream)
        }

        try cmd.start()

        return ProcessSubscription(id: id)
    }

    func wait(_ cmd: Command, subscription: ProcessSubscription) async throws -> Int32 {
        let pid = cmd.pid
        let id = subscription.id

        defer {
            subscribers.withLock { subscribers in
                subscribers[id]?.continuation.finish()
                subscribers.removeValue(forKey: id)
            }
        }

        // Get the stream from the subscriber
        guard let stream = subscribers.withLock({ $0[id]?.stream }) else {
            throw POSIXError(.ECHILD)
        }

        for await (exitPid, status) in stream {
            if exitPid == pid {
                return status
            }
        }

        throw POSIXError(.ECHILD)
    }

    /// Broadcast exit to all subscribers
    func notifyExit(pid: pid_t, status: Int32) {
        subscribers.withLock { subscribers in
            for subscriber in subscribers.values {
                subscriber.continuation.yield((pid, status))
            }
        }
    }
}

#endif
