//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// 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 ContainerBuildIR
import ContainerBuildSnapshotter
import Foundation

/// Executes MetadataOperation (ENV, LABEL, USER, etc.).
public struct MetadataOperationExecutor: OperationExecutor {
    public let capabilities: ExecutorCapabilities

    public init() {
        self.capabilities = ExecutorCapabilities(
            supportedOperations: [.metadata],
            maxConcurrency: 20  // Metadata ops are lightweight
        )
    }

    public func execute(_ operation: ContainerBuildIR.Operation, context: ExecutionContext) async throws -> ExecutionResult {
        guard let metadataOp = operation as? MetadataOperation else {
            throw ExecutorError(
                type: .invalidConfiguration,
                context: ExecutorError.ErrorContext(
                    operation: operation, underlyingError: NSError(domain: "Executor", code: 1),
                    diagnostics: ExecutorError.Diagnostics(environment: [:], workingDirectory: "", recentLogs: [])))
        }

        do {
            // Stub implementation
            // In a real implementation, this would update the image configuration

            let startTime = Date()
            var environmentChanges: [String: EnvironmentValue] = [:]
            var metadataChanges: [String: String] = [:]

            // Apply metadata action
            switch metadataOp.action {
            case .setEnv(let key, let value):
                environmentChanges[key] = value
                context.updateEnvironment([key: value])
                context.updateImageConfig { $0.env.append("\(key)=\(value)") }

            // Note: There's no unsetEnv in MetadataAction
            // This would need to be handled differently in a real implementation

            case .setLabel(let key, let value):
                metadataChanges[key] = value
                context.updateImageConfig { $0.labels[key] = value }

            case .setUser(let user):
                context.setUser(user)
                let userString: String
                switch user {
                case .named(let name):
                    userString = name
                case .uid(let uid):
                    userString = String(uid)
                case .userGroup(let user, let group):
                    userString = "\(user):\(group)"
                case .uidGid(let uid, let gid):
                    userString = "\(uid):\(gid)"
                }
                context.updateImageConfig { $0.user = userString }

            case .setWorkdir(let path):
                context.setWorkingDirectory(path)
                context.updateImageConfig { $0.workingDir = path }

            case .setEntrypoint(let command):
                context.updateImageConfig { config in
                    switch command {
                    case .exec(let args):
                        config.entrypoint = args
                    case .shell(let cmd):
                        config.entrypoint = ["/bin/sh", "-c", cmd]
                    }
                }

            case .setCmd(let command):
                context.updateImageConfig { config in
                    switch command {
                    case .exec(let args):
                        config.cmd = args
                    case .shell(let cmd):
                        config.cmd = ["/bin/sh", "-c", cmd]
                    }
                }

            case .expose(let ports):
                for p in ports {
                    context.updateImageConfig { config in
                        config.exposedPorts.insert(p.stringValue)
                    }
                }

            case .setHealthcheck(let healthcheck):
                context.updateImageConfig { $0.healthcheck = healthcheck }

            case .setStopSignal(let signal):
                context.updateImageConfig { $0.stopSignal = signal }

            case .setShell(let shell):
                // Shell affects how commands are executed
                metadataChanges["shell"] = shell.joined(separator: " ")

            case .addVolume(let path):
                context.updateImageConfig { $0.volumes.insert(path) }

            case .setEnvBatch(let vars):
                for (key, value) in vars {
                    environmentChanges[key] = value
                    context.updateEnvironment([key: value])
                }
                context.updateImageConfig { config in
                    for (key, value) in vars {
                        config.env.append("\(key)=\(value)")
                    }
                }

            case .setLabelBatch(let labels):
                for (key, value) in labels {
                    metadataChanges[key] = value
                }
                context.updateImageConfig { config in
                    for (key, value) in labels {
                        config.labels[key] = value
                    }
                }

            case .declareArg(let name, let defaultValue):
                // ARG declarations are build-time only
                metadataChanges["arg:\(name)"] = defaultValue ?? ""

            case .addOnBuild(let instruction):
                // ONBUILD is stored as metadata
                metadataChanges["onbuild:\(UUID().uuidString)"] = instruction
            }

            // Metadata operations don't change the filesystem, so we reuse the parent snapshot
            // without going through the prepare/commit cycle
            let snapshot = try getOrCreateBaseSnapshot(context: context)

            let duration = Date().timeIntervalSince(startTime)

            return ExecutionResult(
                environmentChanges: environmentChanges,
                metadataChanges: metadataChanges,
                snapshot: snapshot,
                duration: duration
            )

        } catch {
            throw ExecutorError(
                type: .executionFailed,
                context: ExecutorError.ErrorContext(
                    operation: operation,
                    underlyingError: error,
                    diagnostics: ExecutorError.Diagnostics(
                        environment: context.environment.effectiveEnvironment,
                        workingDirectory: context.workingDirectory,
                        recentLogs: ["Failed to execute metadata operation", "Error: \(error.localizedDescription)"]
                    )
                )
            )
        }
    }

    /// Get the latest snapshot or create a base snapshot if none exists.
    ///
    /// Metadata operations don't modify the filesystem, so they can reuse
    /// the parent snapshot directly without snapshotter prepare/commit.
    ///
    /// - Parameter context: The execution context
    /// - Returns: The snapshot to use for this metadata operation
    private func getOrCreateBaseSnapshot(context: ExecutionContext) throws -> Snapshot {
        guard let latest = context.headSnapshot else {
            // Create a minimal base snapshot if no parent exists
            return Snapshot(
                digest: try Digest(algorithm: .sha256, bytes: Data(count: 32)),
                size: 0,
                parent: nil,
                state: .committed()
            )
        }
        return latest
    }

    public func canExecute(_ operation: ContainerBuildIR.Operation) -> Bool {
        operation is MetadataOperation
    }
}
