//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 ContainerPersistence
import ContainerResource
import Containerization
import ContainerizationEXT4
import ContainerizationError
import ContainerizationExtras
import ContainerizationOS
import Foundation
import Logging
import Synchronization
import SystemPackage

public actor VolumesService {
    private let resourceRoot: FilePath
    private let store: ContainerPersistence.FilesystemEntityStore<VolumeConfiguration>
    private let log: Logger
    private let lock = AsyncLock()
    private let containersService: ContainersService

    // Storage constants
    private static let entityFile = "entity.json"
    private static let blockFile = "volume.img"

    public init(resourceRoot: FilePath, containersService: ContainersService, log: Logger) async throws {
        try FileManager.default.createDirectory(atPath: resourceRoot.string, withIntermediateDirectories: true)
        self.resourceRoot = resourceRoot
        self.store = try FilesystemEntityStore<VolumeConfiguration>(path: resourceRoot, type: "volumes", log: log)
        self.containersService = containersService
        self.log = log

        // Migrate configs stored with the old `createdAt` key to `creationDate`.
        // Deprecated: As of 1.0.0. Use ``creationDate`` instead of ``createdAt``.
        // Note: Will be removed in a later release.
        let configurations = try await store.list()
        for configuration in configurations {
            do {
                try await store.update(configuration)
            } catch {
                log.error(
                    "failed to migrate volume configuration",
                    metadata: [
                        "name": "\(configuration.name)",
                        "error": "\(error)",
                    ])
            }
        }
    }

    public func create(
        name: String,
        driver: String = "local",
        driverOpts: [String: String] = [:],
        labels: [String: String] = [:]
    ) async throws -> VolumeConfiguration {
        log.debug(
            "VolumesService: enter",
            metadata: [
                "func": "\(#function)",
                "name": "\(name)",
            ]
        )
        defer {
            log.debug(
                "VolumesService: exit",
                metadata: [
                    "func": "\(#function)",
                    "name": "\(name)",
                ]
            )
        }

        return try await lock.withLock { _ in
            try await self._create(name: name, driver: driver, driverOpts: driverOpts, labels: labels)
        }
    }

    public func delete(name: String) async throws {
        log.debug(
            "VolumesService: enter",
            metadata: [
                "func": "\(#function)",
                "name": "\(name)",
            ]
        )
        defer {
            log.debug(
                "VolumesService: exit",
                metadata: [
                    "func": "\(#function)",
                    "name": "\(name)",
                ]
            )
        }

        try await lock.withLock { _ in
            try await self._delete(name: name)
        }
    }

    public func list() async throws -> [VolumeConfiguration] {
        log.debug(
            "VolumesService: enter",
            metadata: [
                "func": "\(#function)"
            ]
        )
        defer {
            log.debug(
                "VolumesService: exit",
                metadata: [
                    "func": "\(#function)"
                ]
            )
        }

        return try await store.list()
    }

    public func inspect(_ name: String) async throws -> VolumeConfiguration {
        log.debug(
            "VolumesService: enter",
            metadata: [
                "func": "\(#function)",
                "name": "\(name)",
            ]
        )
        defer {
            log.debug(
                "VolumesService: exit",
                metadata: [
                    "func": "\(#function)",
                    "name": "\(name)",
                ]
            )
        }

        return try await lock.withLock { _ in
            try await self._inspect(name)
        }
    }

    /// Calculate disk usage for a single volume
    public func volumeDiskUsage(name: String) async throws -> UInt64 {
        log.debug(
            "VolumesService: enter",
            metadata: [
                "func": "\(#function)",
                "name": "\(name)",
            ]
        )
        defer {
            log.debug(
                "VolumesService: exit",
                metadata: [
                    "func": "\(#function)",
                    "name": "\(name)",
                ]
            )
        }

        let volumePath = self.volumePath(for: name)
        return FileManager.default.allocatedSize(of: URL(fileURLWithPath: volumePath))
    }

    /// Calculate disk usage for volumes
    /// - Returns: Tuple of (total count, active count, total size, reclaimable size)
    public func calculateDiskUsage() async throws -> (Int, Int, UInt64, UInt64) {
        log.debug(
            "VolumesService: enter",
            metadata: [
                "func": "\(#function)"
            ]
        )
        defer {
            log.debug(
                "VolumesService: exit",
                metadata: [
                    "func": "\(#function)"
                ]
            )
        }

        return try await lock.withLock { _ in
            let allVolumes = try await self.store.list()

            // Atomically get active volumes with container list
            return try await self.containersService.withContainerList(logMetadata: ["acquirer": "\(#function)"]) { containers in
                var inUseSet = Set<String>()

                // Find all mounted volumes
                for container in containers {
                    for mount in container.configuration.mounts {
                        if mount.isVolume, let volumeName = mount.volumeName {
                            inUseSet.insert(volumeName)
                        }
                    }
                }

                var totalSize: UInt64 = 0
                var reclaimableSize: UInt64 = 0

                // Calculate sizes
                for volume in allVolumes {
                    let volumePath = self.volumePath(for: volume.name)
                    let volumeSize = FileManager.default.allocatedSize(of: URL(fileURLWithPath: volumePath))
                    totalSize += volumeSize

                    if !inUseSet.contains(volume.name) {
                        reclaimableSize += volumeSize
                    }
                }

                return (allVolumes.count, inUseSet.count, totalSize, reclaimableSize)
            }
        }
    }

    private func parseSize(_ sizeString: String) throws -> UInt64 {
        let measurement = try Measurement.parse(parsing: sizeString)
        let bytes = measurement.converted(to: .bytes).value

        // Validate minimum size
        let minSize: UInt64 = 1.mib()  // 1mib minimum

        let sizeInBytes = UInt64(bytes)

        guard sizeInBytes >= minSize else {
            throw VolumeError.storageError("volume size too small: minimum 1MiB")
        }

        return sizeInBytes
    }

    // FIXME: These don't guarantee that name doesn't have component separators.
    private nonisolated func volumePath(for name: String) -> String {
        resourceRoot.appending(name).string
    }

    private nonisolated func entityPath(for name: String) -> String {
        "\(volumePath(for: name))/\(Self.entityFile)"
    }

    private nonisolated func blockPath(for name: String) -> String {
        "\(volumePath(for: name))/\(Self.blockFile)"
    }

    private func createVolumeDirectory(for name: String) throws {
        let volumePath = volumePath(for: name)
        let fm = FileManager.default
        try fm.createDirectory(atPath: volumePath, withIntermediateDirectories: true, attributes: nil)
    }

    static func parseJournalConfig(_ value: String) throws -> EXT4.JournalConfig {
        let parts = value.split(separator: ":", maxSplits: 1)
        guard let modeSubstring = parts.first else {
            throw VolumeError.storageError("invalid journal configuration: expected 'mode' or 'mode:size'")
        }
        let modeString = String(modeSubstring)
        let mode: EXT4.JournalConfig.JournalMode
        switch modeString {
        case "writeback": mode = .writeback
        case "ordered": mode = .ordered
        case "journal": mode = .journal
        default:
            throw VolumeError.storageError("invalid journal mode '\(modeString)': must be writeback, ordered, or journal")
        }
        let size: UInt64? =
            try parts.count > 1
            ? UInt64(Measurement.parse(parsing: String(parts[1])).converted(to: .bytes).value)
            : nil
        return EXT4.JournalConfig(size: size, defaultMode: mode)
    }

    private func createVolumeImage(for name: String, sizeInBytes: UInt64 = VolumeStorage.defaultVolumeSizeBytes, journal: EXT4.JournalConfig? = nil) throws {
        let blockPath = blockPath(for: name)

        // Use the containerization library's EXT4 formatter
        let formatter = try EXT4.Formatter(
            FilePath(blockPath),
            blockSize: 4096,
            minDiskSize: sizeInBytes,
            journal: journal
        )

        try formatter.close()
    }

    private nonisolated func removeVolumeDirectory(for name: String) throws {
        let volumePath = volumePath(for: name)
        let fm = FileManager.default

        if fm.fileExists(atPath: volumePath) {
            try fm.removeItem(atPath: volumePath)
        }
    }

    private func _create(
        name: String,
        driver: String,
        driverOpts: [String: String],
        labels: [String: String]
    ) async throws -> VolumeConfiguration {
        guard VolumeStorage.isValidVolumeName(name) else {
            throw VolumeError.invalidVolumeName("invalid volume name '\(name)': must match \(VolumeStorage.volumeNamePattern)")
        }

        // Check if volume already exists by trying to list and finding it
        let existingVolumes = try await store.list()
        if existingVolumes.contains(where: { $0.name == name }) {
            throw VolumeError.volumeAlreadyExists(name)
        }

        try createVolumeDirectory(for: name)

        // Parse size from driver options (default 512GB)
        let sizeInBytes: UInt64
        if let sizeString = driverOpts["size"] {
            sizeInBytes = try parseSize(sizeString)
        } else {
            sizeInBytes = VolumeStorage.defaultVolumeSizeBytes
        }

        let journalConfig = try driverOpts["journal"].map { try Self.parseJournalConfig($0) }

        try createVolumeImage(for: name, sizeInBytes: sizeInBytes, journal: journalConfig)

        let volume = VolumeConfiguration(
            name: name,
            driver: driver,
            format: "ext4",
            source: blockPath(for: name),
            labels: labels,
            options: driverOpts,
            sizeInBytes: sizeInBytes
        )

        try await store.create(volume)

        log.info(
            "created volume",
            metadata: [
                "name": "\(name)",
                "driver": "\(driver)",
                "isAnonymous": "\(volume.isAnonymous)",
            ])
        return volume
    }

    private func _delete(name: String) async throws {
        guard VolumeStorage.isValidVolumeName(name) else {
            throw VolumeError.invalidVolumeName("invalid volume name '\(name)': must match \(VolumeStorage.volumeNamePattern)")
        }

        // Check if volume exists by trying to list and finding it
        let existingVolumes = try await store.list()
        guard existingVolumes.contains(where: { $0.name == name }) else {
            throw VolumeError.volumeNotFound(name)
        }

        // Check if volume is in use by any container atomically
        try await containersService.withContainerList(logMetadata: ["acquirer": "\(#function)", "name": "\(name)"]) { containers in
            for container in containers {
                for mount in container.configuration.mounts {
                    if mount.isVolume && mount.volumeName == name {
                        throw VolumeError.volumeInUse(name)
                    }
                }
            }

            try await self.store.delete(name)
            try self.removeVolumeDirectory(for: name)
        }

        log.info("deleted volume", metadata: ["name": "\(name)"])
    }

    private func _inspect(_ name: String) async throws -> VolumeConfiguration {
        guard VolumeStorage.isValidVolumeName(name) else {
            throw VolumeError.invalidVolumeName("invalid volume name '\(name)': must match \(VolumeStorage.volumeNamePattern)")
        }

        let volumes = try await store.list()
        guard let volume = volumes.first(where: { $0.name == name }) else {
            throw VolumeError.volumeNotFound(name)
        }

        return volume
    }

}
