//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerizationError
import Foundation

#if os(macOS)
import Virtualization
#endif

/// A filesystem mount exposed to a container.
public struct Mount: Sendable {
    /// The filesystem or mount type. This is the string
    /// that will be used for the mount syscall itself.
    public var type: String
    /// The source path of the mount.
    public var source: String
    /// The destination path of the mount.
    public var destination: String
    /// Filesystem or mount specific options.
    public var options: [String]
    /// Runtime specific options. This can be used
    /// as a way to discern what kind of device a vmm
    /// should create for this specific mount (virtioblock
    /// virtiofs etc.).
    public let runtimeOptions: RuntimeOptions

    /// A type representing a "hint" of what type
    /// of mount this really is (block, directory, purely
    /// guest mount) and a set of type specific options, if any.
    public enum RuntimeOptions: Sendable {
        case virtioblk([String])
        case virtiofs([String])
        case shared
        case any([String])
    }

    public init(
        type: String,
        source: String,
        destination: String,
        options: [String],
        runtimeOptions: RuntimeOptions
    ) {
        self.type = type
        self.source = source
        self.destination = destination
        self.options = options
        self.runtimeOptions = runtimeOptions
    }

    /// Mount representing a virtio block device.
    public static func block(
        format: String,
        source: String,
        destination: String,
        options: [String] = [],
        runtimeOptions: [String] = []
    ) -> Self {
        .init(
            type: format,
            source: source,
            destination: destination,
            options: options,
            runtimeOptions: .virtioblk(runtimeOptions)
        )
    }

    /// Mount representing a virtiofs share.
    public static func share(
        source: String,
        destination: String,
        options: [String] = [],
        runtimeOptions: [String] = []
    ) -> Self {
        .init(
            type: "virtiofs",
            source: source,
            destination: destination,
            options: options,
            runtimeOptions: .virtiofs(runtimeOptions)
        )
    }

    /// A generic mount.
    public static func any(
        type: String,
        source: String,
        destination: String,
        options: [String] = [],
        runtimeOptions: [String] = []
    ) -> Self {
        .init(
            type: type,
            source: source,
            destination: destination,
            options: options,
            runtimeOptions: .any(runtimeOptions)
        )
    }

    /// A mount referencing a shared pod volume by name.
    public static func sharedMount(
        name: String,
        destination: String,
        options: [String] = []
    ) -> Self {
        .init(
            type: "none",
            source: name,
            destination: destination,
            options: options,
            runtimeOptions: .shared
        )
    }

    #if os(macOS)
    /// Clone the Mount to the provided path.
    ///
    /// This uses `clonefile` to provide a copy-on-write copy of the Mount.
    public func clone(to: String) throws -> Self {
        let fm = FileManager.default
        let src = self.source
        try fm.copyItem(atPath: src, toPath: to)

        return .init(
            type: self.type,
            source: to,
            destination: self.destination,
            options: self.options,
            runtimeOptions: self.runtimeOptions
        )
    }
    #endif
}

#if os(macOS)

extension Mount {
    private enum StorageAttachmentType {
        case diskImage
        case networkBlockDevice
    }

    private var storageAttachmentType: StorageAttachmentType {
        let nbdSchemes = ["nbd://", "nbds://", "nbd+unix://", "nbds+unix://"]
        if nbdSchemes.contains(where: { self.source.hasPrefix($0) }) {
            return .networkBlockDevice
        }
        return .diskImage
    }

    func configure(config: inout VZVirtualMachineConfiguration) throws {
        switch self.runtimeOptions {
        case .virtioblk(let options):
            let device: VZStorageDeviceAttachment
            switch self.storageAttachmentType {
            case .networkBlockDevice:
                device = try VZNetworkBlockDeviceStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options)
            case .diskImage:
                device = try VZDiskImageStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options)
            }
            let attachment = VZVirtioBlockDeviceConfiguration(attachment: device)
            config.storageDevices.append(attachment)
        case .virtiofs(_):
            // VirtioFS mounts are handled centrally via VZMultipleDirectoryShare in VZVirtualMachineInstance
            // No per-mount device configuration needed
            break
        case .shared, .any:
            break
        }
    }
}

extension VZDiskImageStorageDeviceAttachment {
    static func mountToVZAttachment(mount: Mount, options: [String]) throws -> VZDiskImageStorageDeviceAttachment {
        var synchronizationMode: VZDiskImageSynchronizationMode = .fsync
        var cachingMode: VZDiskImageCachingMode = .cached

        for option in options {
            let split = option.split(separator: "=")
            if split.count != 2 {
                continue
            }

            let key = String(split[0])
            let value = String(split[1])

            switch key {
            case "vzDiskImageCachingMode":
                switch value {
                case "automatic":
                    cachingMode = .automatic
                case "cached":
                    cachingMode = .cached
                case "uncached":
                    cachingMode = .uncached
                default:
                    throw ContainerizationError(
                        .invalidArgument,
                        message: "unknown vzDiskImageCachingMode value for virtio block device: \(value)"
                    )
                }
            case "vzDiskImageSynchronizationMode":
                switch value {
                case "full":
                    synchronizationMode = .full
                case "fsync":
                    synchronizationMode = .fsync
                case "none":
                    synchronizationMode = .none
                default:
                    throw ContainerizationError(
                        .invalidArgument,
                        message: "unknown vzDiskImageSynchronizationMode value for virtio block device: \(value)"
                    )
                }
            default:
                throw ContainerizationError(
                    .invalidArgument,
                    message: "unknown vmm option encountered: \(key)"
                )
            }
        }
        return try VZDiskImageStorageDeviceAttachment(
            url: URL(filePath: mount.source),
            readOnly: mount.readonly,
            cachingMode: cachingMode,
            synchronizationMode: synchronizationMode
        )
    }
}

extension VZNetworkBlockDeviceStorageDeviceAttachment {
    static func mountToVZAttachment(mount: Mount, options: [String]) throws -> VZNetworkBlockDeviceStorageDeviceAttachment {
        guard let url = URL(string: mount.source) else {
            throw ContainerizationError(
                .invalidArgument,
                message: "invalid NBD URL: \(mount.source)"
            )
        }

        var timeout: TimeInterval = 5
        var synchronizationMode: VZDiskSynchronizationMode = .full

        for option in options {
            let split = option.split(separator: "=")
            if split.count != 2 {
                continue
            }

            let key = String(split[0])
            let value = String(split[1])

            switch key {
            case "vzTimeout":
                guard let t = TimeInterval(value) else {
                    throw ContainerizationError(
                        .invalidArgument,
                        message: "invalid vzTimeout value for NBD device: \(value)"
                    )
                }
                timeout = t
            case "vzSynchronizationMode":
                switch value {
                case "full":
                    synchronizationMode = .full
                case "none":
                    synchronizationMode = .none
                default:
                    throw ContainerizationError(
                        .invalidArgument,
                        message: "unknown vzSynchronizationMode value for NBD device: \(value)"
                    )
                }
            default:
                throw ContainerizationError(
                    .invalidArgument,
                    message: "unknown vmm option encountered: \(key)"
                )
            }
        }

        return try VZNetworkBlockDeviceStorageDeviceAttachment(
            url: url,
            timeout: timeout,
            isForcedReadOnly: mount.readonly,
            synchronizationMode: synchronizationMode
        )
    }
}

#endif

extension Mount {
    fileprivate var readonly: Bool {
        self.options.contains("ro")
    }

    /// Returns true if this mount is a virtio block device.
    public var isBlock: Bool {
        if case .virtioblk = self.runtimeOptions {
            return true
        }
        return false
    }
}
