//===----------------------------------------------------------------------===//
// 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 Logging
import Synchronization

final class ProcessSupervisor: Sendable {
    private let poller: Epoll
    private let handlers = Mutex<[Int32: @Sendable (Epoll.Mask) -> Void]>([:])

    private let queue: DispatchQueue
    // `DispatchSourceSignal` is thread-safe.
    private nonisolated(unsafe) let source: DispatchSourceSignal

    private struct State {
        var processes: [any ContainerProcess] = []
        var log: Logger?
    }

    private let state: Mutex<State>
    private let reaperCommandRunner = ReaperCommandRunner()

    func setLog(_ log: Logger?) {
        self.state.withLock { $0.log = log }
    }

    static let `default` = ProcessSupervisor()

    private init() {
        let queue = DispatchQueue(label: "process-supervisor")
        self.source = DispatchSource.makeSignalSource(signal: SIGCHLD, queue: queue)
        self.queue = queue
        self.poller = try! Epoll()
        self.state = Mutex(State())
        let t = Thread {
            while true {
                guard let events = self.poller.wait() else {
                    return
                }
                if events.isEmpty {
                    return
                }
                for event in events {
                    let handler = self.handlers.withLock { $0[event.fd] }
                    handler?(event.mask)
                }
            }
        }
        t.start()
    }

    /// Register a file descriptor for epoll monitoring with a handler.
    ///
    /// The handler is stored before the fd is added to epoll, ensuring no
    /// events are missed.
    func registerFd(
        _ fd: Int32,
        mask: Epoll.Mask = [.input, .output],
        handler: @escaping @Sendable (Epoll.Mask) -> Void
    ) throws {
        self.handlers.withLock { $0[fd] = handler }
        do {
            try self.poller.add(fd, mask: mask)
        } catch {
            self.handlers.withLock { _ = $0.removeValue(forKey: fd) }
            throw error
        }
    }

    /// Remove a file descriptor from epoll monitoring and discard its handler.
    func unregisterFd(_ fd: Int32) throws {
        self.handlers.withLock { _ = $0.removeValue(forKey: fd) }
        try self.poller.delete(fd)
    }

    func ready() {
        self.source.setEventHandler {
            self.handleSignal()
        }
        self.source.resume()
    }

    private func handleSignal() {
        dispatchPrecondition(condition: .onQueue(queue))

        let exited = Reaper.reap()

        for (pid, status) in exited {
            reaperCommandRunner.notifyExit(pid: pid, status: status)
        }

        self.state.withLock { state in
            state.log?.debug("received SIGCHLD, reaping processes")
            state.log?.debug("finished wait4 of \(exited.count) processes")
            state.log?.debug("checking for exit of managed process", metadata: ["exits": "\(exited)", "processes": "\(state.processes.count)"])

            let exitedProcesses = state.processes.filter { proc in
                exited.contains { pid, _ in
                    proc.pid == pid
                }
            }

            for proc in exitedProcesses {
                guard let pid = proc.pid else {
                    continue
                }

                if let status = exited[pid] {
                    state.log?.debug(
                        "managed process exited",
                        metadata: [
                            "pid": "\(pid)",
                            "status": "\(status)",
                            "count": "\(state.processes.count - 1)",
                        ])
                    proc.setExit(status)
                    state.processes.removeAll(where: { $0.pid == pid })
                }
            }
        }
    }

    func start(process: any ContainerProcess) async throws -> Int32 {
        self.state.withLock { state in
            state.log?.debug("in supervisor lock to start process")
            state.processes.append(process)
        }
        do {
            return try await process.start()
        } catch {
            self.state.withLock { state in
                state.processes.removeAll(where: { $0.id == process.id })
            }
            throw error
        }
    }

    /// Get a Runc instance configured with the reaper command runner
    func getRuncWithReaper(_ base: Runc = Runc()) -> Runc {
        var runc = base
        runc.commandRunner = reaperCommandRunner
        return runc
    }

    deinit {
        source.cancel()
        poller.shutdown()
    }
}

#endif
