//===----------------------------------------------------------------------===//
// 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 ArgumentParser
import Containerization
import ContainerizationArchive
import ContainerizationError
import ContainerizationExtras
import ContainerizationOCI
import Foundation

extension Application {
    struct Images: AsyncParsableCommand {
        static let configuration = CommandConfiguration(
            commandName: "images",
            abstract: "Manage images",
            subcommands: [
                Get.self,
                Delete.self,
                Pull.self,
                Tag.self,
                Push.self,
                Save.self,
                Load.self,
            ]
        )

        func run() async throws {
            let store = Application.imageStore
            let images = try await store.list()

            print("REFERENCE\tMEDIA TYPE\tDIGEST")
            for image in images {
                print("\(image.reference)\t\(image.mediaType)\t\(image.digest)")
            }
        }

        struct Delete: AsyncParsableCommand {
            @Argument var reference: String

            func run() async throws {
                let store = Application.imageStore
                try await store.delete(reference: reference)
            }
        }

        struct Tag: AsyncParsableCommand {
            @Argument var old: String
            @Argument var new: String

            func run() async throws {
                let store = Application.imageStore
                _ = try await store.tag(existing: old, new: new)
            }
        }

        struct Get: AsyncParsableCommand {
            @Argument var reference: String

            func run() async throws {
                let store = Application.imageStore
                let image = try await store.get(reference: reference)

                let index = try await image.index()

                let enc = JSONEncoder()
                enc.outputFormatting = .prettyPrinted
                let data = try enc.encode(ImageDisplay(reference: image.reference, index: index))
                print(String(data: data, encoding: .utf8)!)
            }
        }

        struct ImageDisplay: Codable {
            let reference: String
            let index: Index
        }

        struct Pull: AsyncParsableCommand {
            static let configuration = CommandConfiguration(
                commandName: "pull",
                abstract: "Pull an image's contents into a content store"
            )

            @Argument var ref: String

            @Option(name: .customLong("platform"), help: "Platform string in the form 'os/arch/variant'. Example 'linux/arm64/v8', 'linux/amd64'") var platformString: String?

            @Option(
                name: .customLong("unpack-path"), help: "Path to a new directory to unpack the image into",
                transform: { str in
                    URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
                })
            var unpackPath: String?

            @Flag(help: "Pull anonymously via plain-text HTTP.")
            var http: Bool = false

            func run() async throws {
                let imageStore = Application.imageStore
                let platform: Platform? = try {
                    if let platformString {
                        return try Platform(from: platformString)
                    }
                    return nil
                }()

                let reference = try Reference.parse(ref)
                reference.normalize()
                let normalizedReference = reference.description
                if normalizedReference != ref {
                    print("Reference resolved to \(reference.description)")
                }

                var startTime = ContinuousClock.now
                let image = try await Images.withAuthentication(ref: normalizedReference, insecure: http) { auth in
                    try await imageStore.pull(reference: normalizedReference, platform: platform, insecure: http, auth: auth)
                }

                guard let image else {
                    print("image pull failed")
                    Application.exit(withError: POSIXError(.EACCES))
                }

                var duration = ContinuousClock.now - startTime
                print("Image pull took: \(duration)\n")

                guard let unpackPath else {
                    return
                }
                guard !FileManager.default.fileExists(atPath: unpackPath) else {
                    throw ContainerizationError(.exists, message: "directory already exists at \(unpackPath)")
                }
                let unpackUrl = URL(filePath: unpackPath)
                try FileManager.default.createDirectory(at: unpackUrl, withIntermediateDirectories: true)

                let unpacker = EXT4Unpacker.init(blockSizeInBytes: 2.gib())

                startTime = ContinuousClock.now
                if let platform {
                    let name = platform.description.replacingOccurrences(of: "/", with: "-")
                    let _ = try await unpacker.unpack(image, for: platform, at: unpackUrl.appending(component: name))
                } else {
                    for descriptor in try await image.index().manifests {
                        if let referenceType = descriptor.annotations?["vnd.docker.reference.type"], referenceType == "attestation-manifest" {
                            continue
                        }
                        guard let descPlatform = descriptor.platform else {
                            continue
                        }
                        let name = descPlatform.description.replacingOccurrences(of: "/", with: "-")
                        let _ = try await unpacker.unpack(image, for: descPlatform, at: unpackUrl.appending(component: name))
                        print("created snapshot for platform \(descPlatform.description)")
                    }
                }
                duration = ContinuousClock.now - startTime
                print("\nUnpacking took: \(duration)")
            }
        }

        struct Push: AsyncParsableCommand {
            static let configuration = CommandConfiguration(
                commandName: "push",
                abstract: "Push an image to a remote registry"
            )

            @Option(help: "Platform string in the form 'os/arch/variant'. Example 'linux/arm64/v8', 'linux/amd64'") var platformString: String?

            @Flag(help: "Push anonymously via plain-text HTTP.")
            var http: Bool = false

            @Argument var ref: String

            func run() async throws {
                let imageStore = Application.imageStore
                let platform: Platform? = try {
                    if let platformString {
                        return try Platform(from: platformString)
                    }
                    return nil
                }()

                let reference = try Reference.parse(ref)
                reference.normalize()
                let normalizedReference = reference.description
                if normalizedReference != ref {
                    print("Reference resolved to \(reference.description)")
                }

                try await Images.withAuthentication(ref: normalizedReference, insecure: http) { auth in
                    try await imageStore.push(reference: normalizedReference, platform: platform, insecure: http, auth: auth)
                }
                print("image pushed")
            }
        }

        struct Save: AsyncParsableCommand {
            static let configuration = CommandConfiguration(
                commandName: "save",
                abstract: "Save one or more images to a tar archive"
            )

            @Option(help: "Platform string in the form 'os/arch/variant'. Example 'linux/arm64/v8', 'linux/amd64'") var platform: String?

            @Option(name: .shortAndLong, help: "Path to tar archive")
            var output: String

            @Argument var reference: [String]

            func run() async throws {
                var p: Platform? = nil
                if let platform {
                    p = try Platform(from: platform)
                }
                let store = Application.imageStore
                let tempDir = FileManager.default.uniqueTemporaryDirectory()
                defer {
                    try? FileManager.default.removeItem(at: tempDir)
                }
                try await store.save(references: reference, out: tempDir, platform: p)
                let writer = try ArchiveWriter(format: .pax, filter: .none, file: URL(filePath: output))
                try writer.archiveDirectory(tempDir)
                try writer.finishEncoding()
                print("image exported")
            }
        }

        struct Load: AsyncParsableCommand {
            static let configuration = CommandConfiguration(
                commandName: "load",
                abstract: "Load one or more images from a tar archive"
            )

            @Option(name: .shortAndLong, help: "Path to tar archive")
            var input: String

            func run() async throws {
                let store = Application.imageStore
                let tarFile = URL(fileURLWithPath: input)
                let reader = try ArchiveReader(file: tarFile.absoluteURL)
                let tempDir = FileManager.default.uniqueTemporaryDirectory()
                defer {
                    try? FileManager.default.removeItem(at: tempDir)
                }
                let rejectedPaths = try reader.extractContents(to: tempDir)
                let imported = try await store.load(from: tempDir)
                for image in imported {
                    print("imported \(image.reference)")
                }
                for rejectedPath in rejectedPaths {
                    print("warning: skipped image archive member \(rejectedPath)")
                }
            }
        }

        private static func withAuthentication<T>(
            ref: String, insecure: Bool,
            _ body: @Sendable @escaping (_ auth: Authentication?) async throws -> T?
        ) async throws -> T? {
            let parsed = try Reference.parse(ref)
            guard let host = parsed.resolvedDomain else {
                throw ContainerizationError(.invalidArgument, message: "no host specified in image reference")
            }
            if insecure {
                return try await body(nil)
            }
            if let auth = Self.authenticationFromEnv(host: host) {
                return try await body(auth)
            }
            #if os(macOS)
            let keychain = KeychainHelper(securityDomain: Application.keychainID)
            let authentication = try? keychain.lookup(hostname: host)
            return try await body(authentication)
            #else
            return try await body(nil)
            #endif
        }

        private static func authenticationFromEnv(host: String) -> Authentication? {
            let env = ProcessInfo.processInfo.environment
            guard env["REGISTRY_HOST"] == host else {
                return nil
            }
            guard let user = env["REGISTRY_USERNAME"], let password = env["REGISTRY_TOKEN"] else {
                return nil
            }
            return BasicAuthentication(username: user, password: password)
        }
    }
}
