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

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

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

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

export class OneboxDockerManager {
  private dockerClient: InstanceType<typeof plugins.docker.Docker> | null = null;
  private networkName = 'onebox-network';

  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: IService, 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 getSwarmVolumeMounts(serviceArg: IService): 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 ':'`);
      }
    }

    this.expandPublishedPorts(serviceArg);
  }

  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 {
          const listener = Deno.listen({ hostname: portArg.hostIp, port: portArg.publishedPort });
          listener.close();
        }
      } 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 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;
    }
  }

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

      // 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;
      }

      if (isSwarmMode) {
        return await this.createSwarmService(service);
      } else {
        return await this.createStandaloneContainer(service);
      }
    } 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): 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(service.envVars)) {
      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,
      Env: env,
      Labels: {
        'managed-by': 'onebox',
        'onebox-service': service.name,
      },
      ExposedPorts: portConfig.exposedPorts,
      HostConfig: {
        NetworkMode: this.networkName,
        RestartPolicy: {
          Name: 'unless-stopped',
        },
        PortBindings: portConfig.portBindings,
        Binds: this.getStandaloneVolumeBinds(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): 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(service.envVars)) {
      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',
      });
    }

    // 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,
          Env: env,
          Mounts: this.getSwarmVolumeMounts(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;
  }

  /**
   * 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): 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 url = force ? `/containers/${containerID}?force=true` : `/containers/${containerID}`;
      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}`);
  }

  /**
   * 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 {
      // Try to get container directly first
      let container: any = null;
      try {
        container = await this.dockerClient!.getContainerById(containerID);
      } catch {
        // Container not found by ID — might be a Swarm service ID
      }

      // If not found, it might be a service ID - try to get the actual container ID
      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) {
        return null;
      }

      const stats = await container.stats({ stream: false });

      // 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;
    }
  }

  /**
   * 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 containers = await this.dockerClient!.listContainers();

      // 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;
    }
  }

  /**
   * Get container logs
   * Handles both regular containers and Swarm services
   */
  async getContainerLogs(
    containerID: string,
    tail = 100
  ): Promise<{ stdout: string; stderr: string }> {
    try {
      let actualContainerId = containerID;

      // Try to get container directly first
      let container = await this.dockerClient!.getContainerById(containerID);

      // If not found, it might be a service ID - try to get the actual container ID
      if (!container) {
        const serviceContainerId = await this.getContainerIdForService(containerID);
        if (serviceContainerId) {
          actualContainerId = serviceContainerId;
          container = await this.dockerClient!.getContainerById(serviceContainerId);
        }
      }

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

      // Get logs as string (v5 handles demultiplexing automatically)
      const logs = await container.logs({
        stdout: true,
        stderr: true,
        tail: tail,
        timestamps: true,
      });

      // v5 returns already-parsed logs as a string
      return {
        stdout: logs,
        stderr: '', // v5 combines stdout/stderr into single string
      };
    } 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 = '';

      stream.on('data', (chunk: Uint8Array) => {
        const streamType = chunk[0];
        const content = new TextDecoder().decode(chunk.slice(8));

        if (streamType === 1) {
          stdout += content;
        } else if (streamType === 2) {
          stderr += content;
        }
      });

      // 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;

      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, MinIO, etc.)
   * Platform containers are long-running infrastructure services
   */
  async createPlatformContainer(options: {
    name: string;
    image: string;
    port: number;
    env: string[];
    volumes?: string[];
    network: string;
    command?: string[];
    exposePorts?: number[];
  }): 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 existing = existingContainers.find((c: any) =>
        c.Names?.some((n: string) => n === `/${options.name}` || n === options.name)
      );

      if (existing) {
        logger.info(`Platform container ${options.name} already exists, removing old container...`);
        await this.removeContainer(existing.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
      const binds: string[] = 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: binds,
        },
      });

      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
  ): Promise<void> {
    try {
      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}`);
      }

      const logStream = await container.streamLogs({
        stdout: true,
        stderr: true,
        timestamps: true,
        tail: 100,
      });

      logStream.on('data', (chunk: Uint8Array) => {
        // Docker multiplexes stdout/stderr with 8-byte header
        // Byte 0: stream type (1=stdout, 2=stderr)
        // Bytes 4-7: frame size (big-endian)
        // Rest: actual log data
        const streamType = chunk[0];
        const isError = streamType === 2;
        const content = new TextDecoder().decode(chunk.slice(8));
        if (content.trim()) {
          callback(content.trim(), isError);
        }
      });

      logStream.on('error', (err: Error) => {
        logger.error(`Log stream error for ${containerID}: ${err.message}`);
      });
    } catch (error) {
      logger.error(`Failed to stream logs for ${containerID}: ${getErrorMessage(error)}`);
      throw error;
    }
  }
}
