import Foundation

/// A bidirectional, ordered pipe of whole frames — the one transport seam shared by the host
/// server, the iOS client, and tests (SYNC_PROTOCOL §2 layer cake). Frames delivered by
/// `frames()` are already de-framed (length prefix stripped by the implementation); `send`
/// re-frames. The LAN implementation wraps NWConnection; tests use an in-memory pair.
public protocol FrameChannel: AnyObject, Sendable {
    /// Short, non-sensitive description for logs (e.g. "192.168.1.5:51900").
    var peerDescription: String { get }
    /// Which transport carries this channel, when known (host-accepted channels tag themselves —
    /// `LANChannel` → `.lan`, the tailnet fd channel → `.tailnet`, the relay room channel →
    /// `.relay`). `nil` for channels that don't self-identify (client dial legs, in-memory test
    /// pipes). The host reads this to notice specifically when a *LAN* connection drops, so it can
    /// nudge the departed device onto a non-local transport (SYNC_PROTOCOL §3.2).
    var transport: SyncTransportHint? { get }
    /// Ordered inbound frames; the stream finishes when the peer disconnects.
    func frames() -> AsyncStream<Data>
    /// Enqueue one frame. Synchronous so sealing + enqueuing stay atomic within one actor
    /// method, keeping the Noise per-direction nonce order identical to the wire order.
    func send(_ frame: Data)
    func close()
}

extension FrameChannel {
    /// Default: a channel doesn't know its transport. Host-accepted channels override this.
    public var transport: SyncTransportHint? { nil }
}
