//===----------------------------------------------------------------------===//
// 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 ContainerizationOCI
import ContainerizationOS
import Foundation

/// Log format for runc output
enum LogFormat: String, Sendable {
    case json
    case text
}

/// Configuration and client for interacting with the runc binary
struct Runc: Sendable {
    /// IO configuration for runc operations
    struct IO: Sendable {
        var stdin: FileHandle?
        var stdout: FileHandle?
        var stderr: FileHandle?

        init(
            stdin: FileHandle? = nil,
            stdout: FileHandle? = nil,
            stderr: FileHandle? = nil
        ) {
            self.stdin = stdin
            self.stdout = stdout
            self.stderr = stderr
        }

        static let inherit = IO()
    }

    /// Path to the runc binary
    var command: String

    /// Root directory for container state
    var root: String?

    /// Enable debug output
    var debug: Bool

    /// Path to log file
    var log: String?

    /// Format for log output
    var logFormat: LogFormat?

    /// Signal to send when parent process dies
    var pdeathSignal: Int32?

    /// Set process group ID
    var setpgid: Bool

    /// Path to criu binary for checkpoint/restore
    var criu: String?

    /// Use systemd cgroup manager
    var systemdCgroup: Bool

    /// Enable rootless mode
    var rootless: Bool

    /// Additional arguments to pass to runc
    var extraArgs: [String]

    /// Command runner to use instead of direct wait4 (for PID 1 environments with reapers)
    var commandRunner: (any CommandRunner)?

    init(
        command: String = "runc",
        root: String? = nil,
        debug: Bool = false,
        log: String? = nil,
        logFormat: LogFormat? = nil,
        pdeathSignal: Int32? = nil,
        setpgid: Bool = false,
        criu: String? = nil,
        systemdCgroup: Bool = false,
        rootless: Bool = false,
        extraArgs: [String] = [],
        commandRunner: (any CommandRunner)? = nil
    ) {
        self.command = command
        self.root = root
        self.debug = debug
        self.log = log
        self.logFormat = logFormat
        self.pdeathSignal = pdeathSignal
        self.setpgid = setpgid
        self.criu = criu
        self.systemdCgroup = systemdCgroup
        self.rootless = rootless
        self.extraArgs = extraArgs
        self.commandRunner = commandRunner
    }
}

/// Options for creating a container
struct CreateOpts: Sendable {
    /// Path to file to write container PID
    var pidFile: String?

    /// Path to console socket for terminal access
    var consoleSocket: String?

    /// Detach from the container process
    var detach: Bool

    /// Do not use pivot_root to change root
    var noPivot: Bool

    /// Do not create a new session
    var noNewKeyring: Bool

    /// Additional file descriptors to pass to the container
    var extraFiles: [FileHandle]

    /// IO configuration for the runc process
    var io: Runc.IO

    init(
        pidFile: String? = nil,
        consoleSocket: String? = nil,
        detach: Bool = false,
        noPivot: Bool = false,
        noNewKeyring: Bool = false,
        extraFiles: [FileHandle] = [],
        io: Runc.IO = .inherit
    ) {
        self.pidFile = pidFile
        self.consoleSocket = consoleSocket
        self.detach = detach
        self.noPivot = noPivot
        self.noNewKeyring = noNewKeyring
        self.extraFiles = extraFiles
        self.io = io
    }
}

/// Options for executing a process in a container
struct ExecOpts: Sendable {
    /// Path to file to write process PID
    var pidFile: String?

    /// Path to console socket for terminal access
    var consoleSocket: String?

    /// Detach from the process
    var detach: Bool

    /// Path to process.json file
    var processPath: String?

    /// IO configuration for the runc process
    var io: Runc.IO

    init(
        pidFile: String? = nil,
        consoleSocket: String? = nil,
        detach: Bool = false,
        processPath: String? = nil,
        io: Runc.IO = .inherit
    ) {
        self.pidFile = pidFile
        self.consoleSocket = consoleSocket
        self.detach = detach
        self.processPath = processPath
        self.io = io
    }
}

/// Options for deleting a container
struct DeleteOpts: Sendable {
    /// Force deletion of a running container
    var force: Bool

    init(force: Bool = false) {
        self.force = force
    }
}

/// Options for restoring a container from checkpoint
struct RestoreOpts: Sendable {
    /// Path to file to write container PID
    var pidFile: String?

    /// Path to console socket for terminal access
    var consoleSocket: String?

    /// Detach from the container process
    var detach: Bool

    /// Do not use pivot_root to change root
    var noPivot: Bool

    /// Do not create a new session
    var noNewKeyring: Bool

    /// Path to checkpoint image
    var imagePath: String?

    /// Path to parent checkpoint
    var parentPath: String?

    /// Work directory for CRIU
    var workPath: String?

    init(
        pidFile: String? = nil,
        consoleSocket: String? = nil,
        detach: Bool = false,
        noPivot: Bool = false,
        noNewKeyring: Bool = false,
        imagePath: String? = nil,
        parentPath: String? = nil,
        workPath: String? = nil
    ) {
        self.pidFile = pidFile
        self.consoleSocket = consoleSocket
        self.detach = detach
        self.noPivot = noPivot
        self.noNewKeyring = noNewKeyring
        self.imagePath = imagePath
        self.parentPath = parentPath
        self.workPath = workPath
    }
}

/// Container information returned from list operation
struct Container: Sendable, Codable {
    let id: String
    let pid: Int
    let status: String
    let bundle: String
    let rootfs: String
    let created: Date
    let annotations: [String: String]?

    enum CodingKeys: String, CodingKey {
        case id
        case pid
        case status
        case bundle
        case rootfs
        case created
        case annotations
    }
}

extension Runc {
    enum Error: Swift.Error, CustomStringConvertible {
        case invalidJSON(String)
        case commandFailed(Int32, String)
        case invalidPidFile(String)

        var description: String {
            switch self {
            case .invalidJSON(let detail):
                return "invalid JSON: \(detail)"
            case .commandFailed(let status, let output):
                return "command failed with status \(status): \(output)"
            case .invalidPidFile(let path):
                return "invalid or missing PID file: \(path)"
            }
        }
    }
}

// MARK: - Command Building and Execution

extension Runc {
    /// Build base arguments for runc command
    func baseArgs() -> [String] {
        var args: [String] = []

        if let root = root {
            args += ["--root", root]
        }

        if debug {
            args.append("--debug")
        }

        if let log = log {
            args += ["--log", log]
        }

        if let logFormat = logFormat {
            args += ["--log-format", logFormat.rawValue]
        }

        if systemdCgroup {
            args.append("--systemd-cgroup")
        }

        if rootless {
            args.append("--rootless")
        }

        args += extraArgs

        return args
    }

    /// Execute a runc command and return the output
    func execute(
        args: [String],
        stdin: FileHandle? = nil,
        stdout: FileHandle? = nil,
        stderr: FileHandle? = nil,
        extraFiles: [FileHandle] = [],
        directory: String? = nil
    ) async throws -> (status: Int32, output: Data) {
        var cmd = Command(
            command,
            arguments: args,
            directory: directory,
            extraFiles: extraFiles
        )

        // Setup IO
        let outPipe = Pipe()
        cmd.stdin = stdin
        cmd.stdout = stdout ?? outPipe.fileHandleForWriting
        cmd.stderr = stderr ?? outPipe.fileHandleForWriting

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

        if setpgid {
            cmd.attrs.setPGroup = true
        }

        let exitStatus: Int32

        if let runner = commandRunner {
            let subscription = try runner.start(&cmd)
            exitStatus = try await runner.wait(cmd, subscription: subscription)
        } else {
            try cmd.start()
            exitStatus = try cmd.wait()
        }

        var output = Data()
        if stdout == nil {
            try? outPipe.fileHandleForWriting.close()
            output = try outPipe.fileHandleForReading.readToEnd() ?? Data()
        }

        return (exitStatus, output)
    }

    /// Execute command and parse JSON output
    func executeJSON<T: Decodable>(
        args: [String],
        directory: String? = nil
    ) async throws -> T {
        let (status, output) = try await execute(args: args, directory: directory)

        guard status == 0 else {
            let errorOutput = String(data: output, encoding: .utf8) ?? ""
            throw Error.commandFailed(status, errorOutput)
        }

        do {
            return try JSONDecoder().decode(T.self, from: output)
        } catch {
            let outputStr = String(data: output, encoding: .utf8) ?? ""
            throw Error.invalidJSON("failed to decode: \(error), output: \(outputStr)")
        }
    }

    /// Execute command without capturing output
    func executeVoid(
        args: [String],
        stdin: FileHandle? = nil,
        stdout: FileHandle? = nil,
        stderr: FileHandle? = nil,
        extraFiles: [FileHandle] = [],
        directory: String? = nil
    ) async throws {
        let (status, output) = try await execute(
            args: args,
            stdin: stdin,
            stdout: stdout,
            stderr: stderr,
            extraFiles: extraFiles,
            directory: directory
        )

        guard status == 0 else {
            let errorOutput = String(data: output, encoding: .utf8) ?? ""
            throw Error.commandFailed(status, errorOutput)
        }
    }

    /// Read PID from a file
    func readPidFile(_ path: String) throws -> Int {
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
            let pidString = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
            let pid = Int(pidString)
        else {
            throw Error.invalidPidFile(path)
        }
        return pid
    }
}

extension Runc {
    /// Create a container
    func create(
        id: String,
        bundle: String,
        opts: CreateOpts = CreateOpts()
    ) async throws -> Int? {
        var args = baseArgs() + ["create"]

        if let pidFile = opts.pidFile {
            args += ["--pid-file", pidFile]
        }

        if let consoleSocket = opts.consoleSocket {
            args += ["--console-socket", consoleSocket]
        }

        if opts.detach {
            args.append("--detach")
        }

        if opts.noPivot {
            args.append("--no-pivot")
        }

        if opts.noNewKeyring {
            args.append("--no-new-keyring")
        }

        args += ["--bundle", bundle, id]

        try await executeVoid(
            args: args,
            stdin: opts.io.stdin,
            stdout: opts.io.stdout,
            stderr: opts.io.stderr,
            extraFiles: opts.extraFiles,
            directory: bundle
        )

        // Read PID if pidFile was specified
        if let pidFile = opts.pidFile {
            return try readPidFile(pidFile)
        }

        return nil
    }

    /// Start a container
    func start(id: String) async throws {
        let args = baseArgs() + ["start", id]
        try await executeVoid(args: args)
    }

    /// Run a container (create + start)
    func run(
        id: String,
        bundle: String,
        opts: CreateOpts = CreateOpts()
    ) async throws -> Int? {
        var args = baseArgs() + ["run"]

        if let pidFile = opts.pidFile {
            args += ["--pid-file", pidFile]
        }

        if let consoleSocket = opts.consoleSocket {
            args += ["--console-socket", consoleSocket]
        }

        if opts.detach {
            args.append("--detach")
        }

        if opts.noPivot {
            args.append("--no-pivot")
        }

        if opts.noNewKeyring {
            args.append("--no-new-keyring")
        }

        args += ["--bundle", bundle, id]

        try await executeVoid(
            args: args,
            stdin: opts.io.stdin,
            stdout: opts.io.stdout,
            stderr: opts.io.stderr,
            extraFiles: opts.extraFiles,
            directory: bundle
        )

        // Read PID if pidFile was specified
        if let pidFile = opts.pidFile {
            return try readPidFile(pidFile)
        }

        return nil
    }

    /// Delete a container
    func delete(id: String, opts: DeleteOpts = DeleteOpts()) async throws {
        var args = baseArgs() + ["delete"]

        if opts.force {
            args.append("--force")
        }

        args.append(id)

        try await executeVoid(args: args)
    }

    /// Send a signal to a container
    func kill(id: String, signal: Int32, all: Bool = false) async throws {
        var args = baseArgs() + ["kill"]

        if all {
            args.append("--all")
        }

        args += [id, String(signal)]

        try await executeVoid(args: args)
    }

    /// Pause a container
    func pause(id: String) async throws {
        let args = baseArgs() + ["pause", id]
        try await executeVoid(args: args)
    }

    /// Resume a paused container
    func resume(id: String) async throws {
        let args = baseArgs() + ["resume", id]
        try await executeVoid(args: args)
    }

    /// Execute a process in a running container
    func exec(
        id: String,
        processSpec: String,
        opts: ExecOpts = ExecOpts()
    ) async throws -> Int? {
        var args = baseArgs() + ["exec"]

        if let pidFile = opts.pidFile {
            args += ["--pid-file", pidFile]
        }

        if let consoleSocket = opts.consoleSocket {
            args += ["--console-socket", consoleSocket]
        }

        if opts.detach {
            args.append("--detach")
        }

        if let processPath = opts.processPath {
            args += ["--process", processPath]
        }

        args += [id, processSpec]

        try await executeVoid(
            args: args,
            stdin: opts.io.stdin,
            stdout: opts.io.stdout,
            stderr: opts.io.stderr
        )

        // Read PID if pidFile was specified
        if let pidFile = opts.pidFile {
            return try readPidFile(pidFile)
        }

        return nil
    }

    /// Update container resources
    func update(id: String, resources: String) async throws {
        let args = baseArgs() + ["update", "--resources", resources, id]
        try await executeVoid(args: args)
    }

    /// Checkpoint a container
    func checkpoint(
        id: String,
        imagePath: String,
        leaveRunning: Bool = false,
        workPath: String? = nil
    ) async throws {
        var args = baseArgs() + ["checkpoint"]

        if leaveRunning {
            args.append("--leave-running")
        }

        if let workPath = workPath {
            args += ["--work-path", workPath]
        }

        args += ["--image-path", imagePath, id]

        try await executeVoid(args: args)
    }

    /// Restore a container from checkpoint
    func restore(
        id: String,
        bundle: String,
        opts: RestoreOpts = RestoreOpts()
    ) async throws -> Int? {
        var args = baseArgs() + ["restore"]

        if let pidFile = opts.pidFile {
            args += ["--pid-file", pidFile]
        }

        if let consoleSocket = opts.consoleSocket {
            args += ["--console-socket", consoleSocket]
        }

        if opts.detach {
            args.append("--detach")
        }

        if opts.noPivot {
            args.append("--no-pivot")
        }

        if opts.noNewKeyring {
            args.append("--no-new-keyring")
        }

        if let imagePath = opts.imagePath {
            args += ["--image-path", imagePath]
        }

        if let parentPath = opts.parentPath {
            args += ["--parent-path", parentPath]
        }

        if let workPath = opts.workPath {
            args += ["--work-path", workPath]
        }

        args += ["--bundle", bundle, id]

        try await executeVoid(args: args, directory: bundle)

        if let pidFile = opts.pidFile {
            return try readPidFile(pidFile)
        }

        return nil
    }
}

// MARK: - List and State Operations

extension Runc {
    /// List all containers
    func list() async throws -> [Container] {
        let args = baseArgs() + ["list", "--format", "json"]
        let containers: [Container] = try await executeJSON(args: args)
        return containers
    }

    /// Get state of a specific container
    func state(id: String) async throws -> ContainerizationOCI.State {
        let args = baseArgs() + ["state", id]
        let state: ContainerizationOCI.State = try await executeJSON(args: args)
        return state
    }

    /// List process IDs in a container
    func ps(id: String) async throws -> [Int] {
        let args = baseArgs() + ["ps", "--format", "json", id]
        let (status, output) = try await execute(args: args)

        guard status == 0 else {
            let errorOutput = String(data: output, encoding: .utf8) ?? ""
            throw Error.commandFailed(status, errorOutput)
        }

        // ps output is just an array of PIDs
        let pids = try JSONDecoder().decode([Int].self, from: output)
        return pids
    }

    /// Get version information
    func version() async throws -> String {
        let args = [command, "--version"]
        let (status, output) = try await execute(args: args)

        guard status == 0 else {
            let errorOutput = String(data: output, encoding: .utf8) ?? ""
            throw Error.commandFailed(status, errorOutput)
        }

        return String(data: output, encoding: .utf8) ?? ""
    }
}

// MARK: - Events

extension Runc {
    /// Event from container runtime
    struct Event: Codable, Sendable {
        let type: String
        let id: String
        let stats: EventStats?

        enum CodingKeys: String, CodingKey {
            case type
            case id
            case stats
        }
    }

    /// Statistics in an event
    struct EventStats: Codable, Sendable {
        let cpu: CPUStats?
        let memory: MemoryStats?
        let pids: PIDStats?

        enum CodingKeys: String, CodingKey {
            case cpu
            case memory
            case pids
        }
    }

    struct CPUStats: Codable, Sendable {
        let usage: CPUUsage?
        let throttling: ThrottlingData?

        struct CPUUsage: Codable, Sendable {
            let total: UInt64?
            let percpu: [UInt64]?
        }

        struct ThrottlingData: Codable, Sendable {
            let periods: UInt64?
            let throttledPeriods: UInt64?
            let throttledTime: UInt64?
        }
    }

    struct MemoryStats: Codable, Sendable {
        let usage: MemoryUsage?
        let limit: UInt64?

        struct MemoryUsage: Codable, Sendable {
            let usage: UInt64?
            let max: UInt64?
        }
    }

    struct PIDStats: Codable, Sendable {
        let current: UInt64?
        let limit: UInt64?
    }
}

#endif
