import Foundation
import GRDB

/// Nash Viewer persistence (`NashEventPersisting`) over the `nash_event` table (v35). Writes are
/// one transaction per batch — insert then evict — so the table is never observably over budget,
/// and a torn flush can't leave rows the retention pass skipped.
extension GRDBMetadataStore: NashEventPersisting {
    public func saveNashEvents(_ records: [NashEventRecord], retention: NashRetention) async throws {
        guard !records.isEmpty else { return }
        let rows = records.compactMap(NashEventRow.init(record:))
        guard !rows.isEmpty else { return }
        let sessions = Set(rows.map(\.session_id))
        try await writer.write { db in
            for row in rows { try row.insert(db) }
            for session in sessions {
                try Self.trimNashSession(db, sessionID: session, rows: retention.rowsPerSession)
            }
            if let budget = retention.previewByteBudget {
                try Self.sweepNashPreviewBudget(db, budget: budget)
            }
        }
    }

    public func loadNashEvents(
        sessionID: SessionID, excludingLaunch: String, limit: Int
    ) async throws -> [NashEventRecord] {
        guard limit > 0 else { return [] }
        let id = sessionID.rawValue
        let rows = try await writer.read { db in
            // Newest `limit` — the tail is what a viewer wants when history exceeds the ring.
            try NashEventRow.fetchAll(db, sql: """
                SELECT * FROM nash_event
                WHERE session_id = ? AND launch_id <> ?
                ORDER BY id DESC LIMIT ?
                """, arguments: [id, excludingLaunch, limit])
        }
        // Back to oldest-first: the ring, and the panel's reading order, are chronological.
        return rows.reversed().compactMap { $0.toRecord() }
    }

    public func nashEventTotals(
        sessionID: SessionID, excludingLaunch: String
    ) async throws -> [NashEvent.Kind: Int] {
        let id = sessionID.rawValue
        return try await writer.read { db in
            try Row.fetchAll(db, sql: """
                SELECT kind, COUNT(*) AS n FROM nash_event
                WHERE session_id = ? AND launch_id <> ?
                GROUP BY kind
                """, arguments: [id, excludingLaunch])
                .reduce(into: [NashEvent.Kind: Int]()) { acc, row in
                    guard let kind = NashEvent.Kind(rawValue: row["kind"]) else { return }
                    acc[kind] = row["n"]
                }
        }
    }

    public func deleteNashEvents(sessionID: SessionID) async throws {
        let id = sessionID.rawValue
        try await writer.write { db in
            try db.execute(sql: "DELETE FROM nash_event WHERE session_id = ?", arguments: [id])
        }
    }

    // MARK: - Retention (docs/NASH.md §9.4)

    /// Row cap for one session: keep the newest `rows`, drop the rest.
    private static func trimNashSession(_ db: Database, sessionID: String, rows: Int) throws {
        try db.execute(sql: """
            DELETE FROM nash_event
            WHERE session_id = :session AND id NOT IN (
              SELECT id FROM nash_event WHERE session_id = :session ORDER BY id DESC LIMIT :keep
            )
            """, arguments: ["session": sessionID, "keep": rows])
    }

    /// Global payload budget: walk the table newest-first, accumulating preview sizes, and drop
    /// everything past the point the running total crosses `budget`. Rows without a preview cost
    /// nothing here — they are bounded by the per-session row cap instead, so a session of bare
    /// `cd` lines is never evicted to make room for one noisy pipe.
    private static func sweepNashPreviewBudget(_ db: Database, budget: Int) throws {
        try db.execute(sql: """
            DELETE FROM nash_event WHERE id IN (
              SELECT id FROM (
                SELECT id, SUM(LENGTH(COALESCE(preview, ''))) OVER (
                  ORDER BY id DESC ROWS UNBOUNDED PRECEDING
                ) AS running
                FROM nash_event
              ) WHERE running > ?
            )
            """, arguments: [budget])
    }
}

/// The `nash_event` row. `details` holds ``NashEvent/Details`` as JSON — the viewer draws a
/// genuinely different layout per kind and cannot do it from the flat `summary` alone, and the
/// shape is too kind-dependent to spread across columns.
private struct NashEventRow: Codable, FetchableRecord, PersistableRecord {
    static let databaseTableName = "nash_event"
    /// Assigned by SQLite on insert; nil on the way in.
    var id: Int64?
    var session_id: String
    var launch_id: String
    var at: Date
    var env_kind: String
    var env_id: String
    var env_label: String
    var shell_id: String
    var kind: String
    var summary: String
    var detail: String?
    var details: String
    var preview: String?
    var bytes: Int?
    var truncated: Bool
    var hash: String?

    /// nil when the details won't encode — a row whose structure can't be stored is not worth
    /// storing half of, and dropping it costs one line of history rather than the whole flush.
    init?(record: NashEventRecord) {
        guard let json = try? JSONEncoder().encode(record.details),
            let details = String(data: json, encoding: .utf8)
        else { return nil }
        id = nil
        session_id = record.sessionID.rawValue
        launch_id = record.launchID
        at = record.at
        env_kind = record.environment.kind.rawValue
        env_id = record.environment.id
        env_label = record.environment.label
        shell_id = record.shellID
        kind = record.kind.rawValue
        summary = record.summary
        detail = record.detail
        self.details = details
        preview = record.preview
        bytes = record.bytes
        truncated = record.truncated
        hash = record.hash
    }

    /// nil for a row this build can't read — a kind or a `details` shape written by a newer
    /// version. Skipping it loses one row of history; failing the load would lose all of it.
    func toRecord() -> NashEventRecord? {
        guard let kind = NashEvent.Kind(rawValue: kind),
            let environment = NashEnvironment.fromWire(kind: env_kind, id: env_id, label: env_label),
            let details = try? JSONDecoder().decode(
                NashEvent.Details.self, from: Data(details.utf8))
        else { return nil }
        return NashEventRecord(
            sessionID: SessionID(rawValue: session_id), launchID: launch_id, at: at,
            environment: environment, shellID: shell_id, kind: kind, summary: summary,
            detail: detail, details: details, preview: preview, bytes: bytes,
            truncated: truncated, hash: hash)
    }
}
