//===----------------------------------------------------------------------===//
// 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 CVersion
import ContainerVersion
import ContainerizationExtras
import Foundation

/// Top-level configuration decoded from config.toml.
///
/// Each section maps to a nested struct. Missing keys fall back to
/// hardcoded defaults via custom `init(from:)` implementations.
public final class ContainerSystemConfig: Codable, Sendable, Initable {
    public let build: BuildConfig
    public let container: ContainerConfig
    public let dns: DNSConfig
    public let kernel: KernelConfig
    public let machine: MachineConfig
    public let network: NetworkConfig
    public let registry: RegistryConfig
    public let vminit: VminitConfig

    public init(
        build: BuildConfig = .init(),
        container: ContainerConfig = .init(),
        dns: DNSConfig = .init(),
        kernel: KernelConfig = .init(),
        machine: MachineConfig = MachineConfig.default,
        network: NetworkConfig = .init(),
        registry: RegistryConfig = .init(),
        vminit: VminitConfig = .init()
    ) {
        self.build = build
        self.container = container
        self.dns = dns
        self.kernel = kernel
        self.machine = machine
        self.network = network
        self.registry = registry
        self.vminit = vminit
    }

    public init() {
        self.build = .init()
        self.container = .init()
        self.dns = .init()
        self.kernel = .init()
        self.machine = MachineConfig.default
        self.network = .init()
        self.registry = .init()
        self.vminit = .init()
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.build = try container.decodeIfPresent(BuildConfig.self, forKey: .build) ?? .init()
        self.container = try container.decodeIfPresent(ContainerConfig.self, forKey: .container) ?? .init()
        self.dns = try container.decodeIfPresent(DNSConfig.self, forKey: .dns) ?? .init()
        self.kernel = try container.decodeIfPresent(KernelConfig.self, forKey: .kernel) ?? .init()
        self.machine = try container.decodeIfPresent(MachineConfig.self, forKey: .machine) ?? MachineConfig.default
        self.network = try container.decodeIfPresent(NetworkConfig.self, forKey: .network) ?? .init()
        self.registry = try container.decodeIfPresent(RegistryConfig.self, forKey: .registry) ?? .init()
        self.vminit = try container.decodeIfPresent(VminitConfig.self, forKey: .vminit) ?? .init()
    }
}

final public class BuildConfig: Codable, Sendable {
    public static let defaultRosetta = true
    public static let defaultCPUs = 2
    public static let defaultMemory = try! MemorySize("2048MB")
    public static var defaultImage: String {
        let tag = String(cString: get_container_builder_shim_version())
        return "ghcr.io/apple/container-builder-shim/builder:\(tag)"
    }

    public let rosetta: Bool
    public let cpus: Int
    public let memory: MemorySize
    public let image: String

    public init(
        rosetta: Bool = defaultRosetta,
        cpus: Int = defaultCPUs,
        memory: MemorySize = defaultMemory,
        image: String = defaultImage
    ) {
        self.rosetta = rosetta
        self.cpus = cpus
        self.memory = memory
        self.image = image
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.rosetta = try container.decodeIfPresent(Bool.self, forKey: .rosetta) ?? Self.defaultRosetta
        self.cpus = try container.decodeIfPresent(Int.self, forKey: .cpus) ?? Self.defaultCPUs
        self.memory = try container.decodeIfPresent(MemorySize.self, forKey: .memory) ?? Self.defaultMemory
        self.image = try container.decodeIfPresent(String.self, forKey: .image) ?? Self.defaultImage
    }
}

final public class ContainerConfig: Codable, Sendable {
    public static let defaultCPUs = 4
    public static let defaultMemory = try! MemorySize("1g")

    public let cpus: Int
    public let memory: MemorySize

    public init(cpus: Int = defaultCPUs, memory: MemorySize = defaultMemory) {
        self.cpus = cpus
        self.memory = memory
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.cpus = try container.decodeIfPresent(Int.self, forKey: .cpus) ?? Self.defaultCPUs
        self.memory = try container.decodeIfPresent(MemorySize.self, forKey: .memory) ?? Self.defaultMemory
    }
}

final public class DNSConfig: Codable, Sendable {
    public let domain: String?

    public init(domain: String? = nil) {
        self.domain = domain
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.domain = try container.decodeIfPresent(String.self, forKey: .domain)
    }
}

final public class VminitConfig: Codable, Sendable {
    public static var defaultImage: String {
        let tag = String(cString: get_swift_containerization_version())
        return tag == "latest"
            ? "vminit:latest"
            : "ghcr.io/apple/containerization/vminit:\(tag)"
    }

    public let image: String

    public init(image: String = defaultImage) {
        self.image = image
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.image = try container.decodeIfPresent(String.self, forKey: .image) ?? Self.defaultImage
    }
}

final public class KernelConfig: Codable, Sendable {
    public static let defaultBinaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.15-186"
    public static let defaultURL: URL =
        URL(string: "https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst")!
    public static let defaultDigest = "sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"

    private enum CodingKeys: String, CodingKey {
        case binaryPath
        case url
        case digest
    }

    public let binaryPath: String
    public let url: URL
    public let digest: String

    public init(binaryPath: String = defaultBinaryPath, url: URL = defaultURL, digest: String = defaultDigest) {
        self.binaryPath = binaryPath
        self.url = url
        self.digest = digest
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.binaryPath =
            try container.decodeIfPresent(String.self, forKey: .binaryPath)
            ?? Self.defaultBinaryPath
        if let urlString = try container.decodeIfPresent(String.self, forKey: .url) {
            guard let parsed = URL(string: urlString) else {
                throw DecodingError.dataCorruptedError(
                    forKey: .url,
                    in: container,
                    debugDescription: "invalid kernel URL '\(urlString)'")
            }
            self.url = parsed
        } else {
            self.url = Self.defaultURL
        }
        if let digest = try container.decodeIfPresent(String.self, forKey: .digest) {
            self.digest = digest
        } else if self.url.absoluteString == Self.defaultURL.absoluteString {
            self.digest = Self.defaultDigest
        } else {
            throw DecodingError.dataCorruptedError(
                forKey: .digest,
                in: container,
                debugDescription: "kernel.digest is required when kernel.url is not the default URL")
        }
    }

    // JSONEncoder special-cases URL to encode as absoluteString, but third-party
    // encoders (e.g. TOMLEncoder) hit Foundation's default Codable conformance which
    // encodes into a keyed container with a "relative" key. Encode as a plain string
    // so all formats produce a consistent URL representation.
    // If more config types start using URL, consider a property wrapper or a wrapper
    // type (like MemorySize) that encodes/decodes URL as a string uniformly.
    public func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(binaryPath, forKey: .binaryPath)
        try container.encode(url.absoluteString, forKey: .url)
        try container.encode(digest, forKey: .digest)
    }
}

final public class NetworkConfig: Codable, Sendable {
    public let subnet: CIDRv4?
    public let subnetv6: CIDRv6?

    public init(subnet: CIDRv4? = nil, subnetv6: CIDRv6? = nil) {
        self.subnet = subnet
        self.subnetv6 = subnetv6
    }
}

final public class RegistryConfig: Codable, Sendable {
    public static let defaultDomain = "docker.io"

    public let domain: String

    public init(domain: String = defaultDomain) {
        self.domain = domain
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.domain = try container.decodeIfPresent(String.self, forKey: .domain) ?? Self.defaultDomain
    }
}
