import Foundation
import NucleicProtocol
#if canImport(TailscaleKit)
import TailscaleKit
#endif

/// The host-side tailnet listener: binds `:port` on the embedded node and yields each
/// accepted connection as an `FDFrameChannel`. Mirrors `LANListener`'s shape so NucleicCore
/// can adopt it as a `SyncListener` (the conformance lives there — the protocol is
/// host-only). Created via `TailnetNode.makeListener`, never directly.
///
/// libtailscale accept mechanics: the listener fd is one half of a socketpair; Go accepts
/// tailnet connections proactively and passes each conn fd across with SCM_RIGHTS, so
/// `POLLIN` on the listener fd means `tailscale_accept` will return promptly. We poll with
/// a short tick rather than blocking so `stop()` can wind the loop down without racing the
/// fd's reuse (closing an fd out from under poll(2) is undefined on Darwin).
public final class TailnetListener: @unchecked Sendable {
    public let port: UInt16
    /// The node's tailnet addresses at creation time (advertised in the pairing QR).
    public let ip4: String?
    public let ip6: String?

    private let handle: Int32
    private let state = NSLock()
    private var stopped = false
    private var listenerFD: Int32 = -1
    private var continuation: AsyncStream<any FrameChannel>.Continuation?

    init(handle: Int32, port: UInt16, ip4: String?, ip6: String?) {
        self.handle = handle
        self.port = port
        self.ip4 = ip4
        self.ip6 = ip6
    }

    public func start() throws -> AsyncStream<any FrameChannel> {
        #if canImport(TailscaleKit)
        var boundFD: Int32 = -1
        let rc = tailscale_listen(handle, "tcp", ":\(port)", &boundFD)
        guard rc == 0 else {
            throw TailnetError.listenFailed(lastTailscaleError(handle: handle))
        }
        let lfd = boundFD
        state.lock()
        listenerFD = lfd
        state.unlock()
        let stream = AsyncStream<any FrameChannel> { continuation in
            self.continuation = continuation
        }
        Thread.detachNewThread { [self] in acceptLoop(lfd) }
        return stream
        #else
        throw TailnetError.notBuiltIn
        #endif
    }

    public func stop() {
        state.lock()
        stopped = true
        state.unlock()
        // Wake consumers immediately; the accept loop notices within one poll tick and
        // closes the listener fd itself.
        continuation?.finish()
    }

    private var isStopped: Bool {
        state.lock()
        defer { state.unlock() }
        return stopped
    }

    #if canImport(TailscaleKit)
    private func acceptLoop(_ lfd: Int32) {
        while !isStopped {
            var probe = pollfd(fd: lfd, events: Int16(POLLIN), revents: 0)
            let n = poll(&probe, 1, 1000)
            if isStopped { break }
            if n < 0 {
                if errno == EINTR { continue }
                break
            }
            if n == 0 { continue }  // tick — re-check the stop flag
            guard probe.revents & Int16(POLLIN) != 0 else { break }  // HUP/ERR/NVAL

            var cfd: Int32 = -1
            guard tailscale_accept(lfd, &cfd) == 0 else { continue }
            let channel = FDFrameChannel(fd: cfd, peerDescription: peerDescription(lfd: lfd, cfd: cfd))
            // stop() may have finished the stream between the poll and here — a dropped
            // yield must still release the conn fd, or it lingers until the peer times out.
            if case .enqueued = continuation?.yield(channel) {} else { channel.close() }
        }
        Darwin.close(lfd)
        continuation?.finish()
    }

    /// Remote tailnet IP for logs. Best-effort: upstream notes the fd bookkeeping behind
    /// `tailscale_getremoteaddr` can miss (EBADF), so fall back to a generic label.
    private func peerDescription(lfd: Int32, cfd: Int32) -> String {
        var buf = [CChar](repeating: 0, count: 128)
        guard tailscale_getremoteaddr(lfd, cfd, &buf, buf.count) == 0 else { return "tailnet peer" }
        let ip = nulTerminatedString(buf)
        return ip.isEmpty ? "tailnet peer" : "\(ip) (tailnet)"
    }
    #endif
}
