import Foundation
import os

/// Diagnostics for the guest host-path re-creation step.
private let hostPathLog = Logger(subsystem: "com.nucleic", category: "macvm.hostpaths")

// MARK: - Host-path re-creation inside the guest
//
// A session's repo lives at some absolute path on the host (e.g. `/Users/me/proj`) but virtiofs
// normally surfaces it at a different guest path. Build artifacts that capture absolute paths then
// fail in the other environment.
//
// The fix is per-OS (see ``MacVMSpec/Mount/recreateHostPath``):
//   • **Linux** mounts the multi-directory share, then bind-mounts its subdirectory at `<host>`.
//   • **macOS** gives each opted-in share a custom virtiofs tag and mounts its single-directory device
//     directly at `<host>` with `mount_virtiofs`. This avoids the filesystem-identity and nested
//     symlink problems caused by linking `<host>` to Apple's `/Volumes/My Shared Files` automount.
//
// The script is idempotent (safe on every boot / restore). A macOS failure is boot-fatal because there
// is deliberately no second `/Volumes` copy to fall back to; Linux retains its best-effort fallback.

#if arch(arm64)
extension MacVMEngine {
    /// Where opted-out **macOS** shares retain Apple's automatic mounting behavior.
    public static let macOSShareRoot = "/Volumes/My Shared Files"
    /// Where a **Linux** guest mounts the `nucleicshare` device.
    public static let linuxShareRoot = "/mnt/nucleic"

    /// The legacy/natural guest path for an automounted or multi-directory share.
    static func guestSharePath(os: GuestOS, name: String) -> String {
        switch os {
        case .macOS: return "\(macOSShareRoot)/\(name)"
        case .linux: return "\(linuxShareRoot)/\(name)"
        }
    }

    /// Custom virtiofs tags must be non-empty and shorter than 36 UTF-8 bytes. The mount index is stable
    /// for a spec and also keeps duplicate/user-supplied share names from colliding.
    static func macOSShareTag(index: Int) -> String { "nucleic-mount-\(index)" }

    /// Where a directly-mounted share lands in a macOS guest: its explicit custom mountpoint when one
    /// is set, else its original host path.
    static func macOSMountPoint(_ mount: MacVMSpec.Mount) -> String {
        mount.guestPath ?? mount.host
    }

    /// Whether this mount is presented directly (custom-tagged single-directory device) in a macOS
    /// guest rather than left to Apple's automount. True for host-path re-creation and for any share
    /// naming an explicit ``MacVMSpec/Mount/guestPath``.
    static func directMacOSMount(_ mount: MacVMSpec.Mount) -> Bool {
        let target = macOSMountPoint(mount)
        return (mount.recreateHostPath || mount.guestPath != nil)
            && !mount.host.isEmpty
            && !target.isEmpty
            && target != guestSharePath(os: .macOS, name: mount.name)
    }

    /// Build the idempotent shell snippet that re-creates each opted-in mount's original host path.
    /// Pure, so the safety and quoting rules are unit-testable.
    static func hostPathRecreationScript(os: GuestOS, mounts: [MacVMSpec.Mount]) -> String? {
        // Keep each mount's original index: it is part of the matching custom device tag on macOS.
        let targets = mounts.enumerated().filter { _, m in
            switch os {
            case .macOS: return directMacOSMount(m)
            case .linux:
                return m.recreateHostPath
                    && !m.host.isEmpty
                    && m.host != guestSharePath(os: .linux, name: m.name)
            }
        }
        guard !targets.isEmpty else { return nil }

        switch os {
        case .macOS:
            // Migrate only our legacy symlink. Never hide unrelated guest state beneath a mount.
            var blocks: [String] = []
            for (index, m) in targets {
                let target = macOSMountPoint(m)
                let host = shQuote(target)
                let legacy = shQuote(guestSharePath(os: .macOS, name: m.name))
                let tag = shQuote(macOSShareTag(index: index))
                let mountMarker = shQuote(" on \(target) (")
                let unrelated = shQuote("refusing to replace unrelated symlink at \(target)")
                let occupied = shQuote("refusing to hide existing path at \(target)")
                blocks.append(
                    "if /sbin/mount | /usr/bin/grep -Fq \(mountMarker); then :; else "
                        + "if [ -L \(host) ]; then "
                        + "[ \"$(/usr/bin/readlink \(host))\" = \(legacy) ] || "
                        + "{ echo \(unrelated) >&2; exit 1; }; "
                        + "sudo /bin/rm \(host); "
                        + "elif [ -e \(host) ]; then "
                        + "[ -d \(host) ] && [ -z \"$(/bin/ls -A \(host) 2>/dev/null)\" ] || "
                        + "{ echo \(occupied) >&2; exit 1; }; fi; "
                        + "sudo /bin/mkdir -p \(host) && sudo /sbin/mount_virtiofs \(tag) \(host); fi")
            }
            return blocks.joined(separator: "\n")

        case .linux:
            let root = shQuote(linuxShareRoot)
            var lines: [String] = [
                "mkdir -p \(root)",
                "mountpoint -q \(root) 2>/dev/null || "
                    + "mount -t virtiofs \(shQuote(nucleicShareTag)) \(root) 2>/dev/null || true",
            ]
            for (_, m) in targets {
                let host = shQuote(m.host)
                let src = shQuote(guestSharePath(os: .linux, name: m.name))
                lines.append("mkdir -p \(host)")
                lines.append(
                    "mountpoint -q \(host) 2>/dev/null || mount --bind \(src) \(host) 2>/dev/null || "
                        + "{ rmdir \(host) 2>/dev/null || true; ln -sfn \(src) \(host); }")
            }
            return lines.joined(separator: "\n")
        }
    }

    /// Ceiling on the host-path re-creation exec. The script is a handful of mounts and stats — even
    /// a slow first boot finishes it in seconds — so a run that's still going after this long is
    /// wedged, overwhelmingly on the invisible per-user network-volumes consent prompt
    /// (`kTCCServiceSystemPolicyNetworkVolumes`) an ungranted guest raises at the first share touch.
    /// This exec runs INSIDE `ensureRunning`, so leaving it unbounded wedged the whole boot — and the
    /// tool call, and the session — for as long as the dialog sat unanswered.
    static let hostPathSetupTimeoutNanos: UInt64 = 90 * 1_000_000_000

    /// Re-create host paths after the guest agent answers. A required macOS mount is part of readiness
    /// and fails the boot; Linux keeps its historical best-effort behavior and share-path fallback.
    func recreateHostPaths(_ spec: MacVMSpec) async throws {
        guard let script = Self.hostPathRecreationScript(os: spec.os, mounts: spec.mounts) else { return }
        do {
            let result = try await run(
                name: spec.name, command: script, workdir: nil,
                timeoutNanos: Self.hostPathSetupTimeoutNanos)
            guard result.exitCode != 0 else { return }
            let detail = result.stderr.trimmingCharacters(in: .whitespacesAndNewlines)
            if spec.os == .macOS {
                // -1 is the timeout sentinel (`MacVMEngine.finalize`): name the known cause so the
                // failure is actionable instead of a mystery hang report.
                let hint = result.exitCode == -1
                    ? " The usual cause is the guest's un-granted network-volume consent "
                        + "(kTCCServiceSystemPolicyNetworkVolumes) blocking the first share access — "
                        + "reprovision the base (recipe v6 pre-answers it) or click Allow once on the "
                        + "VM screen."
                    : ""
                throw MacVMError.startFailed(
                    "mounting the shared workdir at its original path exited \(result.exitCode): "
                        + detail + hint)
            }
            hostPathLog.error(
                "host-path re-creation for \(spec.name, privacy: .public) exited \(result.exitCode, privacy: .public): \(detail, privacy: .public)")
        } catch {
            if spec.os == .macOS { throw error }
            hostPathLog.error(
                "host-path re-creation for \(spec.name, privacy: .public) failed: \(String(describing: error), privacy: .public)")
        }
    }
}
#endif
