import Foundation
import NucleicProtocol

// os.Logger on Darwin; the stderr shim in LinuxSupport.swift elsewhere.
#if canImport(os)
import os
#endif

/// Mesh-cast tracing (publish drops, catch-up failures) — Console.app category "cast".
let castLog = Logger(subsystem: "com.nucleic", category: "cast")

/// The tip of one (origin, channel) this device holds — what a catch-up serve iterates and what
/// a subscriber sends as its cursors (`CastCursor(afterSeq: tipSeq)`).
public struct CastTip: Sendable, Equatable {
    public let originHostID: String
    public let channel: String
    public let mode: CastMode
    /// Highest retained `castSeq`.
    public let tipSeq: UInt64
    /// Lowest retained `castSeq` — the retention floor. A stream subscriber whose cursor is
    /// below `floorSeq - 1` has an unrecoverable gap here (⇒ `reset`).
    public let floorSeq: UInt64

    public init(
        originHostID: String, channel: String, mode: CastMode,
        tipSeq: UInt64, floorSeq: UInt64
    ) {
        self.originHostID = originHostID
        self.channel = channel
        self.mode = mode
        self.tipSeq = tipSeq
        self.floorSeq = floorSeq
    }
}

/// Durable storage for mesh casts — owned publishes and mirrored peers' casts in one table,
/// keyed by the mesh-wide identity `(originHostID, channel, castSeq)`. Implemented by
/// `GRDBMetadataStore` (the `cast_event` table, v27); a seam like `SessionMetadataStore` so
/// tests run it on the in-memory queue.
///
/// All writes are transactional and idempotent:
/// - `publishCast` assigns the next per-(origin, channel) seq, compacts state channels per key,
///   and sweeps stream retention, in one transaction.
/// - `ingestCasts` is INSERT-OR-IGNORE on the identity PK and returns ONLY the rows actually
///   inserted — the changed-guard that makes re-forward gossip loops terminate (a cast seen
///   twice inserts nothing ⇒ re-broadcasts nothing).
public protocol CastPersisting: Sendable {
    /// Publish one cast as `origin` (this device): assign the next castSeq durably, compact /
    /// sweep, and return the stored cast for fanout.
    func publishCast(
        origin: String, channel: String, mode: CastMode, key: String?,
        typeID: String, payload: Data, deleted: Bool, sentAt: Date
    ) async throws -> WireCast

    /// Idempotently store casts received from a peer; returns only the freshly inserted ones
    /// (state-compacted rows that were immediately superseded are still returned — the caller
    /// re-broadcasts what was new *to this device*, receivers compact for themselves).
    func ingestCasts(_ casts: [WireCast]) async throws -> [WireCast]

    /// Replace everything held for one (origin, channel) with `casts` — the `reset` apply path
    /// (a state snapshot from a fresher holder, or a stream re-baseline past a gap).
    func replaceCasts(origin: String, channel: String, with casts: [WireCast]) async throws

    /// Every (origin, channel) held, with tips + retention floors.
    func castTips() async throws -> [CastTip]

    /// Stream serve: ordered casts with `castSeq > afterSeq` for one (origin, channel).
    func castsAfter(origin: String, channel: String, afterSeq: UInt64) async throws -> [WireCast]

    /// State serve: the compacted latest-per-key set (excluding `deleted` tombstones older than
    /// live values is the writer's job; tombstones themselves are returned so late joiners drop
    /// the key too).
    func castStateSnapshot(origin: String, channel: String) async throws -> [WireCast]

    /// Drop everything from one origin — unpair/revoke cleanup.
    func removeCasts(origin: String) async throws
}
