//===----------------------------------------------------------------------===//
// Copyright © 2025-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.
//===----------------------------------------------------------------------===//

import CShim
import Foundation
import Synchronization

#if canImport(Darwin)
import Darwin
private let _kill = Darwin.kill
#elseif canImport(Musl)
import Musl
private let _kill = Musl.kill
#elseif canImport(Glibc)
import Glibc
private let _kill = Glibc.kill
#endif

/// Use a command to run an executable.
public struct Command: Sendable {
    /// Path to the executable binary.
    public var executable: String
    /// Arguments provided to the binary.
    public var arguments: [String]
    /// Environment variables for the process.
    public var environment: [String]
    /// The directory where the process should execute.
    public var directory: String?
    /// Additional files to pass to the process.
    public var extraFiles: [FileHandle]
    /// The standard input.
    public var stdin: FileHandle?
    /// The standard output.
    public var stdout: FileHandle?
    /// The standard error.
    public var stderr: FileHandle?

    private let state: State

    /// System level attributes to set on the process.
    public struct Attrs: Sendable {
        /// Set pgroup for the new process.
        public var setPGroup: Bool
        /// Make the new process group the foreground process group (requires setPGroup).
        public var setForegroundPGroup: Bool
        /// Inherit the real uid/gid of the parent.
        public var resetIDs: Bool
        /// Reset the child's signal handlers to the default.
        public var setSignalDefault: Bool
        /// The initial signal mask for the process.
        public var signalMask: UInt32
        /// Create a new session for the process.
        public var setsid: Bool
        /// Set the controlling terminal for the process to fd 0.
        public var setctty: Bool
        /// Set the process user ID.
        public var uid: UInt32?
        /// Set the process group ID.
        public var gid: UInt32?
        /// Signal to send when parent process dies (Linux only).
        public var pdeathSignal: Int32?

        public init(
            setPGroup: Bool = false,
            setForegroundPGroup: Bool = false,
            resetIDs: Bool = false,
            setSignalDefault: Bool = true,
            signalMask: UInt32 = 0,
            setsid: Bool = false,
            setctty: Bool = false,
            uid: UInt32? = nil,
            gid: UInt32? = nil,
            pdeathSignal: Int32? = nil
        ) {
            self.setPGroup = setPGroup
            self.setForegroundPGroup = setForegroundPGroup
            self.resetIDs = resetIDs
            self.setSignalDefault = setSignalDefault
            self.signalMask = signalMask
            self.setsid = setsid
            self.setctty = setctty
            self.uid = uid
            self.gid = gid
            self.pdeathSignal = pdeathSignal
        }
    }

    private final class State: Sendable {
        let pid: Atomic<pid_t> = Atomic(-1)
    }

    /// Attributes to set on the process.
    public var attrs = Attrs()

    /// System level process identifier.
    public var pid: Int32 { self.state.pid.load(ordering: .acquiring) }

    public init(
        _ executable: String,
        arguments: [String] = [],
        environment: [String] = environment(),
        directory: String? = nil,
        extraFiles: [FileHandle] = []
    ) {
        self.executable = executable
        self.arguments = arguments
        self.environment = environment
        self.extraFiles = extraFiles
        self.directory = directory
        self.state = State()
    }

    public static func environment() -> [String] {
        ProcessInfo.processInfo.environment
            .map { "\($0)=\($1)" }
    }
}

extension Command {
    public enum Error: Swift.Error, CustomStringConvertible {
        case processRunning

        public var description: String {
            switch self {
            case .processRunning:
                return "the process is already running"
            }
        }
    }
}

extension Command {
    @discardableResult
    public func kill(_ signal: Int32) -> Int32? {
        let pid = self.pid
        guard pid > 0 else {
            return nil
        }
        return _kill(pid, signal)
    }
}

extension Command {
    /// Start the process.
    public func start() throws {
        guard self.pid == -1 else {
            throw Error.processRunning
        }
        let child = try execute()
        self.state.pid.store(child, ordering: .releasing)
    }

    /// Wait for the process to exit and return the exit status.
    @discardableResult
    public func wait() throws -> Int32 {
        var rus = rusage()
        var ws = Int32()

        let pid = self.pid
        guard pid > 0 else {
            return -1
        }

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

    private func execute() throws -> pid_t {
        var attrs = exec_command_attrs()
        exec_command_attrs_init(&attrs)

        let set = try createFileset()
        defer {
            for nullHandle in set.nullHandles {
                try? nullHandle.close()
            }
        }
        var fds = [Int32](repeating: 0, count: set.handles.count)
        for (i, handle) in set.handles.enumerated() {
            fds[i] = handle.fileDescriptor
        }

        attrs.setsid = self.attrs.setsid ? 1 : 0
        attrs.setctty = self.attrs.setctty ? 1 : 0
        attrs.setpgid = self.attrs.setPGroup ? 1 : 0
        attrs.setfgpgrp = self.attrs.setForegroundPGroup ? 1 : 0

        var cwdPath: UnsafeMutablePointer<CChar>?
        if let chdir = self.directory {
            cwdPath = strdup(chdir)
        }
        defer {
            if let cwdPath {
                free(cwdPath)
            }
        }

        if let uid = self.attrs.uid {
            attrs.uid = uid
        }
        if let gid = self.attrs.gid {
            attrs.gid = gid
        }

        if let pdeathSignal = self.attrs.pdeathSignal {
            attrs.pdeathSignal = pdeathSignal
        }

        var pid: pid_t = 0
        var argv = ([executable] + arguments).map { strdup($0) } + [nil]
        defer {
            for arg in argv where arg != nil {
                free(arg)
            }
        }

        let env = environment.map { strdup($0) } + [nil]
        defer {
            for e in env where e != nil {
                free(e)
            }
        }

        let result = fds.withUnsafeBufferPointer { file_handles in
            exec_command(
                &pid,
                argv[0],
                &argv,
                env,
                file_handles.baseAddress!, Int32(file_handles.count),
                cwdPath ?? nil,
                &attrs)
        }
        guard result == 0 else {
            throw POSIXError(.init(rawValue: errno)!)
        }

        return pid
    }

    /// Create a posix_spawn file actions set of fds to pass to the new process
    private func createFileset() throws -> (nullHandles: [FileHandle], handles: [FileHandle]) {
        // grab dev null handles for different purposes
        let nullRead = try openDevNull(flags: O_RDONLY)
        let nullWrite = try openDevNull(flags: O_WRONLY)
        var files = [FileHandle]()
        files.append(stdin ?? nullRead)
        files.append(stdout ?? nullWrite)
        files.append(stderr ?? nullWrite)
        files.append(contentsOf: extraFiles)
        return (nullHandles: [nullRead, nullWrite], handles: files)
    }

    /// Returns a file handle to /dev/null with the specified flags.
    private func openDevNull(flags: Int32) throws -> FileHandle {
        let fd = open("/dev/null", flags, 0)
        guard fd >= 0 else {
            throw POSIXError(.init(rawValue: errno)!)
        }
        return FileHandle(fileDescriptor: fd, closeOnDealloc: false)
    }
}

extension Command {
    private static let signalOffset: Int32 = 128

    private static let shift: Int32 = 8
    private static let mask: Int32 = 0x7F
    private static let stopped: Int32 = 0x7F
    private static let exited: Int32 = 0x00

    static func signaled(_ ws: Int32) -> Bool {
        ws & mask != stopped && ws & mask != exited
    }

    static func exited(_ ws: Int32) -> Bool {
        ws & mask == exited
    }

    static func exitStatus(_ ws: Int32) -> Int32 {
        let r: Int32
        #if os(Linux)
        r = ws >> shift & 0xFF
        #else
        r = ws >> shift
        #endif
        return r
    }

    public static func toExitStatus(_ ws: Int32) -> Int32 {
        if signaled(ws) {
            // We use the offset as that is how existing container
            // runtimes minic bash for the status when signaled.
            return Int32(Self.signalOffset + ws & mask)
        }
        if exited(ws) {
            return exitStatus(ws)
        }
        return ws
    }

}

private func WIFEXITED(_ status: Int32) -> Bool {
    _WSTATUS(status) == 0
}

private func _WSTATUS(_ status: Int32) -> Int32 {
    status & 0x7f
}

private func WIFSIGNALED(_ status: Int32) -> Bool {
    (_WSTATUS(status) != 0) && (_WSTATUS(status) != 0x7f)
}

private func WEXITSTATUS(_ status: Int32) -> Int32 {
    (status >> 8) & 0xff
}

private func WTERMSIG(_ status: Int32) -> Int32 {
    status & 0x7f
}
