import Foundation

/// Mesh casting — the general-purpose broadcast primitive (docs/MESH_TRANSFER.md sibling; the
/// generalization of the per-session event fan-out to arbitrary typed payloads). An origin host
/// (Mac or cloud runner) publishes `WireCast`s onto named channels; every connected device
/// receives them live, peer Macs durably mirror + re-forward them, and a late joiner converges
/// by subscribing with per-(origin, channel) cursors.
///
/// Invariants:
/// - **Identity = `(originHostID, channel, castSeq)`** — the universal dedup key. `castSeq` is
///   per-(origin, channel) monotonic, assigned durably at the origin. Delivery is at-least-once
///   (live batches interleave with catch-up, mirrors re-serve); consumers apply exactly-once by
///   ignoring an identity they already hold.
/// - **Ordering is per-(origin, channel) only.** There is no global order across origins;
///   `sentAt` is display-only and never drives merge decisions (wall clocks skew).
/// - **Propagation is HostMsg-only.** There is no ClientMsg that carries a cast, so a phone
///   structurally cannot publish or forge one; peers' casts arrive only over connections a Mac
///   dialed to pinned, session-owning peers — the trust tier that already gossips the roster.
/// - The payload is opaque bytes tagged by `typeID`; relays and mirrors never decode it, so a
///   channel invented by a future build flows through today's Macs untouched.
///
/// The cast protocol has its own format version, independent of `SyncProtocol.version` (which
/// stays 1) — the `TranscriptSyncFormat` pattern.
public enum CastFormat {
    public static let version = 1
}

/// How a channel converges for late joiners. Raw-string-backed so a future mode round-trips
/// through an old peer instead of throwing.
public struct CastMode: RawRepresentable, Sendable, Codable, Equatable, Hashable {
    public let rawValue: String
    public init(rawValue: String) { self.rawValue = rawValue }

    /// Compacted latest-value-per-`key`: only the newest cast per key survives; a late joiner
    /// gets the current snapshot (presence, dashboards, settings echoes).
    public static let state = CastMode(rawValue: "state")
    /// Ordered history: casts are retained (retention-capped) and replayed since the
    /// subscriber's cursor (feeds, messaging).
    public static let stream = CastMode(rawValue: "stream")
}

/// One cast. Self-describing — a batch may mix origins and channels; each element carries
/// everything a receiver needs to store, dedup, and route it.
public struct WireCast: Sendable, Codable, Equatable {
    /// Full SHA-256 hex of the origin's static key (`DeviceIdentity.hostID`) — never a name.
    public let originHostID: String
    public let channel: String
    /// Per-(origin, channel) monotonic, origin-assigned. The identity/dedup component.
    public let castSeq: UInt64
    /// Stamped per cast so relays and mirrors store/compact it correctly without a shared
    /// channel registry.
    public let mode: CastMode
    /// State channels: the compaction key (latest cast per key wins). Nil for stream channels.
    public let key: String?
    /// Payload type identifier (e.g. "nucleic.feed.event.v1") — the *feature's* contract, not
    /// the transport's. Receivers ignore payloads whose typeID they don't know.
    public let typeID: String
    /// Opaque payload bytes (CBOR/JSON — feature-defined). Never decoded by relays or mirrors.
    public let payload: Data
    /// State tombstone: the latest value for `key` is "removed". Omitted ⇒ false.
    public let deleted: Bool
    /// When the origin published — informational/display only; ordering is `castSeq`.
    public let sentAt: Date

    public init(
        originHostID: String, channel: String, castSeq: UInt64, mode: CastMode,
        key: String? = nil, typeID: String, payload: Data, deleted: Bool = false,
        sentAt: Date
    ) {
        self.originHostID = originHostID
        self.channel = channel
        self.castSeq = castSeq
        self.mode = mode
        self.key = key
        self.typeID = typeID
        self.payload = payload
        self.deleted = deleted
        self.sentAt = sentAt
    }

    private enum CodingKeys: String, CodingKey {
        case originHostID, channel, castSeq, mode, key, typeID, payload, deleted, sentAt
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.originHostID = try c.decode(String.self, forKey: .originHostID)
        self.channel = try c.decode(String.self, forKey: .channel)
        self.castSeq = try c.decode(UInt64.self, forKey: .castSeq)
        self.mode = try c.decode(CastMode.self, forKey: .mode)
        self.key = try c.decodeIfPresent(String.self, forKey: .key)
        self.typeID = try c.decode(String.self, forKey: .typeID)
        self.payload = try c.decode(Data.self, forKey: .payload)
        self.deleted = try c.decodeIfPresent(Bool.self, forKey: .deleted) ?? false
        self.sentAt = try c.decode(Date.self, forKey: .sentAt)
    }
}

/// Subscriber→host: what the subscriber already holds, per (origin, channel). An absent pair
/// means "from zero".
public struct CastCursor: Sendable, Codable, Equatable, Hashable {
    public let originHostID: String
    public let channel: String
    /// Exclusive: the host serves casts with `castSeq > afterSeq`.
    public let afterSeq: UInt64

    public init(originHostID: String, channel: String, afterSeq: UInt64) {
        self.originHostID = originHostID
        self.channel = channel
        self.afterSeq = afterSeq
    }
}

/// `ClientMsg.castSubscribe` payload: opt in to the host's cast feed — live fanout plus a
/// catch-up pass over every (origin, channel) the host holds at the subscriber's scope. Send
/// only when the host advertised `WireCapabilities.canCast`; an older host throws on the
/// unknown tag.
public struct CastSubscribe: Sendable, Codable, Equatable {
    /// Restrict to these channels; nil ⇒ everything the host serves at the subscriber's scope.
    public let channels: [String]?
    /// Cursors for what the subscriber already holds — covering every origin it knows, not just
    /// this host, so any holder can top up any origin (convergence-via-any-holder).
    public let cursors: [CastCursor]
    public let formatVersion: Int

    public init(
        channels: [String]? = nil, cursors: [CastCursor] = [],
        formatVersion: Int = CastFormat.version
    ) {
        self.channels = channels
        self.cursors = cursors
        self.formatVersion = formatVersion
    }

    private enum CodingKeys: String, CodingKey { case channels, cursors, formatVersion }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.channels = try c.decodeIfPresent([String].self, forKey: .channels)
        self.cursors = try c.decodeIfPresent([CastCursor].self, forKey: .cursors) ?? []
        self.formatVersion =
            try c.decodeIfPresent(Int.self, forKey: .formatVersion) ?? CastFormat.version
    }
}

/// Host→subscriber: catch-up for ONE (origin, channel), possibly split into ordered batches —
/// the snapshot-then-tail shape of `Subscribe` → `SessionSnapshot`, generalized. State channels
/// get the compacted snapshot (`reset: true`, replace local state wholesale); stream channels
/// get the ordered gap `(cursor, tip]`, or `reset` when the cursor fell below the retention
/// floor (the gap is unrecoverable from this holder — consumers must tolerate a stream gap).
public struct CastCatchUp: Sendable, Codable, Equatable {
    /// Keeps a single frame small even for chatty channels, and bounds per-batch apply work.
    public static let maxCastsPerBatch = 200

    public let originHostID: String
    public let channel: String
    public let mode: CastMode
    public let batchIndex: Int
    /// State: (part of) the compacted latest-per-key set; stream: an ordered slice of the gap.
    public let casts: [WireCast]
    /// The last batch for this (origin, channel).
    public let complete: Bool
    /// The holder's tip at reply time — the subscriber advances its cursor here even when
    /// `casts` is empty (nothing new), so the next reconnect asks for less.
    public let tipSeq: UInt64
    /// Discard local rows for this (origin, channel) before applying: a state-snapshot replace,
    /// or a stream cursor below the holder's retention floor. Omitted ⇒ false.
    public let reset: Bool

    public init(
        originHostID: String, channel: String, mode: CastMode, batchIndex: Int,
        casts: [WireCast], complete: Bool, tipSeq: UInt64, reset: Bool = false
    ) {
        self.originHostID = originHostID
        self.channel = channel
        self.mode = mode
        self.batchIndex = batchIndex
        self.casts = casts
        self.complete = complete
        self.tipSeq = tipSeq
        self.reset = reset
    }

    private enum CodingKeys: String, CodingKey {
        case originHostID, channel, mode, batchIndex, casts, complete, tipSeq, reset
    }
    public init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        self.originHostID = try c.decode(String.self, forKey: .originHostID)
        self.channel = try c.decode(String.self, forKey: .channel)
        self.mode = try c.decode(CastMode.self, forKey: .mode)
        self.batchIndex = try c.decode(Int.self, forKey: .batchIndex)
        self.casts = try c.decode([WireCast].self, forKey: .casts)
        self.complete = try c.decode(Bool.self, forKey: .complete)
        self.tipSeq = try c.decode(UInt64.self, forKey: .tipSeq)
        self.reset = try c.decodeIfPresent(Bool.self, forKey: .reset) ?? false
    }
}

/// Host→subscriber: live fanout — a small batch of casts, possibly mixed origins/channels
/// (each `WireCast` is self-describing). Also the re-forward vehicle: a Mac that ingests a
/// peer's casts re-broadcasts only the rows it actually inserted, so gossip loops terminate.
public struct CastBatch: Sendable, Codable, Equatable {
    public let casts: [WireCast]

    public init(casts: [WireCast]) {
        self.casts = casts
    }
}
