import Foundation

/// One nash emission as it is written to — and read back from — durable storage: a ``NashEvent``
/// minus the per-launch `seq`, plus the launch that produced it.
///
/// The projection (`ShellReportCall.Kind` → row) happens once, on the way in, so a restored row
/// renders through exactly the same fields a live one does. `launchID` is what keeps the two
/// apart: a session that emitted while it was closed has already flushed those rows to disk, so
/// hydration must load *previous* runs only or the panel would show this run's events twice.
public struct NashEventRecord: Sendable, Equatable {
    public let sessionID: SessionID
    /// The app run that recorded this row (``NashEventLog/launchID``).
    public let launchID: String
    public let at: Date
    public let environment: NashEnvironment
    public let shellID: String
    public let kind: NashEvent.Kind
    public let summary: String
    public let detail: String?
    public let details: NashEvent.Details
    public let preview: String?
    public let bytes: Int?
    public let truncated: Bool
    public let hash: String?

    public init(
        sessionID: SessionID, launchID: String, at: Date, environment: NashEnvironment,
        shellID: String, kind: NashEvent.Kind, summary: String, detail: String?,
        details: NashEvent.Details, preview: String?, bytes: Int?, truncated: Bool, hash: String?
    ) {
        self.sessionID = sessionID
        self.launchID = launchID
        self.at = at
        self.environment = environment
        self.shellID = shellID
        self.kind = kind
        self.summary = summary
        self.detail = detail
        self.details = details
        self.preview = preview
        self.bytes = bytes
        self.truncated = truncated
        self.hash = hash
    }

    public init(event: NashEvent, launchID: String) {
        self.init(
            sessionID: event.sessionID, launchID: launchID, at: event.at,
            environment: event.environment, shellID: event.shellID, kind: event.kind,
            summary: event.summary, detail: event.detail, details: event.details,
            preview: event.preview, bytes: event.bytes, truncated: event.truncated,
            hash: event.hash)
    }

    /// Back to a row the viewer can render, numbered into this launch's sequence space.
    public func event(seq: UInt64) -> NashEvent {
        NashEvent(
            seq: seq, at: at, sessionID: sessionID, environment: environment, shellID: shellID,
            kind: kind, summary: summary, detail: detail, details: details, preview: preview,
            bytes: bytes, truncated: truncated, hash: hash)
    }
}

/// How much of the shell history stays on disk (docs/NASH.md §9.4).
///
/// Two independent bounds, because rows and bytes run away for different reasons: a session that
/// runs ten thousand trivial commands is bounded by `rowsPerSession`, while one that pipes a few
/// hundred megabytes through `$(…)` is bounded by `previewByteBudget`. Both evict oldest-first.
public struct NashRetention: Sendable, Equatable {
    /// Rows kept per session (or per app-scoped origin — `buildrun:<projectID>` and friends are
    /// keyed the same way, and no session row exists to cascade them away).
    public var rowsPerSession: Int
    /// Total size of retained payload previews across every session, in bytes. Nil skips the
    /// sweep for this write — it is a whole-table scan, so ``NashEventLog`` only asks for it
    /// once enough new payload has landed to be worth it.
    public var previewByteBudget: Int?

    public init(rowsPerSession: Int = 5_000, previewByteBudget: Int? = 64 * 1024 * 1024) {
        self.rowsPerSession = max(1, rowsPerSession)
        self.previewByteBudget = previewByteBudget
    }
}

/// Durable storage for the Nash Viewer's feed (docs/NASH.md §9.4) — the shell history a session
/// keeps across app launches, redacted previews included (redaction already happened in-guest,
/// §5.4; nothing here re-redacts).
///
/// Separate from ``SessionMetadataStore`` for the same reason `CastPersisting` is: it is one
/// bounded table with its own retention rules, reached through `database as? NashEventPersisting`,
/// so a store that doesn't implement it simply leaves the viewer in-memory-only.
public protocol NashEventPersisting: Sendable {
    /// Append a batch and apply `retention` in the same transaction, so the table can never sit
    /// over budget between a write and its sweep.
    func saveNashEvents(_ records: [NashEventRecord], retention: NashRetention) async throws

    /// The newest `limit` rows for one session from runs *other* than `excludingLaunch`, oldest
    /// first — the order the viewer's ring wants them in.
    func loadNashEvents(
        sessionID: SessionID, excludingLaunch: String, limit: Int
    ) async throws -> [NashEventRecord]

    /// Per-kind counts of what those runs left behind, so the panel header can count emissions it
    /// restored without loading every row.
    func nashEventTotals(
        sessionID: SessionID, excludingLaunch: String
    ) async throws -> [NashEvent.Kind: Int]

    /// Drop one session's stored rows (the viewer's "Clear Retained Rows", and session deletion).
    func deleteNashEvents(sessionID: SessionID) async throws
}
