import Foundation

// The Tailnet transport (SYNC_PROTOCOL §3): both devices run an embedded tsnet node
// (TailscaleKit) on the user's own tailnet, and sync frames flow over an ordinary TCP
// stream between the two tailnet IPs. Noise E2EE runs unchanged above it — like the LAN
// and (future) relay transports, this layer only moves bytes.
//
// TailscaleKit is a locally built binary artifact (scripts/build-tailscalekit.sh), so this
// module must compile without it: every TailscaleKit reference sits behind
// `#if canImport(TailscaleKit)` and the public surface stays identical either way, with
// `TailnetSupport.isBuiltIn` telling the UI whether the transport can actually run.
//
// WINDOWS (docs/WINDOWS_PORT.md §14.3): that same absent-artifact path IS the Windows
// posture — TailscaleKit is an Apple xcframework, so `canImport` is false, `isBuiltIn`
// reports false, and every entry point throws `.notBuiltIn`. No Windows-specific stub is
// needed or wanted: the guards already produce exactly that. The only code here that had to
// change for Windows was `FDFrameChannel`, which sits OUTSIDE the TailscaleKit guards
// because it works over any connected fd. Windows tailnet integration will not be tsnet
// embedded in-process, so when it lands it wants its own backend behind this same
// `isBuiltIn`/`notBuiltIn` surface — not a port of the libtailscale fd plumbing below.

/// Whether this binary was built with the TailscaleKit framework (see Package.swift's
/// `tailscaleKitAvailable`). When false, every Tailnet entry point throws `.notBuiltIn`.
public enum TailnetSupport {
    public static var isBuiltIn: Bool {
        #if canImport(TailscaleKit)
        true
        #else
        false
        #endif
    }
}

public enum TailnetError: Error, Equatable, LocalizedError {
    /// Built without the TailscaleKit artifact (scripts/build-tailscalekit.sh not run).
    case notBuiltIn
    /// Missing prerequisite — typically no auth key on a first login.
    case notConfigured(String)
    case nodeFailed(String)
    case listenFailed(String)
    case dialFailed(String)
    case timedOut(String)

    public var errorDescription: String? {
        switch self {
        case .notBuiltIn:
            "Tailscale support isn't built into this copy of Nucleic (run scripts/build-tailscalekit.sh and rebuild)."
        case .notConfigured(let m): m
        case .nodeFailed(let m): "Tailscale node failed: \(m)"
        case .listenFailed(let m): "Tailnet listener failed: \(m)"
        case .dialFailed(let m): "Tailnet connection failed: \(m)"
        case .timedOut(let m): m
        }
    }
}

/// How to run this device's embedded tsnet node. The state directory is the node's whole
/// identity: reusing it across launches keeps the machine registered on the tailnet, so the
/// auth key is only needed the first time (or after the node is removed from the tailnet).
public struct TailnetConfig: Sendable, Equatable {
    /// Node name shown in the tailnet admin console (e.g. "nucleic-mymac").
    public var hostName: String
    /// Writable directory holding tsnet state (node keys, netmap cache). Unique per node.
    public var stateDirectory: URL
    /// A `tskey-auth-…` key from the tailnet admin console; nil once the state directory
    /// already holds a registered identity.
    public var authKey: String?
    /// Coordination server override (Headscale etc.); nil = the Tailscale SaaS default.
    public var controlURL: String?
    /// Ephemeral nodes are auto-removed by the control plane after going offline.
    public var ephemeral: Bool

    public init(
        hostName: String, stateDirectory: URL, authKey: String? = nil,
        controlURL: String? = nil, ephemeral: Bool = false
    ) {
        self.hostName = hostName
        self.stateDirectory = stateDirectory
        self.authKey = authKey
        self.controlURL = controlURL
        self.ephemeral = ephemeral
    }

    /// DNS-label-safe tailnet node name: "nucleic-<channel>-<name>" with non-alphanumerics
    /// dashed and runs collapsed. The optional channel (the Mac uses its release channel) sits
    /// ahead of the device name so a beta's node doesn't collide with a dev build's.
    public static func nodeName(for deviceName: String, channel: String = "") -> String {
        let dashed = deviceName.lowercased().map { ch -> Character in
            ch.isASCII && (ch.isLetter || ch.isNumber) ? ch : "-"
        }
        let collapsed = String(dashed).split(separator: "-").joined(separator: "-")
        let device = collapsed.isEmpty ? "device" : collapsed
        let prefix = channel.isEmpty ? "" : "\(channel)-"
        return "nucleic-\(prefix)\(device)"
    }

}
