import Foundation

/// A pure, in-memory holding of mesh casts keyed by (origin, channel) — the client-side
/// analogue of the host's `cast_event` table, with the same semantics: identity dedup on
/// `(originHostID, channel, castSeq)`, state-channel compaction to latest-per-key, stream
/// retention per `CastChannelRegistry`, and cursor derivation for `castSubscribe`.
///
/// Used by the iPhone (`RemoteStore`) as its merged cast projection across every connected
/// host: at-least-once delivery from N Macs becomes exactly-once apply here. Pure value type,
/// so it's trivially testable and snapshot-persistable (encode `allCasts`, reload later).
public struct CastLedger: Sendable, Equatable {
    /// Casts per (origin, channel), ascending `castSeq`.
    private var held: [Key: [WireCast]] = [:]

    private struct Key: Hashable, Sendable {
        let origin: String
        let channel: String
    }

    public init(casts: [WireCast] = []) {
        _ = apply(casts)
    }

    /// Apply a batch (live or catch-up-continuation): returns only the casts that were actually
    /// new — the changed-guard consumers key UI refreshes off. Duplicate identities (the same
    /// cast arriving via two connected Macs) are dropped; state channels compact per key.
    @discardableResult
    public mutating func apply(_ casts: [WireCast]) -> [WireCast] {
        var fresh: [WireCast] = []
        for cast in casts {
            let key = Key(origin: cast.originHostID, channel: cast.channel)
            var row = held[key] ?? []
            guard !row.contains(where: { $0.castSeq == cast.castSeq }) else { continue }
            let at = row.firstIndex { $0.castSeq > cast.castSeq } ?? row.endIndex
            row.insert(cast, at: at)
            held[key] = compacted(row, channel: cast.channel, mode: cast.mode)
            fresh.append(cast)
        }
        return fresh
    }

    /// Apply one catch-up batch: a `reset` first batch replaces the (origin, channel) wholesale
    /// (a state snapshot from a fresher holder, or a stream re-baseline past a retention gap);
    /// later batches append. Returns the newly applied casts.
    @discardableResult
    public mutating func applyCatchUp(_ batch: CastCatchUp) -> [WireCast] {
        if batch.reset {
            held[Key(origin: batch.originHostID, channel: batch.channel)] = nil
        }
        return apply(batch.casts)
    }

    /// The cursors a `castSubscribe` carries: the tip of every (origin, channel) held.
    public var cursors: [CastCursor] {
        held.compactMap { key, row in
            guard let tip = row.last?.castSeq else { return nil }
            return CastCursor(originHostID: key.origin, channel: key.channel, afterSeq: tip)
        }
    }

    /// One channel merged across every origin, newest-last by `sentAt` with a deterministic
    /// (origin, castSeq) tiebreak — display order for feed-like surfaces. State tombstones
    /// (`deleted`) are filtered; a tombstone's job is done once compaction dropped its key.
    public func casts(channel: String) -> [WireCast] {
        held.filter { $0.key.channel == channel }
            .flatMap(\.value)
            .filter { !$0.deleted }
            .sorted {
                if $0.sentAt != $1.sentAt { return $0.sentAt < $1.sentAt }
                if $0.originHostID != $1.originHostID { return $0.originHostID < $1.originHostID }
                return $0.castSeq < $1.castSeq
            }
    }

    /// The latest live value per key for a state channel, across all origins (deleted keys
    /// excluded). For per-host cards like runner presence the key is the origin's hostID.
    public func stateValues(channel: String) -> [String: WireCast] {
        var byKey: [String: WireCast] = [:]
        for cast in casts(channel: channel) {
            guard let key = cast.key else { continue }
            byKey[key] = cast
        }
        return byKey
    }

    /// Everything held (persistence snapshot).
    public var allCasts: [WireCast] { held.values.flatMap { $0 } }

    /// Drop one origin entirely (unpair/revoke).
    public mutating func removeOrigin(_ origin: String) {
        held = held.filter { $0.key.origin != origin }
    }

    private func compacted(_ row: [WireCast], channel: String, mode: CastMode) -> [WireCast] {
        let policy = CastChannelRegistry.policy(for: channel)
        if (policy.mode ?? mode) == .state {
            // Latest per key wins (ascending seq order ⇒ later entries overwrite).
            var latest: [String: WireCast] = [:]
            for cast in row { latest[cast.key ?? ""] = cast }
            return latest.values.sorted { $0.castSeq < $1.castSeq }
        }
        return row.count > policy.maxEvents ? Array(row.suffix(policy.maxEvents)) : row
    }
}
