import Foundation

#if canImport(NIOCore) && canImport(NIOFoundationCompat) && canImport(NIOPosix)
import NIOCore
import NIOFoundationCompat
import NIOPosix

/// A client-side TCP implementation of ``FrameChannel`` for platforms without
/// Network.framework (Windows and Linux).
///
/// The stream carries the protocol's ordinary four-byte length-prefixed frames.  The one shared
/// event-loop thread is intentional: the Windows renderer DLL may own several remote-host
/// connections, but it does not need one native thread per host.  Callback serialization remains
/// the DLL layer's responsibility.
public final class NIOClientFrameChannel: FrameChannel, @unchecked Sendable {
    public let peerDescription: String
    public let transport: SyncTransportHint?

    private static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

    private let channel: any Channel
    private let stream: AsyncStream<Data>
    private let continuation: AsyncStream<Data>.Continuation

    private init(
        channel: any Channel, stream: AsyncStream<Data>,
        continuation: AsyncStream<Data>.Continuation,
        peerDescription: String, transport: SyncTransportHint?
    ) {
        self.channel = channel
        self.stream = stream
        self.continuation = continuation
        self.peerDescription = peerDescription
        self.transport = transport
    }

    /// Dial a TCP endpoint and install the protocol frame decoder before returning.
    public static func connect(
        host: String, port: Int, transport: SyncTransportHint? = .lan
    ) async throws -> NIOClientFrameChannel {
        let (stream, continuation) = AsyncStream<Data>.makeStream(bufferingPolicy: .unbounded)
        let inbound = NIOClientFrameInboundHandler(continuation: continuation)
        do {
            let channel = try await ClientBootstrap(group: eventLoopGroup)
                .connectTimeout(.seconds(10))
                .channelOption(ChannelOptions.socketOption(.tcp_nodelay), value: 1)
                .channelInitializer { channel in
                    channel.pipeline.addHandler(inbound)
                }
                .connect(host: host, port: port)
                .get()
            return NIOClientFrameChannel(
                channel: channel, stream: stream, continuation: continuation,
                peerDescription: "\(host):\(port)", transport: transport)
        } catch {
            continuation.finish()
            throw error
        }
    }

    public func frames() -> AsyncStream<Data> { stream }

    public func send(_ frame: Data) {
        var buffer = channel.allocator.buffer(capacity: frame.count + 4)
        buffer.writeBytes(WireFraming.frame(frame))
        channel.writeAndFlush(buffer, promise: nil)
    }

    public func close() {
        continuation.finish()
        channel.close(promise: nil)
    }
}

private final class NIOClientFrameInboundHandler: ChannelInboundHandler, @unchecked Sendable {
    typealias InboundIn = ByteBuffer

    private let accumulator = FrameAccumulator()
    private let continuation: AsyncStream<Data>.Continuation

    init(continuation: AsyncStream<Data>.Continuation) {
        self.continuation = continuation
    }

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        var buffer = unwrapInboundIn(data)
        let bytes = buffer.readData(length: buffer.readableBytes) ?? Data()
        do {
            for frame in try accumulator.push(bytes) {
                continuation.yield(frame)
            }
        } catch {
            continuation.finish()
            context.close(promise: nil)
        }
    }

    func channelInactive(context: ChannelHandlerContext) {
        continuation.finish()
        context.fireChannelInactive()
    }

    func errorCaught(context: ChannelHandlerContext, error: Error) {
        continuation.finish()
        context.close(promise: nil)
    }
}
#endif
