import Foundation
import NucleicProtocol

#if canImport(Network)
import Network

/// The LAN transport (SYNC_PROTOCOL §3.1): advertises `_nucleic._tcp` via Bonjour and
/// accepts TCP connections, wrapping each in a `LANChannel`. TLS is *not* relied on for
/// secrecy — Noise provides E2EE + mutual auth above this; the TCP layer is just bytes.
public final class LANListener: SyncListener, @unchecked Sendable {
    public static let serviceType = "_nucleic._tcp"

    private let preferredPort: NWEndpoint.Port
    private let fingerprint: String
    private let queue = DispatchQueue(label: "nucleic.sync.listener")
    private var listener: NWListener?
    private var continuation: AsyncStream<any FrameChannel>.Continuation?

    /// - Parameters:
    ///   - preferredPort: the port to try first — the one this Mac bound last launch, persisted so
    ///     the LAN hint a sibling stored stays valid across a quit/relaunch/update (an OS-assigned
    ///     port changes every launch, which is what stranded reconnecting Macs). `nil` ⇒ let the OS
    ///     pick. A taken preferred port silently falls back to an OS-assigned one, so this is a
    ///     best-effort reuse, never a hard requirement.
    ///   - identityFingerprint: the host static-key fingerprint, advertised in the TXT record
    ///     so a browsing client can sanity-check before connecting (structural metadata only).
    public init(preferredPort: UInt16? = nil, identityFingerprint: String) {
        self.preferredPort = preferredPort.flatMap { NWEndpoint.Port(rawValue: $0) } ?? .any
        self.fingerprint = identityFingerprint
    }

    public func start() throws -> AsyncStream<any FrameChannel> {
        let listener = try Self.bind(preferredPort)
        self.listener = listener

        let txt = NWTXTRecord(["fp": fingerprint, "v": String(SyncProtocol.version)])
        listener.service = NWListener.Service(type: Self.serviceType, txtRecord: txt)

        let stream = AsyncStream<any FrameChannel> { continuation in
            self.continuation = continuation
        }
        listener.newConnectionHandler = { [weak self] connection in
            guard let self else { return }
            let channel = LANChannel(connection: connection, queue: self.queue)
            self.continuation?.yield(channel)
        }
        listener.stateUpdateHandler = { [weak self] state in
            if case .failed = state { self?.continuation?.finish() }
        }
        listener.start(queue: queue)
        return stream
    }

    public func stop() {
        listener?.cancel()
        listener = nil
        continuation?.finish()
    }

    /// The live listener port once ready — the persisted `preferredPort` when it was free, else the
    /// OS-assigned fallback. Feeds the pairing QR's LAN hint *and* is persisted so the next launch
    /// can reuse it (keeping a sibling's stored `lanHint` dialable across relaunches).
    public var assignedPort: UInt16? { listener?.port?.rawValue }

    /// Bind the preferred port; on failure (a fixed port a lingering socket or another process
    /// still holds) retry with an OS-assigned port. Only a `.any` failure is a true "can't listen".
    private static func bind(_ preferred: NWEndpoint.Port) throws -> NWListener {
        func make(_ port: NWEndpoint.Port) throws -> NWListener {
            let params = NWParameters.tcp
            params.includePeerToPeer = true
            return try NWListener(using: params, on: port)
        }
        do {
            return try make(preferred)
        } catch {
            guard preferred != .any else { throw SyncTransportError.listenerFailed("\(error)") }
            do { return try make(.any) }
            catch { throw SyncTransportError.listenerFailed("\(error)") }
        }
    }
}

/// Wraps one `NWConnection` as a `FrameChannel`: de-frames inbound bytes with a
/// `FrameAccumulator`, length-prefixes outbound frames. Starts receiving immediately so no
/// inbound bytes are missed before `frames()` is consumed.
public final class LANChannel: FrameChannel, @unchecked Sendable {
    public let peerDescription: String
    private let connection: NWConnection
    private let queue: DispatchQueue
    private let accumulator = FrameAccumulator()
    private let stream: AsyncStream<Data>
    private let continuation: AsyncStream<Data>.Continuation

    init(connection: NWConnection, queue: DispatchQueue) {
        self.connection = connection
        self.queue = queue
        switch connection.endpoint {
        case .hostPort(let host, let port): peerDescription = "\(host):\(port)"
        default: peerDescription = "\(connection.endpoint)"
        }
        var cont: AsyncStream<Data>.Continuation!
        stream = AsyncStream(bufferingPolicy: .unbounded) { cont = $0 }
        continuation = cont

        connection.stateUpdateHandler = { [weak self] state in
            switch state {
            case .failed, .cancelled: self?.continuation.finish()
            default: break
            }
        }
        connection.start(queue: queue)
        receiveLoop()
    }

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

    public func send(_ frame: Data) {
        connection.send(content: WireFraming.frame(frame), completion: .idempotent)
    }

    public func close() {
        connection.cancel()
        continuation.finish()
    }

    private func receiveLoop() {
        connection.receive(minimumIncompleteLength: 1, maximumLength: 65_536) { [weak self] data, _, isComplete, error in
            guard let self else { return }
            if let data, !data.isEmpty {
                if let frames = try? self.accumulator.push(data) {
                    for frame in frames { self.continuation.yield(frame) }
                } else {
                    // Oversize/garbage frame → drop the connection.
                    self.close(); return
                }
            }
            if isComplete || error != nil {
                self.continuation.finish()
                return
            }
            self.receiveLoop()
        }
    }
}
#endif
