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

extension OSFile {
    /// [Nucleic vendored patch] One direction of a bidirectional relay: `from` fd → transfer
    /// pipe → `to` fd. Each direction owns its OWN pipe and byte counters.
    ///
    /// The previous `SpliceFile` design threaded ONE offset pair through BOTH directions of
    /// `VsockProxy`'s relay (a fd's struct served as read-counter in one direction and
    /// write-counter in the other). That was survivable only while every splice call fully
    /// drained its pipe before returning. Once the EAGAIN-return backpressure patch let pending
    /// bytes persist across calls, one parked direction skewed the shared counters for the
    /// other: its write leg's `to.offset < from.offset` guard went false with data still in the
    /// pipe, and the outer loop then alternated read-EAGAIN/skip-write forever — a hard spin on
    /// vminitd's single ProcessSupervisor poller thread that froze every exec's stdio and every
    /// control-plane relay in the container until the VM was recreated (the persistent
    /// "produced no output within 60s" / dead-control-plane state).
    struct RelayDirection: Sendable {
        let from: Int32
        let to: Int32
        private let pipe = Pipe()
        /// Bytes spliced from `from` into the transfer pipe so far.
        fileprivate var bytesIn = 0
        /// Bytes spliced from the transfer pipe into `to` so far.
        fileprivate var bytesOut = 0
        /// The source reported EOF. The direction only FINISHES (`.eof`) once the pipe has
        /// also drained, so the stream's tail is never dropped by an early SHUT_WR.
        fileprivate var sawSourceEOF = false

        /// Bytes read from the source that the destination hasn't accepted yet.
        var pendingBytes: Int { bytesIn - bytesOut }

        fileprivate var pipeReader: Int32 { pipe.fileHandleForReading.fileDescriptor }
        fileprivate var pipeWriter: Int32 { pipe.fileHandleForWriting.fileDescriptor }

        init(from: Int32, to: Int32) {
            self.from = from
            self.to = to
        }
    }

    /// The terminal state of one `relay` pass over a direction.
    enum RelayResult: Sendable {
        /// No more progress possible right now: the source has no data (EAGAIN) or the
        /// destination is full (its EPOLLOUT edge resumes the flush of `pendingBytes`).
        case idle
        /// Source EOF observed AND the pipe fully drained — the direction is complete; the
        /// caller should SHUT_WR the destination.
        case eof
        /// The destination hung up mid-write; nothing further can be delivered.
        case brokenPipe
    }

    /// Move as much data as possible along `direction` without blocking. `count` bounds the
    /// bytes buffered in the transfer pipe (it matches the default pipe capacity).
    static func relay(_ direction: inout RelayDirection, count: Int = 1 << 16) throws -> RelayResult {
        let flags = UInt32(bitPattern: LCShim.SPLICE_F_MOVE | LCShim.SPLICE_F_NONBLOCK)
        while true {
            // Read leg: source → pipe, until the pipe is full, the source runs dry, or EOF.
            var sourceDry = false
            if !direction.sawSourceEOF {
                while direction.pendingBytes < count {
                    let toRead = count - direction.pendingBytes
                    let n = LCShim.splice(direction.from, nil, direction.pipeWriter, nil, toRead, flags)
                    if n == -1 {
                        if errno != EAGAIN && errno != EIO {
                            throw POSIXError(.init(rawValue: errno)!)
                        }
                        sourceDry = true
                        break
                    }
                    if n == 0 {
                        direction.sawSourceEOF = true
                        break
                    }
                    direction.bytesIn += n
                    if n < toRead { break }
                }
            }
            // Write leg: pipe → destination, until drained or the destination pushes back.
            while direction.pendingBytes > 0 {
                let n = LCShim.splice(direction.pipeReader, nil, direction.to, nil, direction.pendingBytes, flags)
                if n == -1 {
                    if errno != EAGAIN && errno != EIO {
                        throw POSIXError(.init(rawValue: errno)!)
                    }
                    // Destination full: park with the remainder in the pipe. The destination
                    // fd's EPOLLOUT edge re-enters and resumes exactly here — never spin, and
                    // never block the shared poller thread.
                    return .idle
                }
                if n == 0 {
                    return .brokenPipe
                }
                direction.bytesOut += n
            }
            // Pipe is drained here.
            if direction.sawSourceEOF { return .eof }
            if sourceDry { return .idle }
            // The read leg stopped only because the pipe filled (or read a full window):
            // go around again — the source may still have data.
        }
    }
}

#endif
