import Foundation
#if arch(arm64)
import Virtualization
#endif

#if arch(arm64)
extension MacVMEngine {
    // MARK: - Linux VM configuration builder

    /// Assemble the `VZVirtualMachineConfiguration` for a **Linux** guest — the counterpart to
    /// ``makeMacConfiguration``. Where the Mac path reconstructs a `VZMacPlatformConfiguration` and boots
    /// `VZMacOSBootLoader`, Linux uses the generic platform + `VZLinuxBootLoader` over an external
    /// (uncompressed) kernel + initrd, and swaps the `VZMac*` graphics/keyboard/trackpad for the more
    /// flexible virtio/USB devices Linux supports:
    ///
    ///   • **output** — `VZVirtioGraphicsDeviceConfiguration` (a real virtio-gpu scanout) instead of the
    ///     fixed Mac display; the host-side `VZVirtualMachineView` captures it reliably (no macOS
    ///     "blank framebuffer" quirk).
    ///   • **input** — `VZUSBKeyboardConfiguration` + `VZUSBScreenCoordinatePointingDeviceConfiguration`
    ///     (**absolute** pointing), so injected clicks land 1:1 with screenshot pixels — no relative
    ///     trackpad drift, no TCC/SIP anywhere.
    ///   • **console** — a `VZVirtioConsoleDeviceSerialPortConfiguration` wired to a per-clone
    ///     `console.log`, so a wedged boot is diagnosable host-side (`console=hvc0`).
    ///
    /// vsock (the agent control plane), NAT networking, virtiofs sharing, entropy and the memory balloon
    /// are byte-for-byte the same devices the Mac config uses. `nonisolated static` for the same reason
    /// ``makeMacConfiguration`` is: it reads only the (Sendable) bundle files and builds VZ objects.
    public nonisolated static func makeLinuxConfiguration(
        bundle: MacVMBundle, cpus: Int, memoryGiB: Int, mac: String, mounts: [MacVMSpec.Mount]
    ) throws -> VZVirtualMachineConfiguration {
        let config = VZVirtualMachineConfiguration()
        config.platform = VZGenericPlatformConfiguration()

        // Boot an external, uncompressed arm64 kernel + initrd (there is no in-disk bootloader). The
        // command line is persisted in the bundle so a clone boots exactly as the base was provisioned.
        guard FileManager.default.fileExists(atPath: bundle.kernelURL.path) else {
            throw MacVMError.kernelMissing("the Linux base bundle has no kernel at \(bundle.kernelURL.path)")
        }
        let bootLoader = VZLinuxBootLoader(kernelURL: bundle.kernelURL)
        if FileManager.default.fileExists(atPath: bundle.initrdURL.path) {
            bootLoader.initialRamdiskURL = bundle.initrdURL
        }
        bootLoader.commandLine = linuxCommandLine(bundle: bundle)
        config.bootLoader = bootLoader

        config.cpuCount = Self.clampCPU(cpus)
        config.memorySize = Self.clampMemory(UInt64(memoryGiB) * 1024 * 1024 * 1024)

        // Root filesystem (this bundle's writable ext4 Disk.img) → /dev/vda.
        let attachment = try VZDiskImageStorageDeviceAttachment(url: bundle.diskImageURL, readOnly: false)
        config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]

        // NAT networking — the guest's own outbound internet (apt during provisioning; npm/pip/etc.
        // afterward). NOT the control plane (that is vsock). Pinned MAC → best-effort IP display only.
        let net = VZVirtioNetworkDeviceConfiguration()
        net.macAddress = VZMACAddress(string: mac) ?? VZMACAddress.randomLocallyAdministered()
        net.attachment = VZNATNetworkDeviceAttachment()
        config.networkDevices = [net]

        // vsock — the sole host↔guest control channel (agent ping + streaming exec on port 2035).
        config.socketDevices = [VZVirtioSocketDeviceConfiguration()]

        // virtio-gpu scanout at the shared 1920×1200 computer-use framebuffer size (1:1 with the JPEG
        // screenshots the surface returns), matching `MacVMComputerSurface.fbWidth/Height`.
        let gpu = VZVirtioGraphicsDeviceConfiguration()
        gpu.scanouts = [
            VZVirtioGraphicsScanoutConfiguration(widthInPixels: 1920, heightInPixels: 1200)
        ]
        config.graphicsDevices = [gpu]

        // USB HID: a keyboard + an ABSOLUTE screen-coordinate pointer. Absolute pointing is the Linux
        // advantage the surface leans on — injected coordinates are the guest's pixels, no delta math.
        config.keyboards = [VZUSBKeyboardConfiguration()]
        config.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]

        // Serial console → a per-clone log file, so a boot that never reaches the agent is diagnosable.
        let console = VZVirtioConsoleDeviceSerialPortConfiguration()
        let logURL = bundle.root.appendingPathComponent("console.log")
        console.attachment = try VZFileSerialPortAttachment(url: logURL, append: false)
        config.serialPorts = [console]

        config.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
        config.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()]

        // Shared host directories → virtiofs (one device, tag `nucleicShareTag`). Unlike macOS there is
        // no automount: the guest agent/fstab mounts the tag and exposes each share as a subdirectory
        // under the guest mount root (see docs/LINUX_VM.md).
        if !mounts.isEmpty {
            var shares: [String: VZSharedDirectory] = [:]
            for m in mounts {
                shares[m.name] = VZSharedDirectory(
                    url: URL(fileURLWithPath: m.host), readOnly: m.readOnly)
            }
            let fs = VZVirtioFileSystemDeviceConfiguration(tag: Self.nucleicShareTag)
            fs.share = VZMultipleDirectoryShare(directories: shares)
            config.directorySharingDevices = [fs]
        }

        try config.validate()
        return config
    }

    /// The virtiofs tag the Linux guest mounts the session's shared directories under. A fixed,
    /// tag-valid token both sides agree on (the guest fstab/agent uses the same string).
    public static let nucleicShareTag = "nucleicshare"

    /// The virtiofs tag the Linux **base build** stages its provisioning payload under (tarball, agent
    /// binary, scripts) — distinct from the session share so the two never collide.
    public static let nucleicProvisionTag = "nucleicprov"

    /// The kernel command line for a Linux clone: the bundle's persisted `cmdline` if present (so a
    /// clone boots exactly as the base was provisioned), else a sane default that puts the console on
    /// the virtio console and roots on the single virtio-block disk.
    nonisolated static func linuxCommandLine(bundle: MacVMBundle) -> String {
        if let persisted = try? String(contentsOf: bundle.cmdlineURL, encoding: .utf8) {
            let trimmed = persisted.trimmingCharacters(in: .whitespacesAndNewlines)
            if !trimmed.isEmpty { return trimmed }
        }
        return Self.defaultLinuxCommandLine
    }

    /// Default Linux kernel command line: virtio console, root on `/dev/vda`, quiet boot. Pure so it's
    /// unit-testable and shared by the base build (which persists it into the bundle).
    public static let defaultLinuxCommandLine =
        "console=hvc0 root=/dev/vda rw quiet loglevel=3 systemd.show_status=0"

    /// The command line for the base build's **provision boot**. Default: the quiet line above. With
    /// the base-build debug switch on (``MacVMSettings/linuxBaseBuildDebug``): a verbose one — full
    /// kernel log level, systemd unit status, and the journal forwarded to the serial console — so the
    /// bundle's `provision.log` records the whole boot instead of six quiet lines. Pure and
    /// unit-testable; only the base build uses it (clones keep the persisted quiet cmdline).
    public static func linuxProvisionCommandLine(debug: Bool) -> String {
        guard debug else { return defaultLinuxCommandLine }
        return "console=hvc0 root=/dev/vda rw loglevel=7 systemd.show_status=1 "
            + "systemd.journald.forward_to_console=1"
    }
}
#endif
