import Foundation

/// One running macOS guest, for the VM settings panel.
public struct MacVMEntry: Sendable, Equatable {
    public let name: String
    public let ipAddress: String?
    public init(name: String, ipAddress: String?) {
        self.name = name
        self.ipAddress = ipAddress
    }
}

/// App-level coordinator for per-session **macOS guest VMs** (injected like ``ContainerManager``).
/// Owns the lifecycle policy the low-level ``MacVMEngine`` doesn't track: one long-lived VM per
/// session, an idle timer that stops it after inactivity, teardown on session end, on-disk
/// reconciliation at launch, and — unique to macOS guests — a **concurrent-VM ceiling** (macOS caps
/// how many macOS guests run at once), so a fan-out of agents queues instead of failing a boot.
///
/// Activity is reference-counted exactly like the container manager: `ensureRunning` (start of every
/// turn that uses the VM) marks it busy and disarms the idle timer; `finished` marks the turn done and
/// re-arms it. The VM is only stopped when no turn is in flight.
public actor MacVMManager {
    private let engine: MacVMEngine

    private var active: [String: Int] = [:]              // in-flight turn count, by VM name
    private var idleTimeouts: [String: TimeInterval] = [:]
    private var idleTimers: [String: Task<Void, Never>] = [:]

    public init(engine: MacVMEngine = MacVMEngine()) {
        self.engine = engine
    }

    /// Whether the macOS-guest runtime is usable on this machine (Apple silicon). A `nil`
    /// ``MacVMEngine/unsupportedReason``.
    public nonisolated static var isSupported: Bool { MacVMEngine.isSupported }

    /// Stable macOS-VM name for a session — the `nucleic-mac-<short>` sibling of a container's
    /// `nucleic-<short>`. Lowercased (matches the container/disk-GC convention) and carrying the same
    /// per-channel suffix as containers so several installed Nucleic builds don't collide on the shared
    /// on-disk clone store.
    public nonisolated static func vmName(for session: SessionID) -> String {
        "nucleic-mac-\(session.short.lowercased())\(ContainerManager.channelSuffix)"
    }

    /// Stable **Linux**-VM name for a session — the `nucleic-lin-<short>` sibling of ``vmName(for:)``.
    /// A distinct prefix so a session's Linux and macOS clones never collide on the shared clone store.
    public nonisolated static func linuxVMName(for session: SessionID) -> String {
        "nucleic-lin-\(session.short.lowercased())\(ContainerManager.channelSuffix)"
    }

    /// Ensure the session's macOS VM is up and return its name plus SSH-reachable IP. Marks a turn as
    /// in-flight; pair with ``finished(name:)``. The concurrent-guest ceiling is enforced *inside* the
    /// engine (atomically, on the actor that owns the live registry), which throws
    /// ``MacVMError/concurrencyLimit(_:)`` when a new boot would exceed ``MacVMSettings/maxConcurrentVMs``
    /// — so a fan-out of agents surfaces "the Macs are busy, retry" rather than the OS rejecting a start
    /// opaquely. (Enforcing it here would be a TOCTOU: the check and the boot aren't atomic across actors.)
    public func ensureRunning(_ spec: MacVMSpec) async throws -> (name: String, ipAddress: String) {
        cancelIdleTimer(spec.name)
        active[spec.name, default: 0] += 1
        idleTimeouts[spec.name] = spec.idleTimeout
        do {
            return try await engine.ensureRunning(spec)
        } catch {
            finishBookkeeping(spec.name)  // roll back the activity bump so a failed boot doesn't pin busy
            throw error
        }
    }

    /// Run `argv` inside the named VM over SSH, returning a process handle (forwards to the engine; no
    /// lifecycle effect). Satisfies the same ``ProcessHandle`` contract a host/container exec does.
    public func exec(
        name: String, workdir: String?, env: [String: String], argv: [String]
    ) async throws -> any ProcessHandle {
        try await engine.exec(name: name, workdir: workdir, env: env, argv: argv)
    }

    /// One-shot: run a shell `command` in the named VM and capture exit code + stdout/stderr (the
    /// `mac_vm_exec` tool path). No lifecycle effect.
    public func run(
        name: String, command: String, workdir: String?, env: [String: String] = [:]
    ) async throws -> (exitCode: Int32, stdout: String, stderr: String) {
        try await engine.run(name: name, command: command, workdir: workdir, env: env)
    }

    /// Perform one computer-use action (screenshot / click / type / ax_dump / ax_press / …) in the
    /// named VM and return the post-action screenshot (base64 JPEG) + a summary (the
    /// `mac_vm_computer` tool path). `ref`/`value` feed the AX-semantic actions (native agent).
    /// No lifecycle effect.
    public func performComputerAction(
        name: String, action: String, x: Int?, y: Int?, text: String?,
        ref: String? = nil, value: String? = nil,
        scrollDirection: String?, scrollAmount: Int?, durationMs: Int?
    ) async throws -> (imageBase64: String?, summary: String) {
        try await engine.performComputerAction(
            name: name, action: action, x: x, y: y, text: text, ref: ref, value: value,
            scrollDirection: scrollDirection, scrollAmount: scrollAmount, durationMs: durationMs)
    }

    /// Copy `.app` bundles into a running guest's `/Applications` over vsock (forwards to the engine;
    /// no lifecycle effect) — pushing Settings' "Included apps" into a live clone without a base
    /// rebuild. Returns the bundle names that landed.
    @discardableResult
    public func installApps(name: String, appPaths: [String]) async throws -> [String] {
        try await engine.installApps(name: name, appPaths: appPaths)
    }

    /// Inject `.app` bundles into the golden base image without a full rebuild: the engine briefly boots
    /// the base, copies the apps into `/Applications`, and powers it off (forwards to the engine).
    /// Returns the bundle names that landed; new session clones include them afterward.
    @discardableResult
    public func installAppsIntoBase(appPaths: [String]) async throws -> [String] {
        try await engine.installAppsIntoBase(appPaths: appPaths)
    }

    /// Mark a turn finished. When the last in-flight turn ends, arm the idle timer.
    public func finished(name: String) { finishBookkeeping(name) }

    private func finishBookkeeping(_ name: String) {
        let remaining = max(0, (active[name] ?? 0) - 1)
        active[name] = remaining
        if remaining == 0 { armIdleTimer(name) }
    }

    /// Stop and remove the session's VM (and its on-disk clone). Returns the name if it could NOT be
    /// removed, else `nil` (including when no such VM existed).
    @discardableResult
    public func teardown(_ session: SessionID, waitForActive: Bool = false) async -> String? {
        // A session may have booted a macOS VM, a Linux VM, or neither — tear down both clone names.
        var failed: String?
        for name in [Self.vmName(for: session), Self.linuxVMName(for: session)] {
            if waitForActive { await waitUntilIdle(name) }
            cancelIdleTimer(name)
            active[name] = nil
            idleTimeouts[name] = nil
            let removed = await engine.remove(name: name)
            if !removed, await engine.vmExists(name) { failed = name }
        }
        return failed
    }

    /// Restart the session's VM in place (reclaims host RAM). Skips if a turn is in flight.
    public func restart(_ session: SessionID) async {
        let name = Self.vmName(for: session)
        guard (active[name] ?? 0) == 0 else { return }
        cancelIdleTimer(name)
        await engine.restart(name: name)
    }

    /// Whether the named VM is already booted and ready (forwards to the engine; no lifecycle effect).
    /// Lets a caller tell a cold boot — which can take a couple of minutes for a fresh clone — from a
    /// quick reuse before calling ``ensureRunning(_:)``, e.g. to narrate the wait into the transcript.
    public func isRunning(name: String) async -> Bool {
        await engine.isRunning(name)
    }

    /// The macOS VMs currently running this process, for the settings panel.
    public func runningVMs() async -> [MacVMEntry] {
        await engine.runningVMs()
    }

    /// Current GUI screenshot (base64 JPEG) of the named VM for the live monitor panel, or `nil` when
    /// it isn't running / can't be captured. No lifecycle effect (doesn't boot or keep the VM alive).
    public func captureScreen(name: String) async -> String? {
        await engine.captureScreen(name: name)
    }

    /// Best-effort CPU/RAM sample of the named running VM for the Control panel's resource meters, or
    /// `nil` when it isn't running. No lifecycle effect. Probes the guest over SSH, so callers should
    /// refresh it off their critical path (see `AppStore.refreshMacVMUsageInBackground`).
    public func sampleResourceUsage(name: String) async -> MacVMResourceSample? {
        await engine.sampleResourceUsage(name: name)
    }

    /// The one-time base-image build progress (download/install/provision), for the settings panel.
    public func baseProgress() async -> MacVMBaseProgress? {
        await engine.currentBaseProgress()
    }

    /// The installed guest macOS version of the current base bundle, for the settings panel.
    public func baseImageOSVersion() async -> String? {
        await engine.baseImageOSVersion()
    }

    /// The base image's provisioning status (installed / exec-ready / computer-use-ready), for the
    /// settings panel — drives the "needs SIP disabled once" guidance.
    public func baseStatus() async -> MacVMBaseStatus {
        await engine.currentBaseStatus()
    }

    /// Filesystem path of the complete base bundle, for the Recovery helper (boot into recoveryOS to
    /// `csrutil disable`). `nil` when no base exists.
    public func baseBundleRootPath() async -> String? {
        await engine.baseBundleRootPath()
    }

    /// Claim the base for an exclusive Recovery boot; returns the base path. Throws if the base is
    /// busy (building/Recovery) or missing. Pair with ``endBaseRecovery()`` when the window closes.
    public func beginBaseRecovery() async throws -> String {
        try await engine.beginBaseRecovery()
    }

    /// Release the base after the Recovery window's VM has powered off.
    public func endBaseRecovery() async {
        await engine.endBaseRecovery()
    }

    /// Inject the app-layer host-side computer-use surface (VZVirtualMachineView capture + HID). Called
    /// once at startup; without it, computer use falls back to the SSH path.
    public func setSurfaceHost(_ host: any MacVMSurfaceHost) async {
        await engine.setSurfaceHost(host)
    }

    /// Kick off (or no-op if already present) the golden base image build. Long-running; progress is
    /// polled via ``baseProgress()``.
    public func buildBaseImage(localRestoreImagePath: String? = nil) async throws {
        try await engine.buildBaseImage(localRestoreImagePath: localRestoreImagePath)
    }

    /// Delete the built base image so the next ``buildBaseImage`` reinstalls macOS from scratch.
    /// `includingRestoreImages` also clears the cached `.ipsw` (forces a fresh download). Throws while
    /// the base is busy (building / Recovery).
    public func deleteBaseImage(includingRestoreImages: Bool) async throws {
        try await engine.deleteBaseImage(includingRestoreImages: includingRestoreImages)
    }

    // MARK: - Linux base image

    /// Kick off (or no-op if already built) the golden **Linux** base image build (download kernel +
    /// rootfs, assemble in-guest, provision the desktop + toolchain). Long-running; progress is polled
    /// via ``baseProgress()`` (shared with the macOS build — only one base builds at a time).
    public func buildLinuxBaseImage() async throws {
        try await engine.buildLinuxBaseImage()
    }

    /// The Linux base image's provisioning status (installed / desktop+toolchain provisioned).
    public func linuxBaseStatus() async -> MacVMBaseStatus {
        await engine.currentLinuxBaseStatus()
    }

    /// Delete the built Linux base so the next ``buildLinuxBaseImage`` rebuilds from scratch.
    public func deleteLinuxBaseImage() async throws {
        try await engine.deleteLinuxBaseImage()
    }

    /// Show or hide the diagnostic monitor window for the in-flight base build (watch a stuck build).
    public func setBaseProvisionObserver(visible: Bool) async {
        await engine.setBaseProvisionObserver(visible: visible)
    }

    /// Show or hide the **operator-assist viewer** for the named session VM: bring its live, interactive
    /// guest window on-screen so the user can operate it by hand (`mac_vm_request_operator`), or send it
    /// back off-screen. No lifecycle effect.
    public func presentOperatorAssist(name: String, visible: Bool) async {
        await engine.presentOperatorAssist(name: name, visible: visible)
    }

    /// On launch, reconcile on-disk clone bundles — keep only the active sessions' clones (daemonless,
    /// so this is on-disk GC).
    public func reconcile(activeSessions: [SessionID]) async {
        var keep = Set(activeSessions.map { Self.vmName(for: $0) })
        keep.formUnion(activeSessions.map { Self.linuxVMName(for: $0) })
        await engine.reconcileDisk(keepNames: keep)
    }

    // MARK: - Idle timer

    private func armIdleTimer(_ name: String) {
        cancelIdleTimer(name)
        let timeout = idleTimeouts[name] ?? TimeInterval(MacVMSettings.defaultIdleTimeoutSeconds)
        idleTimers[name] = Task { [weak self] in
            try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
            guard !Task.isCancelled else { return }
            await self?.stopIfIdle(name)
        }
    }

    private func cancelIdleTimer(_ name: String) {
        idleTimers[name]?.cancel()
        idleTimers[name] = nil
    }

    private func stopIfIdle(_ name: String) async {
        idleTimers[name] = nil
        guard (active[name] ?? 0) == 0 else { return }
        await engine.stop(name: name)  // keep the clone on disk — idle-restart reuses it
    }

    private func waitUntilIdle(_ name: String) async {
        let pollNanos: UInt64 = 200_000_000
        let maxPolls = 3_000  // ~10 min ceiling
        var polls = 0
        while (active[name] ?? 0) > 0 && polls < maxPolls {
            try? await Task.sleep(nanoseconds: pollNanos)
            polls += 1
        }
    }
}
