import Foundation

/// Full-transcript mesh sync (copy, not move). Lets any device pull a session's *entire*
/// transcript from any peer that holds it — the authoritative host running the session, or
/// another Mac that already mirrored it — instead of the 200-event tail a cold `subscribe`
/// returns. This is the read-only, idempotent, multi-holder cousin of session transfer
/// (`TransferMessages.swift`, mesh P5): transfer *moves* one session to a single destination and
/// tombstones the source; this *copies* history to many holders and never mutates the source.
///
/// Wire shape mirrors transfer: the **requester acts as a client of the holder**. The request is
/// a `ClientMsg` (`fetchTranscript`) — which throws on an unknown tag, so every send is gated on
/// the holder advertising `WireCapabilities.canSyncTranscripts` — and the replies are `HostMsg`
/// cases (forward-compatible `.unknown` fallback) that also surface as `SyncClient.Event`s so a
/// requester receiving them as a client can observe them.
///
/// The fetch protocol has its own format version, independent of `SyncProtocol.version` (which
/// stays 1): a holder replies `unsupportedFormat` to a request it can't handle.
public enum TranscriptSyncFormat {
    public static let version = 1
}

/// Requester→holder: "send me this session's events after `afterSeq`." `afterSeq == 0` asks for
/// the whole transcript (and the holder returns the session header so the receiver can build a
/// faithful mirror file); a positive cursor resumes an existing mirror at its highest contiguous
/// seq, so top-ups ship only the gap. Forward-paging by seq matches the append-only transcript
/// model (RUNTIME §2.1) — the same cursor the warm `subscribe` path already understands.
public struct TranscriptFetch: Sendable, Codable, Equatable {
    /// Correlates this request with its `transcriptChunk` / completion / unavailable replies —
    /// a requester runs several fetches concurrently, so replies are keyed by this id, not by
    /// session (a holder could be serving the same session to two callers at different cursors).
    public let requestID: String
    public let sessionID: SessionID
    /// Exclusive lower bound: the holder returns events with `seq > afterSeq`. 0 ⇒ whole history.
    public let afterSeq: UInt64
    public let formatVersion: Int

    public init(
        requestID: String, sessionID: SessionID, afterSeq: UInt64,
        formatVersion: Int = TranscriptSyncFormat.version
    ) {
        self.requestID = requestID
        self.sessionID = sessionID
        self.afterSeq = afterSeq
        self.formatVersion = formatVersion
    }
}

/// Holder→requester: one ordered slice of the requested events. The holder streams as many of
/// these as needed (each ≤ `maxEventsPerBatch`) in ascending `seq` order, then a terminal
/// `TranscriptFetchComplete`. `batchIndex` lets the requester detect a dropped/reordered batch
/// defensively; events themselves carry their canonical `seq`, which the receiver preserves.
public struct TranscriptChunk: Sendable, Codable, Equatable {
    /// Max events per batch — keeps a single frame well under the 16 MiB cap even for verbose
    /// tool-call events, and bounds the requester's per-batch apply work.
    public static let maxEventsPerBatch = 500

    public let requestID: String
    public let sessionID: SessionID
    public let batchIndex: Int
    public let events: [AgentEvent]

    public init(requestID: String, sessionID: SessionID, batchIndex: Int, events: [AgentEvent]) {
        self.requestID = requestID
        self.sessionID = sessionID
        self.batchIndex = batchIndex
        self.events = events
    }
}

/// Holder→requester: terminal success — every batch has been sent. `throughSeq` is the highest
/// seq shipped (== the holder's tip at request time); the receiver advances its mirror cursor to
/// it only after verifying the batches were contiguous from `afterSeq + 1`. `headerJSON` is the
/// encoded transcript header (line 0), present iff the request was a full fetch (`afterSeq == 0`)
/// so the receiver can write a faithful mirror file; nil on an incremental top-up (the mirror
/// header already exists). It's carried as opaque bytes because the header type lives in the
/// higher NucleicCore layer — only the receiver there decodes it.
public struct TranscriptFetchComplete: Sendable, Codable, Equatable {
    public let requestID: String
    public let sessionID: SessionID
    public let throughSeq: UInt64
    public let headerJSON: Data?
    /// The holder's revert epoch for this session at serve time (see
    /// `SessionSummary.revertEpoch`), on full fetches *and* top-ups. The requester compares it
    /// against the owner's advertised epoch before committing, so a fetch that raced a revert —
    /// or a holder re-serving a stale mirror — is rejected instead of stitched onto the mirror
    /// at reused seqs. `nil` from a holder that predates the field (no verification possible).
    public let revertEpoch: UInt64?

    public init(
        requestID: String, sessionID: SessionID, throughSeq: UInt64, headerJSON: Data? = nil,
        revertEpoch: UInt64? = nil
    ) {
        self.requestID = requestID
        self.sessionID = sessionID
        self.throughSeq = throughSeq
        self.headerJSON = headerJSON
        self.revertEpoch = revertEpoch
    }
}

/// Holder→requester: terminal failure — this holder can't serve the request. The requester
/// tries the next holder (`notHeld` is the common, expected "I don't have this" while probing
/// the mesh). Raw-string-backed so an older peer decodes a newer reason as its raw value.
public struct TranscriptUnavailable: Sendable, Codable, Equatable {
    public struct Reason: RawRepresentable, Sendable, Codable, Equatable, Hashable {
        public let rawValue: String
        public init(rawValue: String) { self.rawValue = rawValue }

        /// This holder has neither a live controller nor an on-disk transcript for the session —
        /// the expected reply while probing peers for one that holds it.
        public static let notHeld = Reason(rawValue: "notHeld")
        /// The session is archived here and not proactively retained (scope is active sessions).
        public static let archived = Reason(rawValue: "archived")
        /// The holder doesn't support the requested `formatVersion`.
        public static let unsupportedFormat = Reason(rawValue: "unsupportedFormat")
        /// The holder hit an unexpected error reading its transcript.
        public static let internalError = Reason(rawValue: "internalError")
    }

    public let requestID: String
    public let sessionID: SessionID
    public let reason: Reason

    public init(requestID: String, sessionID: SessionID, reason: Reason) {
        self.requestID = requestID
        self.sessionID = sessionID
        self.reason = reason
    }
}
