/**
 * Docker Manager for Onebox
 *
 * Handles all Docker operations: containers, images, networks, volumes
 */

import * as plugins from '../plugins.js';
import type {
  IService,
  IContainerStats,
  IServicePublishedPort,
  IServiceSecretFile,
  IServiceVolume,
} from '../types.js';
import { logger } from '../logging.js';
import { getErrorMessage } from '../utils/error.js';

type TExpandedPublishedPort = Required<Pick<
  IServicePublishedPort,
  'targetPort' | 'publishedPort' | 'protocol' | 'hostIp'
>>;

interface IServiceRuntimeReferences {
  containerIds: string[];
  volumeNames: string[];
  imageName: string;
  imageReference?: string;
  imageId?: string;
}

interface IDockerContainerSummary {
  Id?: string;
  Image?: string;
  ImageID?: string;
  Names?: string[];
  Labels?: Record<string, string>;
}

interface IDockerSecretSummary {
  ID?: string;
  Spec?: {
    Name?: string;
    Labels?: Record<string, string>;
  };
}

interface IResolvedServiceSecretFile {
  mapping: IServiceSecretFile;
  value: string;
}

export interface IInteractiveContainerExec {
  stream: plugins.nodeStream.Duplex;
  close: () => Promise<void>;
  inspect: () => Promise<{ ExitCode?: number | null; Running?: boolean }>;
}

export interface IContainerLogStreamHandle {
  close: () => void;
}

export interface IDockerLogEntry {
  source: 'stdout' | 'stderr';
  content: string;
}

export interface IDockerLogParseResult {
  entries: IDockerLogEntry[];
  remainder: Uint8Array<ArrayBufferLike>;
}

export interface IDockerLogParseOptions {
  flushRemainderAsRaw?: boolean;
}

export class OneboxDockerManager {
  private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
  private networkName = 'onebox-network';
  private static logTextDecoder = new TextDecoder();
  private static logTextEncoder = new TextEncoder();
  private static readonly maxDockerLogFrameSize = 16 * 1024 * 1024;

  private getFullImageName(serviceArg: Pick<IService, 'registry' | 'image'>): string {
    return serviceArg.registry ? `${serviceArg.registry}/${serviceArg.image}` : serviceArg.image;
  }

  private getDockerSafeName(valueArg: string, maxLengthArg = 120): string {
    const safeName = valueArg
      .replace(/[^a-zA-Z0-9_.-]+/g, '-')
      .replace(/^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$/g, '')
      .slice(0, maxLengthArg)
      .replace(/[^a-zA-Z0-9]+$/g, '');
    return safeName || 'data';
  }

  private getServiceVolumeSource(serviceArg: Pick<IService, 'name'>, mountPathArg: string, requestedSourceArg?: string): string {
    if (requestedSourceArg) {
      return this.getDockerSafeName(requestedSourceArg);
    }
    const mountName = this.getDockerSafeName(mountPathArg.replace(/^\/+/, '').replace(/\/+$/g, ''), 40);
    return this.getDockerSafeName(`onebox-${serviceArg.name}-${mountName}`);
  }

  private getStandaloneVolumeBinds(serviceArg: IService): string[] {
    return (serviceArg.volumes || []).map((volumeArg) => {
      const source = this.getServiceVolumeSource(serviceArg, volumeArg.mountPath, volumeArg.source || volumeArg.name);
      return `${source}:${volumeArg.mountPath}${volumeArg.readOnly ? ':ro' : ''}`;
    });
  }

  private getAutoManagedVolumeSourceNames(serviceArg: Pick<IService, 'name'> & { volumes?: IServiceVolume[] }): Set<string> {
    const autoManagedNames = new Set<string>();
    for (const volumeArg of serviceArg.volumes || []) {
      if (!volumeArg.source && !volumeArg.name) {
        autoManagedNames.add(this.getServiceVolumeSource(serviceArg, volumeArg.mountPath));
      }
    }
    return autoManagedNames;
  }

  private async listAllContainerSummaries(): Promise<IDockerContainerSummary[]> {
    const response = await this.dockerClient!.request('GET', '/containers/json?all=true', {});
    if (response.statusCode >= 300) {
      throw new Error(`Failed to list Docker containers: HTTP ${response.statusCode}`);
    }
    return Array.isArray(response.body) ? response.body : [];
  }

  private isContainerSummaryForService(
    containerArg: IDockerContainerSummary,
    serviceArg: Pick<IService, 'name' | 'containerID'>,
  ): boolean {
    const labels = containerArg.Labels || {};
    if (labels['onebox-service'] === serviceArg.name) return true;
    if (serviceArg.containerID && labels['com.docker.swarm.service.id'] === serviceArg.containerID) return true;

    const expectedStandaloneName = `/onebox-${serviceArg.name}`;
    const expectedSwarmPrefix = `/onebox-${serviceArg.name}.`;
    return (containerArg.Names || []).some((nameArg) =>
      nameArg === expectedStandaloneName || nameArg.startsWith(expectedSwarmPrefix)
    );
  }

  private async inspectContainer(containerIdArg: string): Promise<any | null> {
    const response = await this.dockerClient!.request('GET', `/containers/${containerIdArg}/json`, {});
    if (response.statusCode === 404) return null;
    if (response.statusCode >= 300) {
      throw new Error(`Failed to inspect Docker container ${containerIdArg}: HTTP ${response.statusCode}`);
    }
    return response.body;
  }

  private async inspectVolume(volumeNameArg: string): Promise<any | null> {
    const response = await this.dockerClient!.request('GET', `/volumes/${encodeURIComponent(volumeNameArg)}`, {});
    if (response.statusCode === 404) return null;
    if (response.statusCode >= 300) {
      throw new Error(`Failed to inspect Docker volume ${volumeNameArg}: HTTP ${response.statusCode}`);
    }
    return response.body;
  }

  private isVolumeOwnedByService(
    volumeNameArg: string,
    volumeInfoArg: any,
    serviceArg: Pick<IService, 'name'> & { volumes?: IServiceVolume[] },
    mountedVolumeNamesArg: Set<string>,
  ): boolean {
    const labels = volumeInfoArg?.Labels || {};
    if (labels['onebox-service'] === serviceArg.name) return true;
    if ('com.docker.volume.anonymous' in labels && mountedVolumeNamesArg.has(volumeNameArg)) return true;
    return this.getAutoManagedVolumeSourceNames(serviceArg).has(volumeNameArg);
  }

  private async isVolumeUsedOutsideService(
    volumeNameArg: string,
    serviceArg: Pick<IService, 'name' | 'containerID'>,
  ): Promise<boolean> {
    const containers = await this.listAllContainerSummaries();
    for (const containerSummary of containers) {
      if (this.isContainerSummaryForService(containerSummary, serviceArg)) continue;
      const containerId = containerSummary.Id;
      if (!containerId) continue;
      const inspectedContainer = await this.inspectContainer(containerId);
      const mounts = Array.isArray(inspectedContainer?.Mounts) ? inspectedContainer.Mounts : [];
      if (mounts.some((mountArg: any) => mountArg.Type === 'volume' && mountArg.Name === volumeNameArg)) {
        return true;
      }
    }
    return false;
  }

  private async removeVolume(volumeNameArg: string): Promise<void> {
    const response = await this.dockerClient!.request(
      'DELETE',
      `/volumes/${encodeURIComponent(volumeNameArg)}?force=true`,
      {},
    );
    if (response.statusCode === 404) return;
    if (response.statusCode >= 300) {
      throw new Error(`Failed to remove Docker volume ${volumeNameArg}: HTTP ${response.statusCode}`);
    }
  }

  private getDockerVolumeMounts(serviceArg: Pick<IService, 'name'> & { volumes?: IServiceVolume[] }): Array<Record<string, unknown>> {
    return (serviceArg.volumes || []).map((volumeArg) => ({
      Type: 'volume',
      Source: this.getServiceVolumeSource(serviceArg, volumeArg.mountPath, volumeArg.source || volumeArg.name),
      Target: volumeArg.mountPath,
      ReadOnly: Boolean(volumeArg.readOnly),
      VolumeOptions: {
        DriverConfig: {
          Name: volumeArg.driver || 'local',
          Options: volumeArg.options || {},
        },
        Labels: {
          'managed-by': 'onebox',
          'onebox-service': serviceArg.name,
          'onebox-mount-path': volumeArg.mountPath,
          'onebox-backup': String(volumeArg.backup !== false),
        },
      },
    }));
  }

  public validateServiceSpec(serviceArg: IService): void {
    this.assertValidPort(serviceArg.port, `service port for ${serviceArg.name}`);

    for (const volumeArg of serviceArg.volumes || []) {
      if (!volumeArg.mountPath || !volumeArg.mountPath.startsWith('/')) {
        throw new Error(`Volume mountPath for service ${serviceArg.name} must be an absolute path`);
      }
      if (volumeArg.mountPath.includes(':')) {
        throw new Error(`Volume mountPath for service ${serviceArg.name} must not contain ':'`);
      }
      if ((volumeArg.source || volumeArg.name)?.includes(':')) {
        throw new Error(`Volume source/name for service ${serviceArg.name} must not contain ':'`);
      }
    }

    const containerArgErrors = plugins.servezoneInterfaces.data.validateServiceContainerArgs(
      serviceArg.containerArgs,
    );
    const secretFileErrors = plugins.servezoneInterfaces.data.validateServiceSecretFiles(
      serviceArg.secretFiles,
      (serviceArg.volumes || []).map((volumeArg) => volumeArg.mountPath),
    );
    const validationErrors = [...containerArgErrors, ...secretFileErrors];
    if (validationErrors.length > 0) {
      throw new Error(`Invalid service runtime configuration: ${validationErrors.join('; ')}`);
    }

    this.expandPublishedPorts(serviceArg);
  }

  public async isSwarmActive(): Promise<boolean> {
    try {
      const swarmResponse = await this.dockerClient!.request('GET', '/swarm', {});
      return swarmResponse.statusCode === 200;
    } catch {
      return false;
    }
  }

  public async assertSecretFilesSupported(secretFilesArg: IServiceSecretFile[] | undefined): Promise<void> {
    if (!secretFilesArg?.length) return;
    if (!(await this.isSwarmActive())) {
      throw new Error('secretFiles require Docker Swarm mode on Onebox');
    }
  }

  public validateServiceSecrets(
    serviceArg: IService,
    serviceSecretsArg: Record<string, string>,
  ): void {
    this.resolveServiceSecretDelivery(serviceArg, serviceSecretsArg);
  }

  private resolveServiceSecretDelivery(
    serviceArg: IService,
    serviceSecretsArg: Record<string, string>,
  ): { environment: Record<string, string>; secretFiles: IResolvedServiceSecretFile[] } {
    const publicEnvironment = { ...(serviceArg.envVars || {}) };
    const environmentSecrets = { ...serviceSecretsArg };
    const secretFiles: IResolvedServiceSecretFile[] = [];

    for (const mapping of serviceArg.secretFiles || []) {
      if (Object.prototype.hasOwnProperty.call(publicEnvironment, mapping.sourceKey)) {
        throw new Error(
          `File-only secret source ${mapping.sourceKey} collides with public service environment`,
        );
      }
      if (!Object.prototype.hasOwnProperty.call(serviceSecretsArg, mapping.sourceKey)) {
        throw new Error(`File-only secret source ${mapping.sourceKey} is missing`);
      }
      const value = serviceSecretsArg[mapping.sourceKey];
      if (
        typeof value !== 'string'
        || new TextEncoder().encode(value).byteLength
          > plugins.servezoneInterfaces.data.serviceSecretFileLimits.maximumValueBytes
      ) {
        throw new Error(`File-only secret source ${mapping.sourceKey} exceeds the value limit`);
      }
      const templateReference = `\${${mapping.sourceKey}}`;
      if (Object.values({ ...publicEnvironment, ...environmentSecrets }).some((candidateArg) => (
        typeof candidateArg === 'string' && candidateArg.includes(templateReference)
      ))) {
        throw new Error(
          `File-only secret source ${mapping.sourceKey} may not be referenced by an environment template`,
        );
      }
      delete environmentSecrets[mapping.sourceKey];
      secretFiles.push({ mapping, value });
    }

    const environment = { ...publicEnvironment, ...environmentSecrets };
    const templateValues = {
      ...environment,
      SERVICE_NAME: serviceArg.name,
      SERVICE_DOMAIN: serviceArg.domain || '',
      SERVICE_PORT: String(serviceArg.port),
    };
    for (const [key, value] of Object.entries(environment)) {
      environment[key] = value.replace(/\$\{([A-Z0-9_]+)\}/g, (_matchArg, nameArg: string) => {
        if (!Object.prototype.hasOwnProperty.call(templateValues, nameArg)) {
          throw new Error(`Unknown environment template placeholder ${nameArg} in ${key}`);
        }
        return templateValues[nameArg];
      });
    }
    return { environment, secretFiles };
  }

  private getManagedSecretName(serviceNameArg: string, sourceKeyArg: string): string {
    return this.getDockerSafeName(
      `onebox-${serviceNameArg}-${sourceKeyArg.toLowerCase()}`,
      64,
    );
  }

  private async createManagedServiceSecret(
    serviceArg: IService,
    secretFileArg: IResolvedServiceSecretFile,
  ): Promise<{ id: string; name: string }> {
    const name = this.getManagedSecretName(serviceArg.name, secretFileArg.mapping.sourceKey);
    const response = await this.dockerClient!.request('POST', '/secrets/create', {
      Name: name,
      Data: plugins.encoding.encodeBase64(secretFileArg.value),
      Labels: {
        'managed-by': 'onebox',
        'onebox-service': serviceArg.name,
        'onebox-secret': 'true',
      },
    });
    if (response.statusCode >= 300 || typeof response.body?.ID !== 'string') {
      throw new Error(`Failed to create Docker secret ${name}: HTTP ${response.statusCode}`);
    }
    return { id: response.body.ID, name };
  }

  private async deleteDockerSecretById(secretIdArg: string): Promise<void> {
    const response = await this.dockerClient!.request(
      'DELETE',
      `/secrets/${encodeURIComponent(secretIdArg)}`,
      {},
    );
    if (response.statusCode !== 404 && response.statusCode >= 300) {
      throw new Error(`Failed to remove Docker secret ${secretIdArg}: HTTP ${response.statusCode}`);
    }
  }

  public async removeManagedServiceSecrets(serviceNameArg: string): Promise<void> {
    if (!(await this.isSwarmActive())) return;
    const response = await this.dockerClient!.request('GET', '/secrets', {});
    if (response.statusCode === 404) return;
    if (response.statusCode >= 300) {
      throw new Error(`Failed to list Docker secrets: HTTP ${response.statusCode}`);
    }
    const ownedSecrets = (Array.isArray(response.body) ? response.body : [])
      .filter((secretArg: IDockerSecretSummary) => {
        const labels = secretArg.Spec?.Labels || {};
        return labels['managed-by'] === 'onebox'
          && labels['onebox-service'] === serviceNameArg
          && labels['onebox-secret'] === 'true'
          && typeof secretArg.ID === 'string';
      });
    for (const secret of ownedSecrets) {
      await this.deleteDockerSecretById(secret.ID!);
    }
  }

  private assertValidPort(portArg: number, labelArg: string): void {
    if (!Number.isInteger(portArg) || portArg < 1 || portArg > 65535) {
      throw new Error(`Invalid ${labelArg}: ${portArg}. Expected an integer port between 1 and 65535.`);
    }
  }

  private expandPublishedPorts(serviceArg: IService): TExpandedPublishedPort[] {
    const expandedPorts: TExpandedPublishedPort[] = [];
    const seenPublishedPorts = new Set<string>();

    for (const portArg of serviceArg.publishedPorts || []) {
      const protocol = portArg.protocol || 'tcp';
      const targetStart = portArg.targetPort;
      const targetEnd = portArg.targetPortEnd || targetStart;
      const publishedStart = portArg.publishedPort || targetStart;
      const publishedEnd = portArg.publishedPortEnd || (publishedStart + (targetEnd - targetStart));
      const hostIp = portArg.hostIp || '0.0.0.0';

      if (!['tcp', 'udp'].includes(protocol)) {
        throw new Error(`Invalid published port protocol for service ${serviceArg.name}: ${protocol}`);
      }
      this.assertValidPort(targetStart, `published targetPort for service ${serviceArg.name}`);
      this.assertValidPort(targetEnd, `published targetPortEnd for service ${serviceArg.name}`);
      this.assertValidPort(publishedStart, `published publishedPort for service ${serviceArg.name}`);
      this.assertValidPort(publishedEnd, `published publishedPortEnd for service ${serviceArg.name}`);
      if (targetEnd < targetStart) {
        throw new Error(`Invalid target port range for service ${serviceArg.name}: ${targetStart}-${targetEnd}`);
      }
      if (publishedEnd < publishedStart) {
        throw new Error(`Invalid published port range for service ${serviceArg.name}: ${publishedStart}-${publishedEnd}`);
      }
      if ((targetEnd - targetStart) !== (publishedEnd - publishedStart)) {
        throw new Error(
          `Published port range size must match target port range size for service ${serviceArg.name}`,
        );
      }
      if (!this.isValidHostIp(hostIp)) {
        throw new Error(`Invalid hostIp for service ${serviceArg.name}: ${hostIp}`);
      }

      for (let offset = 0; offset <= targetEnd - targetStart; offset++) {
        const publishedPort = publishedStart + offset;
        const publishedKey = `${hostIp}/${protocol}/${publishedPort}`;
        const wildcardKey = `0.0.0.0/${protocol}/${publishedPort}`;
        const conflictsWithWildcard = hostIp === '0.0.0.0'
          ? Array.from(seenPublishedPorts).some((keyArg) => keyArg.endsWith(`/${protocol}/${publishedPort}`))
          : seenPublishedPorts.has(wildcardKey);
        if (seenPublishedPorts.has(publishedKey) || conflictsWithWildcard) {
          throw new Error(`Duplicate published port for service ${serviceArg.name}: ${hostIp}:${publishedPort}/${protocol}`);
        }
        seenPublishedPorts.add(publishedKey);
        expandedPorts.push({
          targetPort: targetStart + offset,
          publishedPort,
          protocol,
          hostIp,
        });
      }
    }

    return expandedPorts;
  }

  private isValidHostIp(hostIpArg: string): boolean {
    if (['0.0.0.0', '127.0.0.1', '::', '::1', 'localhost'].includes(hostIpArg)) return true;
    if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostIpArg)) {
      return hostIpArg.split('.').every((partArg) => Number(partArg) >= 0 && Number(partArg) <= 255);
    }
    return /^[0-9a-fA-F:]+$/.test(hostIpArg);
  }

  private async assertPublishedPortsAvailable(serviceArg: IService): Promise<void> {
    const publishedPorts = this.expandPublishedPorts(serviceArg);
    if (publishedPorts.length === 0) return;

    await this.assertPublishedPortsNotUsedByDocker(serviceArg, publishedPorts);
    await this.assertPublishedPortsNotUsedByHost(serviceArg, publishedPorts);
  }

  private async assertPublishedPortsNotUsedByDocker(
    serviceArg: IService,
    publishedPortsArg: TExpandedPublishedPort[],
  ): Promise<void> {
    const requestedPorts = new Set(
      publishedPortsArg.map((portArg) => `${portArg.protocol}/${portArg.publishedPort}`),
    );

    try {
      const containersResponse = await this.dockerClient!.request('GET', '/containers/json?all=true', {});
      if (containersResponse.statusCode === 200 && Array.isArray(containersResponse.body)) {
        for (const containerArg of containersResponse.body) {
          const labels = containerArg.Labels || {};
          if (labels['onebox-service'] === serviceArg.name) continue;
          for (const portArg of containerArg.Ports || []) {
            if (!portArg.PublicPort || !portArg.Type) continue;
            if (requestedPorts.has(`${portArg.Type}/${portArg.PublicPort}`)) {
              throw new Error(
                `Published port ${portArg.PublicPort}/${portArg.Type} is already used by container ${containerArg.Names?.[0] || containerArg.Id}`,
              );
            }
          }
        }
      }

      const servicesResponse = await this.dockerClient!.request('GET', '/services', {});
      if (servicesResponse.statusCode === 200 && Array.isArray(servicesResponse.body)) {
        for (const service of servicesResponse.body) {
          if (service.Spec?.Name === `onebox-${serviceArg.name}`) continue;
          for (const portArg of service.Endpoint?.Ports || []) {
            if (!portArg.PublishedPort || !portArg.Protocol) continue;
            if (requestedPorts.has(`${portArg.Protocol}/${portArg.PublishedPort}`)) {
              throw new Error(
                `Published port ${portArg.PublishedPort}/${portArg.Protocol} is already used by Docker service ${service.Spec?.Name || service.ID}`,
              );
            }
          }
        }
      }
    } catch (error) {
      if (error instanceof Error && error.message.startsWith('Published port ')) throw error;
      logger.warn(`Could not complete Docker published-port preflight: ${getErrorMessage(error)}`);
    }
  }

  private async assertPublishedPortsNotUsedByHost(
    serviceArg: IService,
    publishedPortsArg: TExpandedPublishedPort[],
  ): Promise<void> {
    for (const portArg of publishedPortsArg) {
      try {
        if (portArg.protocol === 'udp') {
          await this.assertUdpPortAvailable(portArg.hostIp, portArg.publishedPort);
        } else {
          await this.assertTcpPortAvailable(portArg.hostIp, portArg.publishedPort);
        }
      } catch (error) {
        throw new Error(
          `Published port ${portArg.hostIp}:${portArg.publishedPort}/${portArg.protocol} for service ${serviceArg.name} is not available: ${getErrorMessage(error)}`,
        );
      }
    }
  }

  private async assertUdpPortAvailable(hostIpArg: string, portArg: number): Promise<void> {
    const dgram = await import('node:dgram');
    const socket = dgram.createSocket(hostIpArg.includes(':') ? 'udp6' : 'udp4');
    await new Promise<void>((resolve, reject) => {
      socket.once('error', reject);
      socket.bind(portArg, hostIpArg, () => {
        socket.close();
        resolve();
      });
    });
  }

  private async assertTcpPortAvailable(hostIpArg: string, portArg: number): Promise<void> {
    const server = plugins.nodeNet.createServer();
    await new Promise<void>((resolve, reject) => {
      server.once('error', reject);
      server.listen(portArg, hostIpArg, () => {
        server.close((error) => error ? reject(error) : resolve());
      });
    });
  }

  private getStandalonePortConfig(serviceArg: IService): {
    exposedPorts: Record<string, Record<string, never>>;
    portBindings: Record<string, Array<{ HostIp: string; HostPort: string }>>;
  } {
    const exposedPorts: Record<string, Record<string, never>> = {
      [`${serviceArg.port}/tcp`]: {},
    };
    const portBindings: Record<string, Array<{ HostIp: string; HostPort: string }>> = {
      [`${serviceArg.port}/tcp`]: [],
    };

    for (const publishedPort of this.expandPublishedPorts(serviceArg)) {
      const key = `${publishedPort.targetPort}/${publishedPort.protocol}`;
      exposedPorts[key] = {};
      portBindings[key] = [{ HostIp: publishedPort.hostIp, HostPort: String(publishedPort.publishedPort) }];
    }

    return { exposedPorts, portBindings };
  }

  /**
   * Initialize Docker client and create onebox network
   */
  async init(): Promise<void> {
    try {
      // Initialize Docker client (connects to /var/run/docker.sock by default)
      this.dockerClient = new plugins.docker.Docker({
        socketPath: 'unix:///var/run/docker.sock',
      });

      // Start the Docker client
      await this.dockerClient.start();

      logger.info('Docker client initialized');

      // Ensure onebox network exists
      await this.ensureNetwork();
    } catch (error) {
      logger.error(`Failed to initialize Docker client: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Release resources held by the Docker API client.
   */
  async stop(): Promise<void> {
    if (!this.dockerClient) {
      return;
    }

    try {
      await this.dockerClient.stop();
    } catch (error) {
      logger.error(`Failed to stop Docker client: ${getErrorMessage(error)}`);
    } finally {
      this.dockerClient = null;
    }
  }

  /**
   * Ensure onebox network exists
   */
  private async ensureNetwork(): Promise<void> {
    try {
      const networks = await this.dockerClient!.listNetworks();
      const existingNetwork = networks.find((n: any) => n.Name === this.networkName);

      if (!existingNetwork) {
        logger.info(`Creating Docker network: ${this.networkName}`);

        // Check if Docker is in Swarm mode
        let isSwarmMode = false;
        try {
          const swarmResponse = await this.dockerClient!.request('GET', '/swarm', {});
          isSwarmMode = swarmResponse.statusCode === 200;
        } catch (error) {
          isSwarmMode = false;
        }

        await this.dockerClient!.createNetwork({
          Name: this.networkName,
          Driver: isSwarmMode ? 'overlay' : 'bridge',
          Attachable: isSwarmMode ? true : undefined, // Required for overlay networks to allow standalone containers
          Labels: {
            'managed-by': 'onebox',
          },
        });
        logger.success(`Docker network created: ${this.networkName} (${isSwarmMode ? 'overlay' : 'bridge'})`);
      } else {
        logger.debug(`Docker network already exists: ${this.networkName}`);
      }
    } catch (error) {
      logger.error(`Failed to create Docker network: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Pull an image from a registry
   */
  async pullImage(image: string, registry?: string): Promise<void> {
    const fullImage = registry ? `${registry}/${image}` : image;
    logger.info(`Pulling image: ${fullImage}`);

    try {
      // Parse image name and tag (e.g., "nginx:alpine" -> imageUrl: "nginx", imageTag: "alpine")
      const [imageUrl, imageTag] = fullImage.includes(':')
        ? fullImage.split(':')
        : [fullImage, 'latest'];

      // Use the library's built-in createImageFromRegistry method
      await this.dockerClient!.createImageFromRegistry({
        imageUrl,
        imageTag,
      });

      logger.success(`Image pulled successfully: ${fullImage}`);
    } catch (error) {
      logger.error(`Failed to pull image ${fullImage}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  public async getImageReference(imageArg: string, registryArg?: string): Promise<string | undefined> {
    const fullImage = registryArg ? `${registryArg}/${imageArg}` : imageArg;
    const response = await this.dockerClient!.request(
      'GET',
      `/images/${encodeURIComponent(fullImage)}/json`,
      {},
    );
    if (response.statusCode === 404) return undefined;
    if (response.statusCode >= 300) {
      throw new Error(`Failed to inspect Docker image ${fullImage}: HTTP ${response.statusCode}`);
    }
    return typeof response.body?.Id === 'string' && response.body.Id.length > 0
      ? response.body.Id
      : undefined;
  }

  public async tagImage(imageReferenceArg: string, targetImageNameArg: string): Promise<void> {
    const lastSlashIndex = targetImageNameArg.lastIndexOf('/');
    const lastColonIndex = targetImageNameArg.lastIndexOf(':');
    const hasExplicitTag = lastColonIndex > lastSlashIndex;
    const repository = hasExplicitTag
      ? targetImageNameArg.slice(0, lastColonIndex)
      : targetImageNameArg;
    const tag = hasExplicitTag ? targetImageNameArg.slice(lastColonIndex + 1) : 'latest';
    if (!repository || !tag || targetImageNameArg.includes('@')) {
      throw new Error(`Cannot restore unsupported Docker image tag: ${targetImageNameArg}`);
    }
    const response = await this.dockerClient!.request(
      'POST',
      `/images/${encodeURIComponent(imageReferenceArg)}/tag?repo=${encodeURIComponent(repository)}&tag=${encodeURIComponent(tag)}`,
      {},
    );
    if (response.statusCode >= 300) {
      throw new Error(`Failed to tag Docker image ${imageReferenceArg} as ${targetImageNameArg}: HTTP ${response.statusCode}`);
    }
    logger.info(`Restored Docker image tag ${targetImageNameArg} -> ${imageReferenceArg}`);
  }

  /**
   * Create and start a container or service (depending on Swarm mode)
   */
  async createContainer(
    service: IService,
    serviceSecretsArg: Record<string, string> = {},
  ): Promise<string> {
    try {
      this.validateServiceSpec(service);
      await this.assertPublishedPortsAvailable(service);

      const isSwarmMode = await this.isSwarmActive();
      if (service.secretFiles?.length && !isSwarmMode) {
        throw new Error('secretFiles require Docker Swarm mode on Onebox');
      }
      const secretDelivery = this.resolveServiceSecretDelivery(service, serviceSecretsArg);

      if (isSwarmMode) {
        return await this.createSwarmService(service, secretDelivery);
      } else {
        return await this.createStandaloneContainer(service, secretDelivery.environment);
      }
    } catch (error) {
      logger.error(`Failed to create container for ${service.name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Create a standalone container (non-Swarm mode)
   */
  private async createStandaloneContainer(
    service: IService,
    environmentArg: Record<string, string>,
  ): Promise<string> {
    logger.info(`Creating standalone container for service: ${service.name}`);

    const fullImage = service.registry
      ? `${service.registry}/${service.image}`
      : service.image;

    // Prepare environment variables
    const env: string[] = [];
    for (const [key, value] of Object.entries(environmentArg)) {
      env.push(`${key}=${value}`);
    }

    const portConfig = this.getStandalonePortConfig(service);

    // Create container using Docker REST API directly
    const response = await this.dockerClient!.request('POST', `/containers/create?name=onebox-${service.name}`, {
      Image: fullImage,
      ...(service.containerArgs === undefined ? {} : { Cmd: service.containerArgs }),
      Env: env,
      Labels: {
        'managed-by': 'onebox',
        'onebox-service': service.name,
      },
      ExposedPorts: portConfig.exposedPorts,
      HostConfig: {
        NetworkMode: this.networkName,
        RestartPolicy: {
          Name: 'unless-stopped',
        },
        PortBindings: portConfig.portBindings,
        Mounts: this.getDockerVolumeMounts(service),
        ExtraHosts: ['host.docker.internal:host-gateway'],
      },
    });

    if (response.statusCode >= 300) {
      throw new Error(`Failed to create container: HTTP ${response.statusCode}`);
    }

    const containerID = response.body.Id;
    logger.success(`Standalone container created: ${containerID}`);

    return containerID;
  }

  /**
   * Create a Docker Swarm service
   */
  private async createSwarmService(
    service: IService,
    secretDeliveryArg: {
      environment: Record<string, string>;
      secretFiles: IResolvedServiceSecretFile[];
    },
  ): Promise<string> {
    logger.info(`Creating Swarm service for: ${service.name}`);

    const fullImage = service.registry
      ? `${service.registry}/${service.image}`
      : service.image;

    // Prepare environment variables
    const env: string[] = [];
    for (const [key, value] of Object.entries(secretDeliveryArg.environment)) {
      env.push(`${key}=${value}`);
    }

    const expandedPublishedPorts = this.expandPublishedPorts(service);
    const endpointPorts: Array<Record<string, unknown>> = [];
    if (!expandedPublishedPorts.some((publishedPort) => publishedPort.protocol === 'tcp' && publishedPort.targetPort === service.port)) {
      endpointPorts.push({
        Protocol: 'tcp',
        TargetPort: service.port,
        PublishMode: 'host',
      });
    }

    for (const publishedPort of expandedPublishedPorts) {
      endpointPorts.push({
        Protocol: publishedPort.protocol,
        TargetPort: publishedPort.targetPort,
        PublishedPort: publishedPort.publishedPort,
        PublishMode: 'host',
      });
    }

    await this.removeManagedServiceSecrets(service.name);
    const createdSecrets: Array<{ id: string; name: string }> = [];
    try {
      for (const secretFile of secretDeliveryArg.secretFiles) {
        createdSecrets.push(await this.createManagedServiceSecret(service, secretFile));
      }

      // Create Swarm service using Docker REST API
      const response = await this.dockerClient!.request('POST', '/services/create', {
      Name: `onebox-${service.name}`,
      Labels: {
        'managed-by': 'onebox',
        'onebox-service': service.name,
      },
      TaskTemplate: {
        ContainerSpec: {
          Image: fullImage,
          ...(service.containerArgs === undefined ? {} : { Args: service.containerArgs }),
          Env: env,
          ...(createdSecrets.length === 0 ? {} : {
            Secrets: createdSecrets.map((secretArg, indexArg) => {
              const mapping = secretDeliveryArg.secretFiles[indexArg].mapping;
              return {
                File: {
                  Name: mapping.targetPath,
                  UID: String(mapping.uid),
                  GID: String(mapping.gid),
                  Mode: mapping.mode,
                },
                SecretID: secretArg.id,
                SecretName: secretArg.name,
              };
            }),
          }),
          Mounts: this.getDockerVolumeMounts(service),
          Hosts: ['host.docker.internal:host-gateway'],
          Labels: {
            'managed-by': 'onebox',
            'onebox-service': service.name,
          },
        },
        Networks: [
          {
            Target: await this.getNetworkID(this.networkName),
          },
        ],
        RestartPolicy: {
          Condition: 'any',
          MaxAttempts: 0,
        },
      },
      Mode: {
        Replicated: {
          Replicas: 1,
        },
      },
      EndpointSpec: {
        Ports: endpointPorts,
      },
      });

      if (response.statusCode >= 300) {
        throw new Error(`Failed to create service: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
      }

      const serviceID = response.body.ID;
      logger.success(`Swarm service created: ${serviceID}`);

      return serviceID;
    } catch (error) {
      for (const secret of createdSecrets.reverse()) {
        await this.deleteDockerSecretById(secret.id).catch((cleanupError) => {
          logger.warn(`Failed to clean Docker secret ${secret.name}: ${getErrorMessage(cleanupError)}`);
        });
      }
      throw error;
    }
  }

  /**
   * Get network ID by name
   */
  private async getNetworkID(networkName: string): Promise<string> {
    const networks = await this.dockerClient!.listNetworks();
    const network = networks.find((n: any) => n.Name === networkName);
    if (!network) {
      throw new Error(`Network not found: ${networkName}`);
    }
    return network.Id;
  }

  /**
   * Start a container or service by ID
   */
  async startContainer(containerID: string): Promise<void> {
    try {
      // Try service first
      if (await this.isService(containerID)) {
        return await this.startService(containerID);
      }

      logger.info(`Starting container: ${containerID}`);

      const response = await this.dockerClient!.request('POST', `/containers/${containerID}/start`, {});

      if (response.statusCode >= 300 && response.statusCode !== 304) {
        throw new Error(`Failed to start container: HTTP ${response.statusCode}`);
      }

      logger.success(`Container started: ${containerID}`);
    } catch (error) {
      // Ignore "already started" errors (304 status)
      if (getErrorMessage(error).includes('304')) {
        logger.debug(`Container already running: ${containerID}`);
        return;
      }
      logger.error(`Failed to start container ${containerID}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Start a Swarm service (scale to 1 replica)
   */
  private async startService(serviceID: string): Promise<void> {
    logger.info(`Starting service: ${serviceID}`);

    // Get current service spec
    const getResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
    if (getResponse.statusCode >= 300) {
      throw new Error(`Failed to get service: HTTP ${getResponse.statusCode}`);
    }

    const service = getResponse.body;
    const version = service.Version.Index;

    // Update service to scale to 1 replica
    const updateResponse = await this.dockerClient!.request('POST', `/services/${serviceID}/update?version=${version}`, {
      ...service.Spec,
      Mode: {
        Replicated: {
          Replicas: 1,
        },
      },
    });

    if (updateResponse.statusCode >= 300) {
      throw new Error(`Failed to start service: HTTP ${updateResponse.statusCode}`);
    }

    logger.success(`Service started (scaled to 1 replica): ${serviceID}`);
  }

  /**
   * Check if ID is a service (not a container)
   */
  private async isService(id: string): Promise<boolean> {
    try {
      const response = await this.dockerClient!.request('GET', `/services/${id}`, {});
      return response.statusCode === 200;
    } catch (error) {
      return false;
    }
  }

  /**
   * Stop a container or service by ID
   */
  async stopContainer(containerID: string): Promise<void> {
    try {
      // Try service first
      if (await this.isService(containerID)) {
        return await this.stopService(containerID);
      }

      logger.info(`Stopping container: ${containerID}`);

      const response = await this.dockerClient!.request('POST', `/containers/${containerID}/stop`, {});

      if (response.statusCode >= 300 && response.statusCode !== 304) {
        throw new Error(`Failed to stop container: HTTP ${response.statusCode}`);
      }

      logger.success(`Container stopped: ${containerID}`);
    } catch (error) {
      // Ignore "already stopped" errors (304 status)
      if (getErrorMessage(error).includes('304')) {
        logger.debug(`Container already stopped: ${containerID}`);
        return;
      }
      logger.error(`Failed to stop container ${containerID}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Stop a Swarm service (scale to 0 replicas)
   */
  private async stopService(serviceID: string): Promise<void> {
    logger.info(`Stopping service: ${serviceID}`);

    // Get current service spec
    const getResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
    if (getResponse.statusCode >= 300) {
      throw new Error(`Failed to get service: HTTP ${getResponse.statusCode}`);
    }

    const service = getResponse.body;
    const version = service.Version.Index;

    // Update service to scale to 0 replicas
    const updateResponse = await this.dockerClient!.request('POST', `/services/${serviceID}/update?version=${version}`, {
      ...service.Spec,
      Mode: {
        Replicated: {
          Replicas: 0,
        },
      },
    });

    if (updateResponse.statusCode >= 300) {
      throw new Error(`Failed to stop service: HTTP ${updateResponse.statusCode}`);
    }

    logger.success(`Service stopped (scaled to 0 replicas): ${serviceID}`);
  }

  /**
   * Restart a container or service by ID
   */
  async restartContainer(containerID: string): Promise<void> {
    try {
      // Try service first
      if (await this.isService(containerID)) {
        return await this.restartService(containerID);
      }

      logger.info(`Restarting container: ${containerID}`);

      const response = await this.dockerClient!.request('POST', `/containers/${containerID}/restart`, {});

      if (response.statusCode >= 300) {
        throw new Error(`Failed to restart container: HTTP ${response.statusCode}`);
      }

      logger.success(`Container restarted: ${containerID}`);
    } catch (error) {
      logger.error(`Failed to restart container ${containerID}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Restart a Swarm service (force update with same spec)
   */
  private async restartService(serviceID: string): Promise<void> {
    logger.info(`Restarting service: ${serviceID}`);

    // Get current service spec
    const getResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
    if (getResponse.statusCode >= 300) {
      throw new Error(`Failed to get service: HTTP ${getResponse.statusCode}`);
    }

    const service = getResponse.body;
    const version = service.Version.Index;

    // Force update to trigger restart
    const updateResponse = await this.dockerClient!.request('POST', `/services/${serviceID}/update?version=${version}`, {
      ...service.Spec,
      TaskTemplate: {
        ...service.Spec.TaskTemplate,
        ForceUpdate: (service.Spec.TaskTemplate.ForceUpdate || 0) + 1,
      },
    });

    if (updateResponse.statusCode >= 300) {
      throw new Error(`Failed to restart service: HTTP ${updateResponse.statusCode}`);
    }

    logger.success(`Service restarted: ${serviceID}`);
  }

  /**
   * Remove a container or service by ID
   */
  async removeContainer(
    containerID: string,
    force = false,
    optionsArg: { removeVolumes?: boolean } = {},
  ): Promise<void> {
    try {
      // Try service first
      if (await this.isService(containerID)) {
        return await this.removeService(containerID);
      }

      logger.info(`Removing container: ${containerID}`);

      // Stop first if not forced
      if (!force) {
        try {
          await this.stopContainer(containerID);
        } catch (error) {
          // Ignore stop errors
          logger.debug(`Error stopping container before removal: ${getErrorMessage(error)}`);
        }
      }

      const params = new URLSearchParams();
      if (force) params.set('force', 'true');
      if (optionsArg.removeVolumes) params.set('v', 'true');
      const query = params.toString();
      const url = `/containers/${containerID}${query ? `?${query}` : ''}`;
      const response = await this.dockerClient!.request('DELETE', url, {});

      if (response.statusCode >= 300) {
        throw new Error(`Failed to remove container: HTTP ${response.statusCode}`);
      }

      logger.success(`Container removed: ${containerID}`);
    } catch (error) {
      logger.error(`Failed to remove container ${containerID}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Remove a Swarm service
   */
  private async removeService(serviceID: string): Promise<void> {
    logger.info(`Removing service: ${serviceID}`);

    const response = await this.dockerClient!.request('DELETE', `/services/${serviceID}`, {});

    if (response.statusCode >= 300) {
      throw new Error(`Failed to remove service: HTTP ${response.statusCode}`);
    }

    logger.success(`Service removed: ${serviceID}`);
  }

  public async collectServiceRuntimeReferences(serviceArg: IService): Promise<IServiceRuntimeReferences> {
    const containerIds = new Set<string>();
    const volumeNames = new Set<string>();
    let imageReference: string | undefined;
    let imageId: string | undefined;

    for (const volumeName of this.getAutoManagedVolumeSourceNames(serviceArg)) {
      volumeNames.add(volumeName);
    }

    if (serviceArg.containerID) {
      const serviceResponse = await this.dockerClient!.request('GET', `/services/${serviceArg.containerID}`, {});
      if (serviceResponse.statusCode === 200) {
        const serviceImage = serviceResponse.body?.Spec?.TaskTemplate?.ContainerSpec?.Image;
        if (typeof serviceImage === 'string' && serviceImage.length > 0) {
          imageReference = serviceImage;
        }
      } else {
        try {
          const directContainer = await this.inspectContainer(serviceArg.containerID);
          if (directContainer?.Id) {
            containerIds.add(directContainer.Id);
          }
        } catch (error) {
          logger.debug(`Could not inspect direct container for ${serviceArg.name}: ${getErrorMessage(error)}`);
        }
      }
    }

    for (const containerSummary of await this.listAllContainerSummaries()) {
      if (!this.isContainerSummaryForService(containerSummary, serviceArg) || !containerSummary.Id) continue;
      containerIds.add(containerSummary.Id);
    }

    for (const containerId of containerIds) {
      const containerInfo = await this.inspectContainer(containerId);
      if (
        typeof containerInfo?.Image === 'string'
        && containerInfo.Image.length > 0
      ) {
        imageId = containerInfo.Image;
        if (!imageReference?.includes('@sha256:')) {
          imageReference = containerInfo.Image;
        }
      }
      const mounts = Array.isArray(containerInfo?.Mounts) ? containerInfo.Mounts : [];
      for (const mount of mounts) {
        if (mount.Type === 'volume' && mount.Name) {
          volumeNames.add(mount.Name);
        }
      }
    }

    return {
      containerIds: Array.from(containerIds),
      volumeNames: Array.from(volumeNames),
      imageName: this.getFullImageName(serviceArg),
      imageReference,
      imageId,
    };
  }

  public async removeServiceRuntime(serviceArg: IService): Promise<IServiceRuntimeReferences> {
    const runtimeReferences = await this.collectServiceRuntimeReferences(serviceArg);
    let attemptedPrimaryRemoval = false;

    if (serviceArg.containerID) {
      attemptedPrimaryRemoval = true;
      try {
        await this.removeContainer(serviceArg.containerID, true, { removeVolumes: true });
      } catch (error) {
        const message = getErrorMessage(error);
        if (!message.toLowerCase().includes('not found') && !message.includes('404')) {
          throw error;
        }
        logger.warn(`Primary runtime for service ${serviceArg.name} was already absent: ${message}`);
      }
    }

    const dockerServiceName = `onebox-${serviceArg.name}`;
    if (await this.isService(dockerServiceName)) {
      attemptedPrimaryRemoval = true;
      await this.removeService(dockerServiceName);
    }

    if (!attemptedPrimaryRemoval) {
      logger.warn(`Service ${serviceArg.name} has no tracked Docker runtime ID; cleaning by labels and names`);
    }

    await this.waitForServiceContainersGone(serviceArg, 10, 500);
    await this.removeLeftoverServiceContainers(serviceArg);
    await this.removeManagedServiceSecrets(serviceArg.name);

    return runtimeReferences;
  }

  public async removeServiceRuntimeForReplacement(
    serviceArg: IService,
    onRuntimeRemovedArg?: () => void,
  ): Promise<void> {
    let runtimeRemoved = false;
    const markRuntimeRemoved = () => {
      if (runtimeRemoved) return;
      runtimeRemoved = true;
      onRuntimeRemovedArg?.();
    };
    if (serviceArg.containerID) {
      try {
        await this.removeContainer(serviceArg.containerID, true);
        markRuntimeRemoved();
      } catch (error) {
        const message = getErrorMessage(error);
        if (!message.toLowerCase().includes('not found') && !message.includes('404')) {
          throw error;
        }
      }
    }
    const dockerServiceName = `onebox-${serviceArg.name}`;
    if (await this.isService(dockerServiceName)) {
      await this.removeService(dockerServiceName);
      markRuntimeRemoved();
    }
    await this.waitForServiceContainersGone(serviceArg, 10, 500);
    const containers = (await this.listAllContainerSummaries())
      .filter((containerArg) => this.isContainerSummaryForService(containerArg, serviceArg));
    for (const container of containers) {
      if (container.Id) {
        await this.removeContainer(container.Id, true);
        markRuntimeRemoved();
      }
    }
    await this.removeManagedServiceSecrets(serviceArg.name);
  }

  private async waitForServiceContainersGone(
    serviceArg: Pick<IService, 'name' | 'containerID'>,
    attemptsArg: number,
    intervalMsArg: number,
  ): Promise<void> {
    for (let i = 0; i < attemptsArg; i++) {
      const containers = (await this.listAllContainerSummaries())
        .filter((containerArg) => this.isContainerSummaryForService(containerArg, serviceArg));
      if (containers.length === 0) return;
      await new Promise((resolve) => setTimeout(resolve, intervalMsArg));
    }
  }

  private async removeLeftoverServiceContainers(serviceArg: Pick<IService, 'name' | 'containerID'>): Promise<void> {
    const containers = (await this.listAllContainerSummaries())
      .filter((containerArg) => this.isContainerSummaryForService(containerArg, serviceArg));

    for (const container of containers) {
      if (!container.Id) continue;
      await this.removeContainer(container.Id, true, { removeVolumes: true });
    }
  }

  public async removeServiceOwnedVolumes(
    serviceArg: IService,
    candidateVolumeNamesArg: string[],
  ): Promise<string[]> {
    const mountedVolumeNames = new Set(candidateVolumeNamesArg);
    const removedVolumes: string[] = [];

    for (const volumeName of new Set(candidateVolumeNamesArg)) {
      const volumeInfo = await this.inspectVolume(volumeName);
      if (!volumeInfo) continue;
      if (!this.isVolumeOwnedByService(volumeName, volumeInfo, serviceArg, mountedVolumeNames)) {
        logger.debug(`Skipping non-service-owned volume ${volumeName} for ${serviceArg.name}`);
        continue;
      }
      if (await this.isVolumeUsedOutsideService(volumeName, serviceArg)) {
        throw new Error(`Refusing to remove volume ${volumeName}: it is used outside service ${serviceArg.name}`);
      }
      await this.removeVolume(volumeName);
      removedVolumes.push(volumeName);
      logger.info(`Removed Docker volume ${volumeName} for service ${serviceArg.name}`);
    }

    return removedVolumes;
  }

  public async removeCandidateOnlyServiceVolumes(
    candidateServiceArg: IService,
    priorVolumeNamesArg: string[],
    candidateVolumeNamesArg: string[],
  ): Promise<string[]> {
    const priorVolumeNames = new Set(priorVolumeNamesArg);
    const candidateAutoManagedNames = this.getAutoManagedVolumeSourceNames(candidateServiceArg);
    const removedVolumes: string[] = [];

    for (const volumeName of new Set(candidateVolumeNamesArg)) {
      if (priorVolumeNames.has(volumeName)) continue;
      const volumeInfo = await this.inspectVolume(volumeName);
      if (!volumeInfo) continue;
      const labels = volumeInfo.Labels || {};
      const isCandidateOwned = candidateAutoManagedNames.has(volumeName)
        || 'com.docker.volume.anonymous' in labels;
      if (!isCandidateOwned) {
        logger.debug(`Preserving candidate volume ${volumeName}: it is not auto-managed`);
        continue;
      }
      if (await this.isVolumeUsedOutsideService(volumeName, candidateServiceArg)) {
        throw new Error(`Refusing to remove volume ${volumeName}: it is used outside service ${candidateServiceArg.name}`);
      }
      await this.removeVolume(volumeName);
      removedVolumes.push(volumeName);
      logger.info(`Removed candidate-only Docker volume ${volumeName} for service ${candidateServiceArg.name}`);
    }

    return removedVolumes;
  }

  public async removeImageIfUnused(imageNameArg: string): Promise<boolean> {
    const containers = await this.listAllContainerSummaries();
    if (containers.some((containerArg) => containerArg.Image === imageNameArg || containerArg.ImageID === imageNameArg)) {
      logger.debug(`Skipping image removal for ${imageNameArg}: image is used by a container`);
      return false;
    }

    const servicesResponse = await this.dockerClient!.request('GET', '/services', {});
    if (servicesResponse.statusCode === 200 && Array.isArray(servicesResponse.body)) {
      const usedByService = servicesResponse.body.some((serviceArg: any) => {
        const serviceImage = serviceArg.Spec?.TaskTemplate?.ContainerSpec?.Image;
        return typeof serviceImage === 'string' && (serviceImage === imageNameArg || serviceImage.startsWith(`${imageNameArg}@`));
      });
      if (usedByService) {
        logger.debug(`Skipping image removal for ${imageNameArg}: image is used by a Docker service`);
        return false;
      }
    }

    const response = await this.dockerClient!.request(
      'DELETE',
      `/images/${encodeURIComponent(imageNameArg)}?force=false&noprune=false`,
      {},
    );
    if (response.statusCode === 404) return false;
    if (response.statusCode === 409) {
      logger.warn(`Skipping image removal for ${imageNameArg}: Docker reports it is still in use`);
      return false;
    }
    if (response.statusCode >= 300) {
      throw new Error(`Failed to remove Docker image ${imageNameArg}: HTTP ${response.statusCode}`);
    }

    logger.info(`Removed unused Docker image ${imageNameArg}`);
    return true;
  }

  /**
   * Get container or service status
   */
  async getContainerStatus(containerID: string): Promise<string> {
    try {
      // Try service first
      if (await this.isService(containerID)) {
        return await this.getServiceStatus(containerID);
      }

      const response = await this.dockerClient!.request('GET', `/containers/${containerID}/json`, {});

      if (response.statusCode >= 300) {
        return 'unknown';
      }

      return response.body.State?.Status || 'unknown';
    } catch (error) {
      logger.error(`Failed to get container status ${containerID}: ${getErrorMessage(error)}`);
      return 'unknown';
    }
  }

  /**
   * Get Swarm service status
   */
  private async getServiceStatus(serviceID: string): Promise<string> {
    try {
      // Get service details
      const serviceResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
      if (serviceResponse.statusCode >= 300) {
        return 'unknown';
      }

      const service = serviceResponse.body;
      const replicas = service.Spec?.Mode?.Replicated?.Replicas || 0;

      if (replicas === 0) {
        return 'stopped';
      }

      // Get tasks for this service to check if they're running
      const tasksResponse = await this.dockerClient!.request('GET', `/tasks?filters=${encodeURIComponent(JSON.stringify({service: [serviceID]}))}`, {});
      if (tasksResponse.statusCode >= 300) {
        return 'unknown';
      }

      const tasks = tasksResponse.body;
      if (tasks.length === 0) {
        return 'starting';
      }

      // Check if any task is running
      const hasRunning = tasks.some((task: any) => task.Status?.State === 'running');
      if (hasRunning) {
        return 'running';
      }

      // Check task states
      const latestTask = tasks[0];
      const taskState = latestTask?.Status?.State || 'unknown';

      // Map Swarm task states to container-like states
      switch (taskState) {
        case 'new':
        case 'allocated':
        case 'pending':
        case 'assigned':
        case 'accepted':
        case 'preparing':
        case 'ready':
        case 'starting':
          return 'starting';
        case 'running':
          return 'running';
        case 'complete':
          return 'exited';
        case 'failed':
        case 'shutdown':
        case 'rejected':
        case 'orphaned':
        case 'remove':
          return 'stopped';
        default:
          return 'unknown';
      }
    } catch (error) {
      logger.error(`Failed to get service status ${serviceID}: ${getErrorMessage(error)}`);
      return 'unknown';
    }
  }

  /**
   * Get container stats (CPU, memory, network)
   * Handles both regular containers and Swarm services
   */
  async getContainerStats(containerID: string): Promise<IContainerStats | null> {
    try {
      let stats: any = null;

      try {
        stats = await this.getContainerStatsRaw(containerID);
      } catch (error) {
        if (!OneboxDockerManager.isDockerMissingError(error)) {
          throw error;
        }
      }

      if (!stats) {
        const serviceContainerId = await this.getContainerIdForService(containerID);
        if (serviceContainerId) {
          stats = await this.getContainerStatsRaw(serviceContainerId);
        }
      }

      if (!stats) {
        return null;
      }

      // Validate stats structure
      if (!stats || !stats.cpu_stats || !stats.cpu_stats.cpu_usage) {
        logger.warn(`Invalid stats structure for container ${containerID}`);
        return null;
      }

      // Calculate CPU percentage
      const cpuDelta =
        stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage || 0);
      const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage || 0);
      const cpuPercent =
        systemDelta > 0 ? (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100 : 0;

      // Memory stats
      const memoryUsed = stats.memory_stats?.usage || 0;
      const memoryLimit = stats.memory_stats?.limit || 0;
      const memoryPercent = memoryLimit > 0 ? (memoryUsed / memoryLimit) * 100 : 0;

      // Network stats
      let networkRx = 0;
      let networkTx = 0;
      if (stats.networks) {
        for (const network of Object.values(stats.networks)) {
          networkRx += (network as any).rx_bytes || 0;
          networkTx += (network as any).tx_bytes || 0;
        }
      }

      return {
        cpuPercent,
        memoryUsed,
        memoryLimit,
        memoryPercent,
        networkRx,
        networkTx,
      };
    } catch (error) {
      // Don't log errors for container not found - this is expected for Swarm services
      const errMsg = getErrorMessage(error);
      if (!errMsg.includes('No such container') && !errMsg.includes('not found')) {
        logger.error(`Failed to get container stats ${containerID}: ${errMsg}`);
      }
      return null;
    }
  }

  private static isDockerMissingError(errorArg: unknown): boolean {
    const errMsg = getErrorMessage(errorArg);
    return errMsg.includes('No such container') || errMsg.includes('not found') || errMsg.includes('status 404') || errMsg.includes('HTTP 404');
  }

  private async getContainerStatsRaw(containerIDArg: string): Promise<any> {
    const queryParams = new URLSearchParams({ stream: '0' });
    const rawStats = await this.requestDockerRaw(
      'GET',
      `/containers/${encodeURIComponent(containerIDArg)}/stats?${queryParams.toString()}`,
    );
    return JSON.parse(OneboxDockerManager.logTextDecoder.decode(rawStats));
  }

  /**
   * Helper: Get actual container ID for a Swarm service
   * For Swarm services, we need to find the task/container that's actually running
   */
  private async getContainerIdForService(serviceId: string): Promise<string | null> {
    try {
      // List all containers and find one with the service label matching our service ID
      const rawContainers = await this.requestDockerRaw('GET', '/containers/json');
      const containers = JSON.parse(OneboxDockerManager.logTextDecoder.decode(rawContainers));

      // Find a container that belongs to this service
      const serviceContainer = containers.find((container: any) => {
        const labels = container.Labels || {};
        // Swarm services have a com.docker.swarm.service.id label
        return labels['com.docker.swarm.service.id'] === serviceId;
      });

      if (serviceContainer) {
        return serviceContainer.Id;
      }

      return null;
    } catch (error) {
      logger.warn(`Failed to get container ID for service ${serviceId}: ${getErrorMessage(error)}`);
      return null;
    }
  }

  private static getDockerLogSource(streamTypeArg: number): IDockerLogEntry['source'] {
    return streamTypeArg === 2 ? 'stderr' : 'stdout';
  }

  private static concatenateLogBuffers(
    bufferArg: Uint8Array<ArrayBufferLike>,
    chunkArg: Uint8Array<ArrayBufferLike>,
  ): Uint8Array<ArrayBufferLike> {
    if (bufferArg.length === 0) return chunkArg;
    const nextBuffer = new Uint8Array(bufferArg.length + chunkArg.length);
    nextBuffer.set(bufferArg);
    nextBuffer.set(chunkArg, bufferArg.length);
    return nextBuffer;
  }

  private static getDockerLogFrameSize(bufferArg: Uint8Array<ArrayBufferLike>, offsetArg: number): number {
    return (
      ((bufferArg[offsetArg + 4] << 24) >>> 0) +
      (bufferArg[offsetArg + 5] << 16) +
      (bufferArg[offsetArg + 6] << 8) +
      bufferArg[offsetArg + 7]
    );
  }

  private static isDockerLogFrameHeader(bufferArg: Uint8Array<ArrayBufferLike>, offsetArg: number): boolean {
    if (bufferArg.length - offsetArg < 8) return false;
    const streamType = bufferArg[offsetArg];
    if (streamType !== 1 && streamType !== 2) return false;
    if (bufferArg[offsetArg + 1] !== 0 || bufferArg[offsetArg + 2] !== 0 || bufferArg[offsetArg + 3] !== 0) return false;
    return OneboxDockerManager.getDockerLogFrameSize(bufferArg, offsetArg) <= OneboxDockerManager.maxDockerLogFrameSize;
  }

  public static parseDockerLogFrames(
    bufferArg: Uint8Array<ArrayBufferLike>,
    optionsArg: IDockerLogParseOptions = {},
  ): IDockerLogParseResult {
    const entries: IDockerLogEntry[] = [];
    let offset = 0;

    while (bufferArg.length - offset >= 8) {
      if (!OneboxDockerManager.isDockerLogFrameHeader(bufferArg, offset)) {
        if (entries.length === 0) {
          return {
            entries: [{
              source: 'stdout',
              content: OneboxDockerManager.logTextDecoder.decode(bufferArg),
            }],
            remainder: new Uint8Array(0),
          };
        }
        break;
      }

      const frameSize = OneboxDockerManager.getDockerLogFrameSize(bufferArg, offset);

      if (bufferArg.length - offset < 8 + frameSize) {
        break;
      }

      const frameData = bufferArg.slice(offset + 8, offset + 8 + frameSize);
      entries.push({
        source: OneboxDockerManager.getDockerLogSource(bufferArg[offset]),
        content: OneboxDockerManager.logTextDecoder.decode(frameData),
      });
      offset += 8 + frameSize;
    }

    if (optionsArg.flushRemainderAsRaw && bufferArg.length > offset) {
      entries.push({
        source: 'stdout',
        content: OneboxDockerManager.logTextDecoder.decode(bufferArg.slice(offset)),
      });
      offset = bufferArg.length;
    }

    return {
      entries,
      remainder: bufferArg.slice(offset),
    };
  }

  public static parseDockerLogText(rawLogsArg: string): IDockerLogEntry[] {
    if (rawLogsArg.includes('\uFFFD')) {
      return [{ source: 'stdout', content: rawLogsArg }];
    }
    const rawBytes = OneboxDockerManager.logTextEncoder.encode(rawLogsArg);
    const parsed = OneboxDockerManager.parseDockerLogFrames(rawBytes, { flushRemainderAsRaw: true });
    return parsed.entries.length > 0 ? parsed.entries : [{ source: 'stdout', content: rawLogsArg }];
  }

  private async requestDockerRaw(methodArg: string, routeArg: string): Promise<Uint8Array<ArrayBufferLike>> {
    if (!this.dockerClient) {
      throw new Error('Docker client is not initialized');
    }
    const socketPath = this.dockerClient.socketPath;
    const requestOptions: any = {
      method: methodArg,
      headers: {
        Host: 'docker.sock',
      },
    };

    if (socketPath.startsWith('http://unix:')) {
      requestOptions.socketPath = socketPath.slice('http://unix:'.length).replace(/:$/, '');
      requestOptions.path = routeArg;
    } else {
      const baseUrl = new URL(socketPath);
      requestOptions.hostname = baseUrl.hostname;
      requestOptions.port = baseUrl.port || (baseUrl.protocol === 'https:' ? 443 : 80);
      requestOptions.path = routeArg;
    }

    const requestModule = socketPath.startsWith('https:') ? plugins.nodeHttps : plugins.nodeHttp;
    return await new Promise<Uint8Array<ArrayBufferLike>>((resolve, reject) => {
      const request = requestModule.request(requestOptions, (response) => {
        const chunks: Uint8Array<ArrayBufferLike>[] = [];
        response.on('data', (chunk: Uint8Array<ArrayBufferLike> | string) => {
          chunks.push(typeof chunk === 'string' ? OneboxDockerManager.logTextEncoder.encode(chunk) : chunk);
        });
        response.once('error', reject);
        response.once('end', () => {
          let body: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
          for (const chunk of chunks) {
            body = OneboxDockerManager.concatenateLogBuffers(body, chunk);
          }
          if ((response.statusCode || 0) >= 300) {
            reject(new Error(
              `Docker request failed with status ${response.statusCode}: ${OneboxDockerManager.logTextDecoder.decode(body)}`,
            ));
            return;
          }
          resolve(new Uint8Array(body));
        });
      });
      request.once('error', reject);
      request.setTimeout(30_000, () => {
        request.destroy(new Error(`Docker request timed out: ${methodArg} ${routeArg}`));
      });
      request.end();
    });
  }

  /**
   * Get container logs
   * Handles both regular containers and Swarm services
   */
  async getContainerLogEntries(
    containerID: string,
    tail = 100
  ): Promise<IDockerLogEntry[]> {
    try {
      try {
        return await this.getContainerLogEntriesById(containerID, tail);
      } catch (error) {
        if (!OneboxDockerManager.isDockerMissingError(error)) {
          throw error;
        }
        const serviceContainerId = await this.getContainerIdForService(containerID);
        if (!serviceContainerId) {
          throw new Error(`Container not found: ${containerID}`);
        }
        return await this.getContainerLogEntriesById(serviceContainerId, tail);
      }
    } catch (error) {
      logger.error(`Failed to get container logs ${containerID}: ${getErrorMessage(error)}`);
      return [];
    }
  }

  private async getContainerLogEntriesById(containerIDArg: string, tailArg: number): Promise<IDockerLogEntry[]> {
    const queryParams = new URLSearchParams({
      stdout: '1',
      stderr: '1',
      tail: String(tailArg),
      timestamps: '1',
    });
    const rawLogs = await this.requestDockerRaw(
      'GET',
      `/containers/${encodeURIComponent(containerIDArg)}/logs?${queryParams.toString()}`,
    );
    return OneboxDockerManager.parseDockerLogFrames(rawLogs, { flushRemainderAsRaw: true }).entries;
  }

  async getContainerLogs(
    containerID: string,
    tail = 100
  ): Promise<{ stdout: string; stderr: string }> {
    try {
      const parsedLogs = await this.getContainerLogEntries(containerID, tail);
      return parsedLogs.reduce(
        (acc, entry) => {
          acc[entry.source] += entry.content;
          return acc;
        },
        { stdout: '', stderr: '' },
      );
    } catch (error) {
      logger.error(`Failed to get container logs ${containerID}: ${getErrorMessage(error)}`);
      return { stdout: '', stderr: '' };
    }
  }

  /**
   * List all onebox-managed containers
   */
  async listContainers(): Promise<any[]> {
    try {
      const containers = await this.dockerClient!.listContainers();
      // Filter for onebox-managed containers
      return containers.filter((c: any) =>
        c.Labels && c.Labels['managed-by'] === 'onebox'
      );
    } catch (error) {
      logger.error(`Failed to list containers: ${getErrorMessage(error)}`);
      return [];
    }
  }

  /**
   * Check if Docker is running
   */
  async isDockerRunning(): Promise<boolean> {
    try {
      await this.dockerClient!.ping();
      return true;
    } catch (error) {
      return false;
    }
  }

  /**
   * Get Docker version info
   * Note: v5 API doesn't expose version() method, so we return a placeholder
   */
  async getDockerVersion(): Promise<any> {
    // v5 API doesn't have a version() method
    // Return a basic structure for compatibility
    return {
      Version: 'N/A',
      ApiVersion: 'N/A',
      Note: 'Version info not available in @apiclient.xyz/docker v5'
    };
  }

  /**
   * Prune unused images
   */
  async pruneImages(): Promise<void> {
    try {
      logger.info('Pruning unused Docker images...');
      await this.dockerClient!.pruneImages();
      logger.success('Unused images pruned successfully');
    } catch (error) {
      logger.error(`Failed to prune images: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Get container IP address in onebox network
   */
  async getContainerIP(containerID: string): Promise<string | null> {
    try {
      const container = await this.dockerClient!.getContainerById(containerID);

      if (!container) {
        throw new Error(`Container not found: ${containerID}`);
      }

      const info = await container.inspect();

      const networks = info.NetworkSettings.Networks;
      if (networks && networks[this.networkName]) {
        return networks[this.networkName].IPAddress;
      }

      return null;
    } catch (error) {
      logger.error(`Failed to get container IP ${containerID}: ${getErrorMessage(error)}`);
      return null;
    }
  }

  /**
   * Get host port binding for a container's exposed port
   * @returns The host port number, or null if not bound
   */
  async getContainerHostPort(containerID: string, containerPort: number): Promise<number | null> {
    try {
      const container = await this.dockerClient!.getContainerById(containerID);

      if (!container) {
        throw new Error(`Container not found: ${containerID}`);
      }

      const info = await container.inspect();

      const portKey = `${containerPort}/tcp`;
      const bindings = info.NetworkSettings.Ports?.[portKey];

      if (bindings && bindings.length > 0 && bindings[0].HostPort) {
        return parseInt(bindings[0].HostPort, 10);
      }

      return null;
    } catch (error) {
      logger.error(`Failed to get container host port ${containerID}:${containerPort}: ${getErrorMessage(error)}`);
      return null;
    }
  }

  /**
   * Execute a command in a running container
   */
  private async resolveContainer(containerID: string): Promise<any> {
    let container: any = null;
    try {
      container = await this.dockerClient!.getContainerById(containerID);
    } catch {
      // Not a direct container ID — try Swarm service lookup.
    }

    if (!container) {
      const serviceContainerId = await this.getContainerIdForService(containerID);
      if (serviceContainerId) {
        try {
          container = await this.dockerClient!.getContainerById(serviceContainerId);
        } catch {
          // Service container also not found.
        }
      }
    }

    if (!container) {
      throw new Error(`Container not found: ${containerID}`);
    }
    return container;
  }

  async execInContainer(
    containerID: string,
    cmd: string[]
  ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
    try {
      const container = await this.resolveContainer(containerID);

      const { stream, inspect } = await container.exec(cmd, {
        attachStdout: true,
        attachStderr: true,
      });

      let stdout = '';
      let stderr = '';
      let buffer: Uint8Array<ArrayBufferLike> = new Uint8Array(0);

      const appendExecEntries = (entriesArg: IDockerLogEntry[]) => {
        for (const entry of entriesArg) {
          if (entry.source === 'stderr') {
            stderr += entry.content;
          } else {
            stdout += entry.content;
          }
        }
      };

      stream.on('data', (chunk: Uint8Array<ArrayBufferLike>) => {
        buffer = OneboxDockerManager.concatenateLogBuffers(buffer, chunk);
        const parsed = OneboxDockerManager.parseDockerLogFrames(buffer);
        appendExecEntries(parsed.entries);
        buffer = parsed.remainder;
      });

      // Wait for completion with timeout
      await Promise.race([
        new Promise<void>((resolve) => stream.on('end', resolve)),
        new Promise<void>((_, reject) => setTimeout(() => reject(new Error('Exec timeout after 30s')), 30000))
      ]);

      const execInfo = await inspect();
      const exitCode = execInfo.ExitCode ?? -1;
      if (buffer.length > 0) {
        const parsed = OneboxDockerManager.parseDockerLogFrames(buffer, { flushRemainderAsRaw: true });
        appendExecEntries(parsed.entries);
      }

      return { stdout, stderr, exitCode };
    } catch (error) {
      logger.error(`Failed to exec in container ${containerID}: ${getErrorMessage(error)}`);
      return { stdout: '', stderr: getErrorMessage(error), exitCode: -1 };
    }
  }

  async startInteractiveExecInContainer(
    containerID: string,
    cmd: string[],
  ): Promise<IInteractiveContainerExec> {
    const container = await this.resolveContainer(containerID);
    return await container.exec(cmd, {
      tty: true,
      attachStdin: true,
      attachStdout: true,
      attachStderr: true,
    });
  }

  /**
   * Create a platform service container (MongoDB, SmartStorage, etc.)
   * Platform containers are long-running infrastructure services
   */
  async createPlatformContainer(options: {
    name: string;
    image: string;
    port: number;
    env: string[];
    volumes?: string[];
    volumeMounts?: IServiceVolume[];
    network: string;
    command?: string[];
    exposePorts?: number[];
    removeExistingNames?: string[];
  }): Promise<string> {
    try {
      logger.info(`Creating platform container: ${options.name}`);

      // Pull the image first to ensure it's available
      logger.info(`Pulling image for platform service: ${options.image}`);
      await this.pullImage(options.image);

      // Check running and stopped containers; stopped platform containers still reserve names.
      const existingContainersResponse = await this.dockerClient!.request('GET', '/containers/json?all=true', {});
      const existingContainers = Array.isArray(existingContainersResponse.body) ? existingContainersResponse.body : [];
      const namesToRemove = new Set([options.name, ...(options.removeExistingNames || [])]);
      const existingContainersToRemove = existingContainers.filter((containerArg: any) =>
        containerArg.Names?.some((nameArg: string) => {
          const normalizedName = nameArg.replace(/^\/+/, '');
          return namesToRemove.has(normalizedName);
        })
      );

      for (const existingContainer of existingContainersToRemove) {
        logger.info(`Platform container ${existingContainer.Names?.[0] || existingContainer.Id} already exists, removing old container...`);
        await this.removeContainer(existingContainer.Id, true);
      }

      // Prepare exposed ports
      const exposedPorts: Record<string, Record<string, never>> = {};
      const portBindings: Record<string, Array<{ HostIp: string; HostPort: string }>> = {};

      const portsToExpose = options.exposePorts || [options.port];
      for (const port of portsToExpose) {
        exposedPorts[`${port}/tcp`] = {};
        // Bind to random host port so we can access from host (for provisioning)
        portBindings[`${port}/tcp`] = [{ HostIp: '127.0.0.1', HostPort: '' }];
      }

      // Prepare volume bindings/mounts. volumeMounts are storage-class-resolved Docker volumes.
      const mounts = options.volumeMounts?.length
        ? this.getDockerVolumeMounts({ name: options.name, volumes: options.volumeMounts })
        : [];
      const binds: string[] = mounts.length === 0 ? options.volumes || [] : [];

      // Create the container
      const response = await this.dockerClient!.request('POST', `/containers/create?name=${options.name}`, {
        Image: options.image,
        Cmd: options.command,
        Env: options.env,
        Labels: {
          'managed-by': 'onebox',
          'onebox-platform-service': options.name,
        },
        ExposedPorts: exposedPorts,
        HostConfig: {
          NetworkMode: options.network,
          RestartPolicy: {
            Name: 'unless-stopped',
          },
          PortBindings: portBindings,
          ...(binds.length > 0 ? { Binds: binds } : {}),
          ...(mounts.length > 0 ? { Mounts: mounts } : {}),
        },
      });

      if (response.statusCode >= 300) {
        const errorMsg = response.body?.message || `HTTP ${response.statusCode}`;
        throw new Error(`Failed to create platform container: ${errorMsg}`);
      }

      const containerID = response.body.Id;
      logger.info(`Platform container created: ${containerID}`);

      // Start the container
      const startResponse = await this.dockerClient!.request('POST', `/containers/${containerID}/start`, {});

      if (startResponse.statusCode >= 300 && startResponse.statusCode !== 304) {
        throw new Error(`Failed to start platform container: HTTP ${startResponse.statusCode}`);
      }

      logger.success(`Platform container ${options.name} started successfully`);
      return containerID;
    } catch (error) {
      logger.error(`Failed to create platform container ${options.name}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  /**
   * Get a container by ID
   * Public wrapper for Docker client method
   */
  async getContainerById(containerID: string): Promise<any> {
    if (!this.dockerClient) {
      throw new Error('Docker client not initialized');
    }
    return this.dockerClient.getContainerById(containerID);
  }

  /**
   * List all containers
   * Public wrapper for Docker client method
   */
  async listAllContainers(): Promise<any[]> {
    if (!this.dockerClient) {
      throw new Error('Docker client not initialized');
    }
    return this.dockerClient.listContainers();
  }

  /**
   * Stream container logs continuously
   * @param containerID The container ID
   * @param callback Callback for each log line (line, isError)
   */
  async streamContainerLogs(
    containerID: string,
    callback: (line: string, isError: boolean) => void,
    optionsArg: { onClose?: () => void } = {},
  ): Promise<IContainerLogStreamHandle> {
    try {
      let logStream: any;
      try {
        logStream = await this.openContainerLogStreamById(containerID);
      } catch (error) {
        if (!OneboxDockerManager.isDockerMissingError(error)) {
          throw error;
        }
        const serviceContainerId = await this.getContainerIdForService(containerID);
        if (!serviceContainerId) {
          throw new Error(`Container not found: ${containerID}`);
        }
        logStream = await this.openContainerLogStreamById(serviceContainerId);
      }

      let closed = false;
      let buffer: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
      const maxBufferedLogLineLength = 64 * 1024;
      const lineBuffers: Record<IDockerLogEntry['source'], string> = { stdout: '', stderr: '' };
      const emitLine = (lineArg: string, sourceArg: IDockerLogEntry['source']) => {
        const normalizedLine = lineArg.endsWith('\r') ? lineArg.slice(0, -1) : lineArg;
        if (normalizedLine.length > 0) {
          callback(normalizedLine, sourceArg === 'stderr');
        }
      };
      const emitEntries = (entriesArg: IDockerLogEntry[]) => {
        for (const entry of entriesArg) {
          const source = entry.source;
          const combined = lineBuffers[source] + entry.content;
          const lines = combined.split('\n');
          lineBuffers[source] = lines.pop() ?? '';
          for (const line of lines) {
            emitLine(line, source);
          }
          while (lineBuffers[source].length >= maxBufferedLogLineLength) {
            emitLine(lineBuffers[source].slice(0, maxBufferedLogLineLength), source);
            lineBuffers[source] = lineBuffers[source].slice(maxBufferedLogLineLength);
          }
        }
      };
      const flushBuffers = () => {
        if (buffer.length > 0) {
          const parsed = OneboxDockerManager.parseDockerLogFrames(buffer, { flushRemainderAsRaw: true });
          emitEntries(parsed.entries);
          buffer = parsed.remainder;
        }
        for (const source of ['stdout', 'stderr'] as const) {
          const line = lineBuffers[source];
          lineBuffers[source] = '';
          if (line.length > 0) {
            emitLine(line, source);
          }
        }
      };
      const handleClose = () => {
        if (closed) return;
        closed = true;
        flushBuffers();
        optionsArg.onClose?.();
      };

      logStream.on('data', (chunk: Uint8Array<ArrayBufferLike>) => {
        buffer = OneboxDockerManager.concatenateLogBuffers(buffer, chunk);
        const parsed = OneboxDockerManager.parseDockerLogFrames(buffer);
        buffer = parsed.remainder;
        emitEntries(parsed.entries);
      });

      logStream.on('error', (err: Error) => {
        logger.error(`Log stream error for ${containerID}: ${err.message}`);
        try {
          logStream.destroy();
        } catch {
          // The stream may already be closed.
        }
        handleClose();
      });

      logStream.on('end', handleClose);
      logStream.on('close', handleClose);

      return {
        close: () => {
          try {
            logStream.destroy();
          } finally {
            handleClose();
          }
        },
      };
    } catch (error) {
      logger.error(`Failed to stream logs for ${containerID}: ${getErrorMessage(error)}`);
      throw error;
    }
  }

  private async openContainerLogStreamById(containerIDArg: string): Promise<any> {
    const queryParams = new URLSearchParams({
      stdout: '1',
      stderr: '1',
      follow: '1',
      timestamps: '1',
      tail: '100',
    });
    const logStream = await this.dockerClient!.requestStreaming(
      'GET',
      `/containers/${encodeURIComponent(containerIDArg)}/logs?${queryParams.toString()}`,
    );
    const statusCode = (logStream as any)?.statusCode || 0;
    if (statusCode >= 300) {
      (logStream as any)?.destroy?.();
      throw new Error(`Docker log stream failed with HTTP ${statusCode}`);
    }
    if (typeof (logStream as any)?.on !== 'function') {
      throw new Error('Docker log stream response did not include a readable stream');
    }
    return logStream;
  }
}
