import Foundation

/// An append-only, thread-safe debug trace for the MDM plane, written to a file that survives the
/// process (docs/MACOS_VM_PROVISIONING_REPAIR_PLAN.md).
///
/// The per-attempt `transcript.log` records the *orchestration's* view — which steps ran, what the
/// surface saw. This records the **server's** view at a much finer grain: listener lifecycle, every
/// accepted connection and its state transitions, raw byte counts, parsed requests, routing decisions,
/// signature verification, queue movement, and every response. That distinction matters because the
/// failure being chased is invisible from the orchestration side: a guest whose TLS handshake fails
/// never produces an HTTP request, so the old request-level logging emitted nothing at all and the
/// server looked idle whether it was healthy, unreachable, or rejecting the client outright.
///
/// Deliberately cheap and total-failure-tolerant: logging must never be able to break enrollment, so
/// every write is best-effort and a log that cannot be opened degrades to a no-op sink.
public final class MDMDebugLog: @unchecked Sendable {
    /// Roll the file over once it passes this size, keeping one previous generation. A trace at this
    /// verbosity would otherwise grow without bound across repeated base builds.
    public static let maxBytes = 8 * 1024 * 1024

    private let lock = NSLock()
    private var handle: FileHandle?
    private let url: URL
    /// Monotonic sequence so interleaved connection lines can be ordered even at identical timestamps.
    private var sequence: UInt64 = 0

    /// Open (creating/rotating as needed). A failure to open yields a live object whose writes no-op.
    public init(url: URL) {
        self.url = url
        let fm = FileManager.default
        try? fm.createDirectory(
            at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
        // Rotate before opening so a run always starts with room.
        if let size = try? fm.attributesOfItem(atPath: url.path)[.size] as? UInt64,
            size > UInt64(Self.maxBytes)
        {
            let previous = url.appendingPathExtension("1")
            try? fm.removeItem(at: previous)
            try? fm.moveItem(at: url, to: previous)
        }
        if !fm.fileExists(atPath: url.path) {
            fm.createFile(
                atPath: url.path, contents: nil, attributes: [.posixPermissions: 0o600])
        }
        handle = try? FileHandle(forWritingTo: url)
        try? handle?.seekToEnd()
        write(category: "log", message: "=== MDM debug trace opened ===")
    }

    deinit { try? handle?.close() }

    /// Where this trace lives, so a failure message can point at it.
    public var path: String { url.path }

    /// Record one line under `category` (`listener`, `conn`, `http`, `checkin`, `command`, `queue`,
    /// `tls`, `sig`). Never throws, never blocks on anything but the file lock.
    public func log(_ category: String, _ message: String) {
        write(category: category, message: message)
    }

    /// Record a byte buffer as a bounded, single-line preview — printable text where possible, hex
    /// otherwise. Used for raw reads and request bodies, where "what actually arrived" is the question.
    public func logBytes(_ category: String, _ label: String, _ bytes: Data, limit: Int = 512) {
        guard !bytes.isEmpty else {
            write(category: category, message: "\(label): <empty>")
            return
        }
        let head = bytes.prefix(limit)
        var rendering: String
        if let text = String(data: head, encoding: .utf8),
            text.unicodeScalars.allSatisfy({ $0.isASCII && ($0.value >= 32 || $0 == "\n" || $0 == "\r" || $0 == "\t") })
        {
            rendering = text.split(whereSeparator: \.isNewline)
                .map { $0.trimmingCharacters(in: .whitespaces) }
                .filter { !$0.isEmpty }
                .joined(separator: " ⏎ ")
        } else {
            rendering = head.map { String(format: "%02x", $0) }.joined()
        }
        if bytes.count > limit { rendering += "… (\(bytes.count) bytes total)" }
        write(category: category, message: "\(label): \(rendering)")
    }

    private func write(category: String, message: String) {
        let stamp = Self.timestamp()
        lock.lock()
        defer { lock.unlock() }
        guard let handle else { return }
        sequence &+= 1
        let line = "[\(stamp)] #\(sequence) \(category.padding(toLength: 8, withPad: " ", startingAt: 0)) \(message)\n"
        try? handle.write(contentsOf: Data(line.utf8))
    }

    private static func timestamp() -> String {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter.string(from: Date())
    }
}
