import Foundation

#if canImport(Network)
import Network

/// Watches this Mac's network path (Wi-Fi ⇄ wired ⇄ cellular/hotspot ⇄ offline) and fires `onChange`
/// the instant a *relevant* aspect of it changes, so the mesh dialer can re-plan its transports
/// immediately instead of waiting out a dead LAN socket. This is the Mac counterpart to the iOS
/// `NetworkPathMonitor`: `PeerClient.manage` only redials when a live channel's stream ends, but a
/// LAN `NWConnection` orphaned by a path change goes `.waiting` (not `.failed`) and never ends its
/// stream — so nothing rebuilds the candidate list to fall through to the tailnet. This is that
/// proactive signal.
///
/// Deliberately callback-only (no published transport preference): the mesh already tries LAN before
/// tailnet per peer, so the monitor's whole job is to say "the link moved — reconsider now."
public final class MeshPathMonitor: @unchecked Sendable {
    /// Fired off the monitor's queue whenever the path's reachability or interface set changes. The
    /// callback hops to wherever it needs to run (the app bounces it onto the main actor).
    private let onChange: @Sendable () -> Void

    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "nucleic.sync.path")
    private let lock = NSLock()
    private var started = false
    /// The last path we reported, as a cheap signature, so we only fire on a real change — the OS
    /// redelivers the current path on start and can repeat updates that don't change what we care
    /// about (dropping a needless mesh-wide reconnect on every spurious callback).
    private var lastSignature: String?

    public init(onChange: @escaping @Sendable () -> Void) {
        self.onChange = onChange
    }

    /// Begin monitoring (idempotent — a second call while already running is a no-op).
    public func start() {
        lock.lock()
        guard !started else { lock.unlock(); return }
        started = true
        lock.unlock()
        monitor.pathUpdateHandler = { [weak self] path in self?.apply(path) }
        monitor.start(queue: queue)
    }

    public func stop() {
        lock.lock()
        started = false
        lastSignature = nil
        lock.unlock()
        monitor.cancel()
    }

    private func apply(_ path: NWPath) {
        // Signature = overall reachability + which interface types are in play. A change to either
        // means routes may have moved (e.g. Wi-Fi → cellular), which is exactly when a LAN socket
        // silently dies. Interface *identity* churn we don't need — the type set is enough.
        let interfaces = [NWInterface.InterfaceType.wifi, .wiredEthernet, .cellular, .other, .loopback]
            .filter { path.usesInterfaceType($0) }
            .map { "\($0)" }
            .joined(separator: ",")
        let signature = "\(path.status)|\(interfaces)|\(path.isExpensive)"
        let changed = lock.withLock {
            guard signature != lastSignature else { return false }
            lastSignature = signature
            return true
        }
        // The first callback (nil → signature) counts as a change, but there's no live connection to
        // disturb yet at startup, so re-planning then is harmless.
        if changed { onChange() }
    }
}
#endif
