//===----------------------------------------------------------------------===//
// 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 ContainerNetworkService
import ContainerXPC
import Containerization
import ContainerizationError
import ContainerizationOCI
import Foundation
import TerminalProgress

public struct ClientContainer: Sendable, Codable {
    static let serviceIdentifier = "com.apple.container.apiserver"

    private var sandboxClient: SandboxClient {
        SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
    }

    /// Identifier of the container.
    public var id: String {
        configuration.id
    }

    public let status: RuntimeStatus

    /// Configured platform for the container.
    public var platform: ContainerizationOCI.Platform {
        configuration.platform
    }

    /// Configuration for the container.
    public let configuration: ContainerConfiguration

    /// Network allocated to the container.
    public let networks: [Attachment]

    package init(configuration: ContainerConfiguration) {
        self.configuration = configuration
        self.status = .stopped
        self.networks = []
    }

    init(snapshot: ContainerSnapshot) {
        self.configuration = snapshot.configuration
        self.status = snapshot.status
        self.networks = snapshot.networks
    }

    public var initProcess: ClientProcess {
        ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
    }
}

extension ClientContainer {
    private static func newClient() -> XPCClient {
        XPCClient(service: serviceIdentifier)
    }

    @discardableResult
    private static func xpcSend(
        client: XPCClient,
        message: XPCMessage,
        timeout: Duration? = .seconds(15)
    ) async throws -> XPCMessage {
        try await client.send(message, responseTimeout: timeout)
    }

    public static func create(
        configuration: ContainerConfiguration,
        options: ContainerCreateOptions = .default,
        kernel: Kernel
    ) async throws -> ClientContainer {
        do {
            let client = Self.newClient()
            let request = XPCMessage(route: .createContainer)

            let data = try JSONEncoder().encode(configuration)
            let kdata = try JSONEncoder().encode(kernel)
            let odata = try JSONEncoder().encode(options)
            request.set(key: .containerConfig, value: data)
            request.set(key: .kernel, value: kdata)
            request.set(key: .containerOptions, value: odata)

            try await xpcSend(client: client, message: request)
            return ClientContainer(configuration: configuration)
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to create container",
                cause: error
            )
        }
    }

    public static func list() async throws -> [ClientContainer] {
        do {
            let client = Self.newClient()
            let request = XPCMessage(route: .listContainer)

            let response = try await xpcSend(
                client: client,
                message: request,
                timeout: .seconds(10)
            )
            let data = response.dataNoCopy(key: .containers)
            guard let data else {
                return []
            }
            let configs = try JSONDecoder().decode([ContainerSnapshot].self, from: data)
            return configs.map { ClientContainer(snapshot: $0) }
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to list containers",
                cause: error
            )
        }
    }

    /// Get the container for the provided id.
    public static func get(id: String) async throws -> ClientContainer {
        let containers = try await list()
        guard let container = containers.first(where: { $0.id == id }) else {
            throw ContainerizationError(
                .notFound,
                message: "get failed: container \(id) not found"
            )
        }
        return container
    }
}

extension ClientContainer {
    public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
        let client = self.sandboxClient
        try await client.bootstrap(stdio: stdio)
        return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
    }

    /// Stop the container and all processes currently executing inside.
    public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
        do {
            let client = self.sandboxClient
            try await client.stop(options: opts)
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to stop container",
                cause: error
            )
        }
    }

    /// Delete the container along with any resources.
    public func delete(force: Bool = false) async throws {
        do {
            let client = XPCClient(service: Self.serviceIdentifier)
            let request = XPCMessage(route: .deleteContainer)
            request.set(key: .id, value: self.id)
            request.set(key: .forceDelete, value: force)
            try await client.send(request)
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to delete container",
                cause: error
            )
        }
    }
}

extension ClientContainer {
    /// Execute a new process inside a running container.
    public func createProcess(
        id: String,
        configuration: ProcessConfiguration,
        stdio: [FileHandle?]
    ) async throws -> ClientProcess {
        do {
            let client = self.sandboxClient
            try await client.createProcess(id, config: configuration, stdio: stdio)
            return ClientProcessImpl(containerId: self.id, processId: id, client: client)
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to exec in container",
                cause: error
            )
        }
    }

    /// Send or "kill" a signal to the initial process of the container.
    /// Kill does not wait for the process to exit, it only delivers the signal.
    public func kill(_ signal: Int32) async throws {
        do {
            let client = self.sandboxClient
            try await client.kill(self.id, signal: Int64(signal))
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to kill container \(self.id)",
                cause: error
            )
        }
    }

    public func logs() async throws -> [FileHandle] {
        do {
            let client = XPCClient(service: Self.serviceIdentifier)
            let request = XPCMessage(route: .containerLogs)
            request.set(key: .id, value: self.id)

            let response = try await client.send(request)
            let fds = response.fileHandles(key: .logs)
            guard let fds else {
                throw ContainerizationError(
                    .internalError,
                    message: "No log fds returned"
                )
            }
            return fds
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to get logs for container \(self.id)",
                cause: error
            )
        }
    }

    public func dial(_ port: UInt32) async throws -> FileHandle {
        do {
            let client = self.sandboxClient
            return try await client.dial(port)
        } catch {
            throw ContainerizationError(
                .internalError,
                message: "failed to dial \(port) in container \(self.id)",
                cause: error
            )
        }
    }
}
