import Foundation

/// What kind of device a mesh peer is (mesh P4). Raw-string-backed for forward compatibility:
/// a future kind decodes to its raw value rather than throwing, so an old build never fails to
/// read a newer peer record. A cloud `nucleicd` host slots in as `.cloud` with zero new mechanism.
public struct PeerKind: RawRepresentable, Sendable, Codable, Equatable, Hashable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }

    public static let mac = PeerKind(rawValue: "mac")
    public static let iphone = PeerKind(rawValue: "iphone")
    public static let cloud = PeerKind(rawValue: "cloud")

    /// Whether this kind can own sessions / accept transfers (Mac and cloud, not a phone).
    public var canOwnSessions: Bool { self == .mac || self == .cloud }

    /// A human label for when a paired device carries no custom name of its own.
    public var displayName: String {
        switch rawValue {
        case "mac": return "Mac"
        case "iphone": return "iPhone"
        case "cloud": return "Cloud"
        default: return rawValue.isEmpty ? "Device" : rawValue.capitalized
        }
    }
}

/// What a peer can *do*, orthogonal to what it's *allowed* (`DeviceScope`). All fields decode with
/// a `false` default so a record from a build that predates a capability reads cleanly.
public struct PeerCapabilities: Sendable, Codable, Equatable, Hashable {
    /// Accepts inbound sync connections (a Mac host, a cloud host).
    public var canHost: Bool
    /// Owns `SessionController`s / runs agents locally (Mac, cloud — not a phone).
    public var canRunAgents: Bool

    public init(canHost: Bool = false, canRunAgents: Bool = false) {
        self.canHost = canHost
        self.canRunAgents = canRunAgents
    }

    public static let none = PeerCapabilities()
    public static let hostAndAgents = PeerCapabilities(canHost: true, canRunAgents: true)

    private enum CodingKeys: String, CodingKey { case canHost, canRunAgents }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.canHost = try c.decodeIfPresent(Bool.self, forKey: .canHost) ?? false
        self.canRunAgents = try c.decodeIfPresent(Bool.self, forKey: .canRunAgents) ?? false
    }
}

/// Where a mesh peer can be dialed (mesh P4). Rides optionally in `Hello` (the client's own
/// addresses) and `Welcome` (the host's), and is persisted per peer so a Mac can dial a sibling
/// Mac later without a fresh pairing QR. Every field is optional — a peer advertises only the
/// paths it actually has (no LAN listener → no `lanHint`), and a record from a build that
/// predates a field reads it as nil.
public struct PeerAddresses: Sendable, Codable, Equatable, Hashable {
    /// Last-known LAN endpoint as "host:port". The port is the OS-assigned listener port, so
    /// this hint goes stale across restarts — fresh values arrive on every connect.
    public var lanHint: String?
    /// The peer's tailnet IP (v4 preferred). The sync port on a tailnet is fixed
    /// (`SyncTransportSetting.tailnetPort`), so the host alone is enough to dial.
    public var tailnet: String?
    /// The peer's relay room (hex SHA-256 of its static key — same string as its `HostID`).
    /// Populated once the relay data path ships (mesh P2 deploy-gated).
    public var relayRoomID: String?
    /// When these addresses were computed, so a reader can prefer the freshest record.
    public var updatedAt: Date?

    public init(
        lanHint: String? = nil, tailnet: String? = nil,
        relayRoomID: String? = nil, updatedAt: Date? = nil
    ) {
        self.lanHint = lanHint
        self.tailnet = tailnet
        self.relayRoomID = relayRoomID
        self.updatedAt = updatedAt
    }

    /// Whether there is anything here to dial.
    public var isEmpty: Bool { lanHint == nil && tailnet == nil && relayRoomID == nil }

    /// Fold an incoming snapshot onto this one **per field**: a present (non-nil) field in
    /// `incoming` wins; a field `incoming` omits keeps the current value. This is deliberately
    /// *not* a wholesale replace — the LAN hint is regenerated constantly (the OS-assigned
    /// listener port rebinds late at startup and changes on every relaunch), and those frequent
    /// LAN-only refreshes are often produced at a moment when the tailnet IP isn't in the snapshot
    /// yet (the tsnet node is still coming up). A blob-replace on each one would erase a still-valid
    /// `tailnet`/`relayRoomID`, leaving nothing to dial once the peers are no longer on one LAN.
    /// `updatedAt` takes the incoming stamp when present so freshness ordering still advances.
    ///
    /// Trade-off: a field the peer genuinely dropped (e.g. it turned tailnet off) lingers until a
    /// later snapshot carries a replacement, so a dial may hit a dead tailnet endpoint once and
    /// time out — strictly better than never having a tailnet candidate at all.
    public func merged(with incoming: PeerAddresses) -> PeerAddresses {
        PeerAddresses(
            lanHint: incoming.lanHint ?? lanHint,
            tailnet: incoming.tailnet ?? tailnet,
            relayRoomID: incoming.relayRoomID ?? relayRoomID,
            updatedAt: incoming.updatedAt ?? updatedAt)
    }

    private enum CodingKeys: String, CodingKey { case lanHint, tailnet, relayRoomID, updatedAt }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.lanHint = try c.decodeIfPresent(String.self, forKey: .lanHint)
        self.tailnet = try c.decodeIfPresent(String.self, forKey: .tailnet)
        self.relayRoomID = try c.decodeIfPresent(String.self, forKey: .relayRoomID)
        self.updatedAt = try c.decodeIfPresent(Date.self, forKey: .updatedAt)
    }
}

/// What a connecting client advertises about itself in `Hello` (mesh P4). `mesh` is a feature
/// level (0 = pre-mesh); a host only pushes mesh-only `HostMsg`s to a client that advertised
/// `mesh >= 1`. Optional on the wire, so an old client that omits it reads as pre-mesh.
public struct WireClientCapabilities: Sendable, Codable, Equatable {
    public let mesh: Int
    public let canHost: Bool
    public let canRunAgents: Bool
    /// Whether this device speaks mesh roster gossip (`meshRoster` in both directions) — it
    /// pushes its own roster and merges what the host pushes. Gates the host's `HostMsg.meshRoster`
    /// exactly like `WireCapabilities.canListPeers` gates `peerList`. Omitted ⇒ `false`, so an
    /// older device that only knows the display-only `listPeers` path is left on it.
    public let canSyncRoster: Bool
    /// Whether this device can execute delegated intelligence requests on its local Apple
    /// Foundation Models (`HostMsg.intelligenceRequest` → `ClientMsg.intelligenceResult`) — an
    /// AFM-capable Mac/iPhone standing in for the models a cloud runner doesn't have
    /// (docs/COVALENCE_RUNNER.md §5). A host only pushes requests to devices that set this.
    /// Omitted ⇒ `false`.
    public let canProvideIntelligence: Bool
    /// How this device rates as an intelligence worker (device class + Apple Silicon chip) —
    /// the mesh queue's placement gate and power-bias key. Meaningful only alongside
    /// `canProvideIntelligence`; omitted ⇒ scheduled as an unranked mobile device.
    public let intelligenceProfile: IntelligenceWorkerProfile?
    /// Whether this device consumes mesh casts (`CastMessages.swift`) — the host only pushes
    /// `HostMsg.casts`/`castCatchUp` to clients that set this (the `.unknown` fallback already
    /// protects old clients; this bit saves the metered relay bytes). Omitted ⇒ `false`.
    public let canCast: Bool
    /// Whether this device runs the direct-path upgrade of a relay session (Covalence direct,
    /// SYNC_PROTOCOL §3.3) — it may send `ClientMsg.directOffer`/`directSelect` and it acts on
    /// `HostMsg.directAnswer`/`directGo`. A host only answers offers from devices that set this
    /// (and only sends `directAnswer` to them). Omitted ⇒ `false`.
    public let canDirectConnect: Bool
    /// Whether this device mirrors Carbon shards (docs/CARBON_SHARDING.md §5.1) — Macs and
    /// runners only, never phones. A placement input (a member must set this to be counted in
    /// a replica set) and the gate for `HostMsg.carbonHeadAdvanced` pushes: a host only pushes
    /// heads to clients that set it. Omitted ⇒ `false`.
    public let canMirrorCarbon: Bool
    /// Whether this member is always-on (D4: a runner, or a user-pinned Mac) — pinned into
    /// every replica set it's eligible for (spec §5.2). Omitted ⇒ `false`.
    public let alwaysOn: Bool

    public init(
        mesh: Int = 0, canHost: Bool = false, canRunAgents: Bool = false,
        canSyncRoster: Bool = false, canProvideIntelligence: Bool = false,
        intelligenceProfile: IntelligenceWorkerProfile? = nil,
        canCast: Bool = false,
        canDirectConnect: Bool = false,
        canMirrorCarbon: Bool = false,
        alwaysOn: Bool = false
    ) {
        self.mesh = mesh
        self.canHost = canHost
        self.canRunAgents = canRunAgents
        self.canSyncRoster = canSyncRoster
        self.canProvideIntelligence = canProvideIntelligence
        self.intelligenceProfile = intelligenceProfile
        self.canCast = canCast
        self.canDirectConnect = canDirectConnect
        self.canMirrorCarbon = canMirrorCarbon
        self.alwaysOn = alwaysOn
    }

    private enum CodingKeys: String, CodingKey {
        case mesh, canHost, canRunAgents, canSyncRoster, canProvideIntelligence
        case intelligenceProfile
        case canCast
        case canDirectConnect
        case canMirrorCarbon, alwaysOn
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.mesh = try c.decodeIfPresent(Int.self, forKey: .mesh) ?? 0
        self.canHost = try c.decodeIfPresent(Bool.self, forKey: .canHost) ?? false
        self.canRunAgents = try c.decodeIfPresent(Bool.self, forKey: .canRunAgents) ?? false
        self.canSyncRoster = try c.decodeIfPresent(Bool.self, forKey: .canSyncRoster) ?? false
        self.canProvideIntelligence =
            try c.decodeIfPresent(Bool.self, forKey: .canProvideIntelligence) ?? false
        self.intelligenceProfile =
            try c.decodeIfPresent(IntelligenceWorkerProfile.self, forKey: .intelligenceProfile)
        self.canCast = try c.decodeIfPresent(Bool.self, forKey: .canCast) ?? false
        self.canDirectConnect = try c.decodeIfPresent(Bool.self, forKey: .canDirectConnect) ?? false
        self.canMirrorCarbon = try c.decodeIfPresent(Bool.self, forKey: .canMirrorCarbon) ?? false
        self.alwaysOn = try c.decodeIfPresent(Bool.self, forKey: .alwaysOn) ?? false
    }
}

/// One peer as reported by a host in answer to `ClientMsg.listPeers` (mesh P4). Powers the mesh
/// device list in Settings and the session-transfer destination picker (which Macs are reachable
/// right now to receive a session).
public struct PeerSummary: Sendable, Codable, Equatable {
    public let deviceID: String
    public let label: String
    public let kind: PeerKind
    public let capabilities: PeerCapabilities
    /// Whether this peer currently has a live connection to the reporting host.
    public let online: Bool
    public let lastSeenAt: Date?

    public init(
        deviceID: String, label: String, kind: PeerKind,
        capabilities: PeerCapabilities = .none, online: Bool = false, lastSeenAt: Date? = nil
    ) {
        self.deviceID = deviceID
        self.label = label
        self.kind = kind
        self.capabilities = capabilities
        self.online = online
        self.lastSeenAt = lastSeenAt
    }

    private enum CodingKeys: String, CodingKey {
        case deviceID, label, kind, capabilities, online, lastSeenAt
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.deviceID = try c.decode(String.self, forKey: .deviceID)
        self.label = try c.decode(String.self, forKey: .label)
        self.kind = try c.decodeIfPresent(PeerKind.self, forKey: .kind) ?? .iphone
        self.capabilities = try c.decodeIfPresent(PeerCapabilities.self, forKey: .capabilities) ?? .none
        self.online = try c.decodeIfPresent(Bool.self, forKey: .online) ?? false
        self.lastSeenAt = try c.decodeIfPresent(Date.self, forKey: .lastSeenAt)
    }
}

/// A full mesh-group member, as gossiped between devices (mesh "join"). Where `PeerSummary` is
/// display-only, a `MeshMember` carries the two things that make a peer *pinnable and dialable*
/// without a fresh QR — its Noise `staticPublicKey` (the pin) and its `addresses` (where to dial)
/// — plus `pairedAt`, the origin's membership-birth clock that the revocation-tombstone ordering
/// compares against. Rides in `MeshRosterPush`; a receiver merges unknown members into its own
/// `PairedDeviceStore` (auto-trusting the introducer) so the mesh converges: scanning any one
/// member's QR joins the scanner to the whole group.
public struct MeshMember: Sendable, Codable, Equatable {
    public let deviceID: String
    public let label: String
    public let kind: PeerKind
    public let capabilities: PeerCapabilities
    /// The member's Noise static public key — the pin a receiver stores to authenticate it on an
    /// IK reconnect. A public key, already broadcast in the pairing QR and every `Welcome`, so
    /// gossiping it leaks nothing. For a mac/cloud member the id is `sha256(key)`, so a receiver
    /// can verify `hostID(ofStaticKey:) == deviceID` and reject a forged (id, key) pair.
    public let staticPublicKey: Data
    /// Where the member can be dialed (nil for a phone, which is never dialed by a Mac).
    public let addresses: PeerAddresses?
    /// When the *origin* member first pinned this device — carried verbatim through every hop of
    /// gossip (never "when I learned about them"). The tombstone rule compares `revokedAt` against
    /// this, so a re-paired member (fresh, larger `pairedAt`) beats a stale revocation.
    public let pairedAt: Date
    public let lastSeenAt: Date?

    public init(
        deviceID: String, label: String, kind: PeerKind,
        capabilities: PeerCapabilities = .none, staticPublicKey: Data,
        addresses: PeerAddresses? = nil, pairedAt: Date, lastSeenAt: Date? = nil
    ) {
        self.deviceID = deviceID
        self.label = label
        self.kind = kind
        self.capabilities = capabilities
        self.staticPublicKey = staticPublicKey
        self.addresses = addresses
        self.pairedAt = pairedAt
        self.lastSeenAt = lastSeenAt
    }

    private enum CodingKeys: String, CodingKey {
        case deviceID, label, kind, capabilities, staticPublicKey, addresses, pairedAt, lastSeenAt
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.deviceID = try c.decode(String.self, forKey: .deviceID)
        self.label = try c.decode(String.self, forKey: .label)
        self.kind = try c.decodeIfPresent(PeerKind.self, forKey: .kind) ?? .iphone
        self.capabilities = try c.decodeIfPresent(PeerCapabilities.self, forKey: .capabilities) ?? .none
        self.staticPublicKey = try c.decode(Data.self, forKey: .staticPublicKey)
        self.addresses = try c.decodeIfPresent(PeerAddresses.self, forKey: .addresses)
        self.pairedAt = try c.decodeIfPresent(Date.self, forKey: .pairedAt) ?? Date(timeIntervalSince1970: 0)
        self.lastSeenAt = try c.decodeIfPresent(Date.self, forKey: .lastSeenAt)
    }
}

/// A revocation marker (mesh "join"): the group-wide record that a device was removed, so it
/// can't be re-gossiped back in by a member that hasn't heard yet. Rides in every `MeshRosterPush`
/// alongside live members. `revokedAt` is the ordering clock — a member record re-enters the mesh
/// only if it was re-paired *after* the revocation (`member.pairedAt > tombstone.revokedAt`).
public struct MeshTombstone: Sendable, Codable, Equatable {
    public let deviceID: String
    public let revokedAt: Date

    public init(deviceID: String, revokedAt: Date) {
        self.deviceID = deviceID
        self.revokedAt = revokedAt
    }
}

/// One gossip payload (mesh "join"): the sender's current view of the group — every live member
/// plus every revocation it knows. Pushed on connect (both directions) and whenever membership
/// changes. Merging is idempotent and monotonic, so over-gossiping only costs a few bytes and the
/// mesh converges once all stores agree.
public struct MeshRosterPush: Sendable, Codable, Equatable {
    public let members: [MeshMember]
    public let tombstones: [MeshTombstone]

    public init(members: [MeshMember] = [], tombstones: [MeshTombstone] = []) {
        self.members = members
        self.tombstones = tombstones
    }
}
