import Foundation
import NucleicProtocol
import NucleicTailnet

/// The tailnet listener speaks the host transport seam natively (`start()` yields accepted
/// `FrameChannel`s, `stop()` winds down); the conformance lives here because `SyncListener`
/// is host-only while `NucleicTailnet` also compiles for the phone.
extension TailnetListener: SyncListener {}

/// Host-side transport selection + embedded-node configuration for the sync server
/// (Settings ▸ Remote ▸ Connection; SYNC_PROTOCOL §3). Mirrors `PushRelayConfig.resolve`:
/// env var first (scripted runs), then the defaults key the Settings picker writes.
public enum SyncTransportSetting {
    /// Legacy single-choice storage: `lan` | `tailnet` | `relay` (`SyncTransportHint` raw
    /// values). Seeds the multi-select set on first read; retained one release for rollback.
    public static let defaultsKey = "nucleic.sync.transport"
    public static let envKey = "NUCLEIC_SYNC_TRANSPORT"

    /// Multi-select storage: comma-joined raw values ("lan,tailnet"). The Settings
    /// checkboxes write this; the sync server listens on every enabled method at once.
    public static let setDefaultsKey = "nucleic.sync.transports"
    public static let setEnvKey = "NUCLEIC_SYNC_TRANSPORTS"

    /// The tailnet listener's fixed port. Unlike LAN (OS-assigned, carried in the QR
    /// because it changes), a tailnet port has no collision pressure — the IP is private to
    /// the user's tailnet — and staying fixed keeps a phone's stored hint valid forever.
    public static let tailnetPort: UInt16 = 43_753

    public static func resolve(
        environment: [String: String] = ProcessInfo.processInfo.environment,
        defaults: UserDefaults = .standard
    ) -> SyncTransportHint {
        let raw = environment[envKey] ?? defaults.string(forKey: defaultsKey)
        return raw.flatMap(SyncTransportHint.init(rawValue:)) ?? .lan
    }

    /// The set of connection methods the user enabled (Settings ▸ Remote ▸ Connection
    /// methods). Env first, then the multi-select defaults key; a missing/empty set falls
    /// back to the legacy single-choice value — `tailnet` historically ran LAN alongside,
    /// and a stale `relay` value (the picker never enabled it) degrades to LAN.
    public static func resolveEnabled(
        environment: [String: String] = ProcessInfo.processInfo.environment,
        defaults: UserDefaults = .standard
    ) -> Set<SyncTransportHint> {
        if let raw = environment[setEnvKey] ?? defaults.string(forKey: setDefaultsKey) {
            let parsed = parseEnabled(raw)
            if !parsed.isEmpty { return parsed }
        }
        switch resolve(environment: environment, defaults: defaults) {
        case .tailnet: return [.lan, .tailnet]
        // `.direct` is a runtime upgrade state, never a stored method — unreachable here.
        case .lan, .relay, .direct: return [.lan]
        }
    }

    public static func parseEnabled(_ raw: String) -> Set<SyncTransportHint> {
        Set(raw.split(separator: ",").compactMap {
            SyncTransportHint(rawValue: $0.trimmingCharacters(in: .whitespaces))
        })
    }

    /// Stable serialization (allCases order) so the stored string doesn't churn.
    public static func serializeEnabled(_ set: Set<SyncTransportHint>) -> String {
        SyncTransportHint.allCases.filter(set.contains).map(\.rawValue).joined(separator: ",")
    }

    public static func saveEnabled(_ set: Set<SyncTransportHint>, defaults: UserDefaults = .standard) {
        defaults.set(serializeEnabled(set), forKey: setDefaultsKey)
    }

    /// This Mac's embedded-node config. The state directory is the node's identity on the
    /// tailnet — per release channel, so a beta and a local dev build don't fight over one
    /// registration. The node registers via interactive browser login on first start; once
    /// the state directory holds a registered identity, later starts are silent.
    public static func nodeConfig(hostName: String) -> TailnetConfig {
        TailnetConfig(
            hostName: tailnetHostName(hostName),
            stateDirectory: stateDirectory)
    }

    private static var stateDirectory: URL {
        NucleicPaths.dataRoot
            .appendingPathComponent("tailnet\(channelSuffix)", isDirectory: true)
    }

    /// The node name in the tailnet admin console: "nucleic-<channel>-<mac-name>",
    /// DNS-label-safe, with the channel separating a beta's node from a dev build's.
    static func tailnetHostName(_ hostName: String) -> String {
        TailnetConfig.nodeName(for: hostName, channel: String(channelSuffix.dropFirst()))
    }

    /// Same channel → suffix mapping as `ContainerManager`'s container names (Package.swift
    /// defines), so every per-channel artifact reads consistently.
    private static var channelSuffix: String {
        #if NUCLEIC_STABLE
        ""
        #elseif NUCLEIC_RC
        "-rc"
        #elseif NUCLEIC_BETA
        "-beta"
        #elseif NUCLEIC_CANARY
        "-canary"
        #else
        "-local"
        #endif
    }
}
