import Foundation

/// nash's on-disk spool of shell-event batches inside a macOS guest (docs/NASH.md §6.2, §7.5).
///
/// nash is the forced default shell in the guest, but inside a VM it can't reach the host's control
/// socket the way container nash does — so it spools each `shell-batch` as a line of JSONL. The exec op
/// points nash at ``defaultDir`` via `NUCLEIC_SHELL_SPOOL`; the `shell-events` op drains it and the host
/// polls that after every `mac_vm_exec`. Both sides share ``defaultDir`` so they always agree.
///
/// Framework-light and unit-tested here (plain `FileManager`), which is why it lives in `VMAgentCore`
/// rather than in the executable target alongside the op handlers.
public enum ShellSpool {
    /// The env var nash reads to learn where to spool (and the signal that spooling is on at all).
    public static let spoolEnvKey = "NUCLEIC_SHELL_SPOOL"
    /// The env var nash reads for capture depth (`off` | `meta` | `tap`).
    public static let captureEnvKey = "NUCLEIC_SHELL_CAPTURE"

    /// Default cap on bytes returned in one drain (the host re-polls for the rest).
    public static let defaultCap = 4 * 1024 * 1024

    /// Per-user spool directory. The macOS agent runs as the `agent` login account (not root) — so,
    /// unlike the Linux agent's root-owned `/var/spool/nucleic-nash`, the spool lives under the user's
    /// caches, which is always writable by both the agent and the shells it execs.
    public static var defaultDir: String {
        (NSHomeDirectory() as NSString).appendingPathComponent("Library/Caches/nucleic-nash")
    }

    /// The result of a drain: the concatenated NDJSON bytes, how many files contributed, whether more
    /// remains for the next poll, and the paths of the files fully read (which the caller deletes
    /// **after** it has sent the bytes, so the drain is at-least-once — a failed send re-delivers
    /// rather than loses).
    public struct DrainResult: Equatable {
        public var data: Data
        public var files: Int
        public var truncated: Bool
        public var drainedPaths: [String]
        public init(data: Data, files: Int, truncated: Bool, drainedPaths: [String]) {
            self.data = data
            self.files = files
            self.truncated = truncated
            self.drainedPaths = drainedPaths
        }
    }

    /// Read every `*.jsonl` batch file under `dir` (sorted, up to `maxBytes` total) concatenated as
    /// NDJSON. A missing directory drains nothing (not an error). A file larger than the remaining
    /// budget is skipped this round and left intact for the next poll — never split — so no batch is
    /// ever half-delivered. Does not delete anything; the caller removes ``DrainResult/drainedPaths``
    /// once the host has the bytes.
    public static func drain(dir: String, maxBytes: Int = defaultCap) -> DrainResult {
        let fm = FileManager.default
        guard let names = try? fm.contentsOfDirectory(atPath: dir) else {
            return DrainResult(data: Data(), files: 0, truncated: false, drainedPaths: [])
        }
        var acc = Data()
        var files = 0
        var truncated = false
        var drained: [String] = []
        for name in names.sorted() where name.hasSuffix(".jsonl") {
            if acc.count >= maxBytes { truncated = true; break }
            let path = (dir as NSString).appendingPathComponent(name)
            guard let bytes = fm.contents(atPath: path) else { continue }
            if bytes.count > maxBytes - acc.count {
                truncated = true  // whole file won't fit this round; leave it for the next drain
                continue
            }
            acc.append(bytes)
            files += 1
            drained.append(path)
        }
        return DrainResult(data: acc, files: files, truncated: truncated, drainedPaths: drained)
    }
}
