import Foundation
import Network
import NucleicProtocol

/// Client-side `FrameChannel` over an outbound `NWConnection` (SYNC_PROTOCOL §3.1). De-frames
/// inbound bytes with `FrameAccumulator`; length-prefixes outbound frames. Mirrors the host's
/// `LANChannel` but for the phone's side of the TCP connection.
final class NWFrameChannel: FrameChannel, @unchecked Sendable {
    let peerDescription: String
    private let connection: NWConnection
    private let queue = DispatchQueue(label: "nucleic.remote.channel")
    private let accumulator = FrameAccumulator()
    private let stream: AsyncStream<Data>
    private let continuation: AsyncStream<Data>.Continuation

    /// State callbacks so the store can reflect connectivity truth (UX_IOS §6).
    var onReady: (@Sendable () -> Void)?
    var onFailed: (@Sendable (String) -> Void)?

    /// Whether TCP reached `.ready` — latched, thread-safe. The connect-timeout guard
    /// polls this instead of relying on a callback that could race connection start.
    private let stateLock = NSLock()
    private var ready = false
    var isReady: Bool {
        stateLock.lock()
        defer { stateLock.unlock() }
        return ready
    }

    init(endpoint: NWEndpoint) {
        // Short TCP keepalive so a silently-dead LAN link (Wi-Fi dropped, Mac left the subnet) is
        // detected in ~15s instead of the OS default of minutes — the belt to the path-monitor's
        // braces for the case where a path still exists but the route to the Mac is gone.
        let tcp = NWProtocolTCP.Options()
        tcp.enableKeepalive = true
        tcp.keepaliveIdle = 5
        tcp.keepaliveInterval = 5
        tcp.keepaliveCount = 2
        let params = NWParameters(tls: nil, tcp: tcp)
        params.includePeerToPeer = true
        connection = NWConnection(to: endpoint, using: params)
        peerDescription = "\(endpoint)"
        var cont: AsyncStream<Data>.Continuation!
        stream = AsyncStream(bufferingPolicy: .unbounded) { cont = $0 }
        continuation = cont

        connection.stateUpdateHandler = { [weak self] state in
            switch state {
            case .ready: self?.markReady(); self?.onReady?()
            case .waiting(let error):
                // No usable path right now (Wi-Fi dropped, host unreachable). NWConnection would sit
                // in `.waiting` indefinitely, holding the transport hostage — the core of the "still
                // on LAN after leaving Wi-Fi" stall. Fail fast so the store falls straight through to
                // tailnet/relay. (Healthy connections go setup→preparing→ready and never hit this;
                // the connect-timeout guard covers a wrong-network socket that stays `.preparing`.)
                self?.onFailed?("waiting: \(error)"); self?.continuation.finish()
            case .failed(let error): self?.onFailed?("\(error)"); self?.continuation.finish()
            case .cancelled: self?.continuation.finish()
            default: break
            }
        }
        connection.start(queue: queue)
        receiveLoop()
    }

    private func markReady() {
        stateLock.lock()
        ready = true
        stateLock.unlock()
    }

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

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

    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 {
                    self.close(); return
                }
            }
            if isComplete || error != nil { self.continuation.finish(); return }
            self.receiveLoop()
        }
    }
}
