import Foundation

/// The per-device command queue that drives the `/server` poll loop (docs/MACOS_VM_MDM.md §4).
///
/// Because Nucleic treats all clones as **one shared logical device** (§2.5), there is a single
/// queue. The protocol is strictly serial: the server hands out one command, the device works it and
/// reports back, then asks for the next. So the queue tracks exactly one **in-flight** command at a
/// time and does not hand out the next until the current one is acknowledged (or errored) — matching
/// how `mdmclient` actually polls.
///
/// An actor because both the enrollment drain (Mode A) and live nudges (Mode B) touch it
/// concurrently with the HTTP handlers.
public actor MDMCommandQueue {
    /// A queued command plus its delivery state.
    public struct Entry: Sendable {
        public let command: MDMCommand
        public var deliveredAt: Date?
        public var completedStatus: String?  // "Acknowledged" / "Error" once terminal
    }

    private var pending: [MDMCommand] = []
    /// The command handed to the device but not yet acknowledged.
    private var inFlight: MDMCommand?
    /// Completed commands, newest last — retained for status/verification (Mode A checks these).
    private var completed: [(command: MDMCommand, status: String)] = []

    public init() {}

    /// Add a command to the tail of the queue.
    public func enqueue(_ command: MDMCommand) {
        pending.append(command)
    }

    public func enqueue(_ commands: [MDMCommand]) {
        pending.append(contentsOf: commands)
    }

    /// The device reported `status` for `commandUUID` (its previous command). Clears the in-flight
    /// slot when the report is terminal so the next `next()` can advance. `NotNow` leaves it in
    /// flight to be re-offered. Returns `true` if the report matched the in-flight command.
    @discardableResult
    public func report(status: String, commandUUID: String?) -> Bool {
        guard let uuid = commandUUID, let flight = inFlight, flight.uuid == uuid else {
            return false
        }
        switch status {
        case "Acknowledged", "Error", "CommandFormatError":
            completed.append((flight, status))
            inFlight = nil
        case "NotNow":
            break  // keep in flight; re-offered on the next poll
        default:
            break
        }
        return true
    }

    /// The next command to send for a device that just PUT a status report. Follows the protocol:
    ///   • a terminal/idle report first advances the in-flight slot via ``report(status:commandUUID:)``;
    ///   • if a command is still in flight (e.g. `NotNow`), re-offer it;
    ///   • otherwise pop the next pending command into flight and return it;
    ///   • `nil` means "queue empty" → the handler sends an empty 200 to end the poll loop.
    public func next(afterReport report: MDMCommandStatusReport) -> MDMCommand? {
        _ = self.report(status: report.status, commandUUID: report.commandUUID)
        if let flight = inFlight { return flight }  // NotNow re-offer
        guard !pending.isEmpty else { return nil }
        let cmd = pending.removeFirst()
        inFlight = cmd
        return cmd
    }

    // MARK: - Introspection (Mode A verification / spike logging)

    public var pendingCount: Int { pending.count }
    public var inFlightUUID: String? { inFlight?.uuid }
    public var completedCommands: [(command: MDMCommand, status: String)] { completed }

    /// Terminal status for each requested command that has completed. Callers retain the UUIDs from
    /// their own enqueue batch so older acknowledgements cannot make a later policy push look valid.
    public func completionStatuses(for commandUUIDs: [String]) -> [String: String] {
        let wanted = Set(commandUUIDs)
        return Dictionary(uniqueKeysWithValues: completed.compactMap { entry in
            wanted.contains(entry.command.uuid) ? (entry.command.uuid, entry.status) : nil
        })
    }

    /// Whether every enqueued command has completed and none is in flight — the signal Mode A waits
    /// on before sealing the base.
    public var isDrained: Bool { pending.isEmpty && inFlight == nil }

    /// How many completed with a non-error status.
    public var acknowledgedCount: Int { completed.filter { $0.status == "Acknowledged" }.count }
}
