import Foundation

/// Length-prefixed framing for the byte stream (SYNC_PROTOCOL §4.4: "length-prefixed AEAD
/// ciphertext"). TCP/WS guarantee in-order bytes but not message boundaries, so each frame
/// is a 4-byte big-endian length followed by that many payload bytes. The payload is an
/// encrypted `Frame` once `SecureChannel` is in play; pre-handshake bytes use the same shape.
public enum WireFraming {
    /// Hard cap so a malicious/corrupt length can't make us buffer unboundedly. Transcript
    /// event batches are small; 16 MiB is far above any legitimate frame.
    public static let maxFrameSize = 16 * 1024 * 1024

    public static func frame(_ payload: Data) -> Data {
        var out = Data(capacity: payload.count + 4)
        var len = UInt32(payload.count).bigEndian
        withUnsafeBytes(of: &len) { out.append(contentsOf: $0) }
        out.append(payload)
        return out
    }
}

/// Reassembles whole frames from arbitrarily-chunked stream reads. Not thread-safe by
/// itself — drive it from the transport's serial receive path.
public final class FrameAccumulator {
    private var buffer = Data()
    private let maxFrameSize: Int

    public init(maxFrameSize: Int = WireFraming.maxFrameSize) {
        self.maxFrameSize = maxFrameSize
    }

    public enum FramingError: Error, Equatable { case frameTooLarge(Int) }

    /// Append freshly-received bytes and return every complete frame now available.
    public func push(_ data: Data) throws -> [Data] {
        buffer.append(data)
        var frames: [Data] = []
        while true {
            guard buffer.count >= 4 else { break }
            let len = buffer.prefix(4).reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
            let length = Int(len)
            if length > maxFrameSize { throw FramingError.frameTooLarge(length) }
            guard buffer.count >= 4 + length else { break }
            let start = buffer.index(buffer.startIndex, offsetBy: 4)
            let end = buffer.index(start, offsetBy: length)
            frames.append(Data(buffer[start..<end]))
            buffer.removeSubrange(buffer.startIndex..<end)
        }
        return frames
    }
}
